Files
pulse-signage/src/web/public/js/slide-form.js
T
2026-07-25 15:11:41 +01:00

1258 lines
45 KiB
JavaScript

import {
Alignment,
BlockQuote,
Bold,
ClassicEditor,
Base64UploadAdapter,
ClipboardPipeline,
Essentials,
FontBackgroundColor,
FontColor,
FontFamily,
FontSize,
Heading,
HorizontalLine,
Image,
ImageBlockEditing,
ImageCaption,
ImageInlineEditing,
ImageInsert,
ImageInsertUI,
ImageInsertViaUrl,
ImageInsertViaUrlUI,
ImageResize,
ImageResizeEditing,
ImageResizeHandles,
ImageStyle,
ImageTextAlternative,
ImageUpload,
Indent,
IndentBlock,
Italic,
InputTextView,
Plugin,
Paragraph,
Strikethrough,
Subscript,
Superscript,
Underline
} from '/assets/js/vendor/ckeditor5/ckeditor5.js';
import { createTemplateSelectorLockController } from '/assets/js/slides/slide-form-template-lock.js';
(function () {
var DEFAULT_FONT_SIZE = 32;
var dataElement = document.getElementById('slide-editor-data');
if (!dataElement) {
return;
}
var slideEditorData = {};
try {
slideEditorData = JSON.parse(dataElement.getAttribute('data-json') || dataElement.textContent || '{}') || {};
} catch (_error) {
slideEditorData = {};
}
var templates = Array.isArray(slideEditorData.templates) ? slideEditorData.templates : [];
var existingTemplateId = slideEditorData.existingTemplateId !== undefined ? slideEditorData.existingTemplateId : null;
var existingContent = slideEditorData.existingContent || {};
var templateSelect = document.getElementById('template-select');
var templateFields = document.getElementById('template-fields');
var slideForm = document.getElementById('slide-form');
var slideEditorShell = document.querySelector('.slide-editor-shell');
var slideEditorSidebar = document.querySelector('.slide-editor-sidebar');
var slidePreviewMeta = document.getElementById('slide-preview-meta');
var slidePreviewStage = document.getElementById('slide-preview-stage');
var slidePreviewCanvas = document.getElementById('slide-preview-canvas');
var slidePreviewBackground = document.getElementById('slide-preview-background');
var slidePreviewEmpty = document.getElementById('slide-preview-empty');
var slidePreviewOverlay = document.getElementById('slide-preview-overlay');
var openPreviewPopupButton = document.getElementById('open-preview-popup');
var editorInstances = new Map();
var submitting = false;
var previewRenderFrame = 0;
var previewPopupWindow = null;
var previewPopupRenderFrame = 0;
var currentPreviewCanvasWidth = 0;
var currentPreviewCanvasHeight = 0;
var sidebarSyncFrame = 0;
var sidebarTopOffset = 16;
var templateSelectorLock = createTemplateSelectorLockController(templateSelect);
class FontSizeInputUI extends Plugin {
static get pluginName() {
return 'FontSizeInputUI';
}
init() {
var editor = this.editor;
var fontSizeCommand = editor.commands.get('fontSize');
var plugin = this;
if (!fontSizeCommand) {
return;
}
editor.ui.componentFactory.add('fontSizeInput', function (locale) {
var view = new InputTextView(locale);
view.extendTemplate({
attributes: {
class: ['ck-font-size-input']
}
});
view.set({
placeholder: 'Font size',
ariaLabel: 'Font size'
});
function getCurrentFontSizeValue() {
var value = fontSizeCommand.value;
if (value === null || value === undefined || value === '') {
return String(DEFAULT_FONT_SIZE);
}
var parsed = Math.round(Number(String(value).replace(/[^0-9.]/g, '')));
return Number.isFinite(parsed) && parsed > 0 ? String(parsed) : '';
}
function syncValue() {
if (view.element && document.activeElement === view.element) {
return;
}
view.value = getCurrentFontSizeValue();
}
view.on('render', function () {
if (!view.element) {
return;
}
view.element.type = 'text';
view.element.inputMode = 'numeric';
view.element.autocomplete = 'off';
view.element.spellcheck = false;
view.element.style.width = '3.5em';
syncValue();
});
function applyValue() {
if (!view.element) {
return;
}
var size = Math.max(1, Math.round(Number(view.element.value || 0)));
if (!size) {
return;
}
editor.execute('fontSize', { value: size + 'px' });
}
view.on('input', applyValue);
view.on('blur', applyValue);
view.on('keydown', function (_eventInfo, domEvent) {
if (domEvent.key === 'Enter') {
domEvent.preventDefault();
if (view.element) {
view.element.blur();
}
}
});
plugin.listenTo(editor.model.document.selection, 'change:range', syncValue);
plugin.listenTo(fontSizeCommand, 'change:value', syncValue);
syncValue();
return view;
});
}
}
class ForcePlainTextPaste extends Plugin {
static get pluginName() {
return 'ForcePlainTextPaste';
}
static get requires() {
return [ClipboardPipeline];
}
init() {
var editor = this.editor;
var clipboardPipeline = editor.plugins.get(ClipboardPipeline);
this.listenTo(clipboardPipeline, 'inputTransformation', function (_event, data) {
if (clipboardContainsImageContent(data.dataTransfer)) {
return;
}
var plainText = data.dataTransfer.getData('text/plain');
data.content = editor.data.processor.toView(plainTextToHtml(plainText));
}, { priority: 'high' });
}
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function plainTextToHtml(value) {
var lines = String(value ?? '').replace(/\r\n?/g, '\n').split('\n');
var paragraphs = [];
var currentParagraph = [];
lines.forEach(function (line) {
if (line === '') {
if (currentParagraph.length) {
paragraphs.push('<p>' + currentParagraph.join('<br />') + '</p>');
currentParagraph = [];
} else {
paragraphs.push('<p></p>');
}
return;
}
currentParagraph.push(escapeHtml(line));
});
if (currentParagraph.length) {
paragraphs.push('<p>' + currentParagraph.join('<br />') + '</p>');
}
return paragraphs.length ? paragraphs.join('') : '<p></p>';
}
function clipboardContainsImageContent(dataTransfer) {
if (!dataTransfer) {
return false;
}
var files = Array.from(dataTransfer.files || []);
if (files.some(function (file) {
return file && String(file.type || '').indexOf('image/') === 0;
})) {
return true;
}
var html = dataTransfer.getData('text/html') || '';
return /<img\b|<figure\b[^>]*\bclass=["'][^"']*\bimage\b/i.test(html);
}
function getTemplateById(id) {
return templates.find(function (item) { return Number(item.id) === Number(id); }) || null;
}
function getCurrentRegionValue(region) {
var current = existingContent[region.region_key];
return current && current.value !== undefined ? current.value : '';
}
function normalizeEditorData(value) {
return String(value || '');
}
function requestPreviewRender() {
if (previewRenderFrame) {
return;
}
previewRenderFrame = window.requestAnimationFrame(function () {
previewRenderFrame = 0;
renderPreview();
});
}
function getTemplateFieldControls() {
return templateFields ? templateFields.querySelectorAll('input, textarea, select') : [];
}
function sanitizePreviewHtml(html) {
var container = document.createElement('div');
container.innerHTML = String(html || '');
return sanitizePreviewNode(container);
}
function sanitizePreviewNode(node) {
var allowed = ['a', 'b', 'blockquote', 'br', 'code', 'div', 'em', 'figure', 'figcaption', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'li', 'ol', 'p', 'pre', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'th', 'thead', 'tr', 'u', 'ul'];
var output = '';
Array.from(node.childNodes || []).forEach(function (child) {
if (child.nodeType === Node.TEXT_NODE) {
output += escapeHtml(child.textContent);
return;
}
if (child.nodeType !== Node.ELEMENT_NODE) {
return;
}
var name = String(child.tagName || '').toLowerCase();
if (allowed.indexOf(name) === -1) {
return;
}
if (name === 'br' || name === 'hr') {
output += '<' + name + '>';
return;
}
var attrs = sanitizePreviewElementAttributes(child, name);
if (name === 'img') {
output += '<img' + attrs + '>';
return;
}
output += '<' + name + attrs + '>' + sanitizePreviewNode(child) + '</' + name + '>';
});
return output;
}
function sanitizePreviewElementAttributes(element, tagName) {
var allowedAttributes = {
a: ['href', 'title', 'target', 'rel', 'class', 'style'],
blockquote: ['class', 'style'],
div: ['class', 'style'],
figure: ['class', 'style'],
figcaption: ['class', 'style'],
h1: ['class', 'style'],
h2: ['class', 'style'],
h3: ['class', 'style'],
h4: ['class', 'style'],
h5: ['class', 'style'],
h6: ['class', 'style'],
img: ['alt', 'class', 'decoding', 'height', 'loading', 'src', 'style', 'title', 'width'],
li: ['class', 'style'],
ol: ['class', 'style', 'start'],
p: ['class', 'style'],
pre: ['class', 'style'],
span: ['class', 'style'],
table: ['class', 'style'],
td: ['class', 'style', 'colspan', 'rowspan'],
th: ['class', 'style', 'colspan', 'rowspan', 'scope'],
tr: ['class', 'style'],
ul: ['class', 'style']
};
var allowed = allowedAttributes[tagName] || [];
var attrs = [];
Array.from(element.attributes || []).forEach(function (attribute) {
var lowerKey = String(attribute.name || '').toLowerCase();
if (allowed.indexOf(lowerKey) === -1) {
return;
}
var value = String(attribute.value || '');
if (tagName === 'img' && lowerKey === 'src') {
value = sanitizeImageSrc(value);
}
if (lowerKey === 'href' && /^(?:\s*javascript:|\s*data:)/i.test(value)) {
return;
}
if (tagName === 'img' && lowerKey === 'src' && !value) {
return;
}
if (lowerKey === 'style' && /(?:expression\s*\(|javascript:|url\s*\()/i.test(value)) {
return;
}
if (lowerKey === 'target') {
var targetValue = value.trim();
if (targetValue === '_blank') {
attrs.push(' target="_blank"');
if (attrs.indexOf(' rel="noreferrer noopener"') === -1) {
attrs.push(' rel="noreferrer noopener"');
}
return;
}
}
attrs.push(' ' + lowerKey + '="' + escapeHtml(value) + '"');
});
return attrs.join('');
}
function sanitizeTagAttributes(tagName, attrText) {
var allowedAttributes = {
a: ['href', 'title', 'target', 'rel', 'class', 'style'],
blockquote: ['class', 'style'],
div: ['class', 'style'],
figure: ['class', 'style'],
figcaption: ['class', 'style'],
h1: ['class', 'style'],
h2: ['class', 'style'],
h3: ['class', 'style'],
h4: ['class', 'style'],
h5: ['class', 'style'],
h6: ['class', 'style'],
img: ['alt', 'class', 'decoding', 'height', 'loading', 'src', 'style', 'title', 'width'],
li: ['class', 'style'],
ol: ['class', 'style', 'start'],
p: ['class', 'style'],
pre: ['class', 'style'],
span: ['class', 'style'],
table: ['class', 'style'],
td: ['class', 'style', 'colspan', 'rowspan'],
th: ['class', 'style', 'colspan', 'rowspan', 'scope'],
tr: ['class', 'style'],
ul: ['class', 'style']
};
var allowed = allowedAttributes[tagName] || [];
if (!allowed.length) {
return '';
}
var attrs = [];
attrText.replace(/([a-zA-Z0-9:-]+)(?:\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>/=`]+)))?/g, function (_full, key, _valuePart, doubleQuoted, singleQuoted, bareValue) {
var lowerKey = String(key || '').toLowerCase();
if (allowed.indexOf(lowerKey) === -1) {
return '';
}
var value = doubleQuoted !== undefined ? doubleQuoted : singleQuoted !== undefined ? singleQuoted : bareValue !== undefined ? bareValue : '';
if (tagName === 'img' && lowerKey === 'src') {
value = sanitizeImageSrc(value);
}
if (lowerKey === 'href' && /^(?:\s*javascript:|\s*data:)/i.test(String(value || ''))) {
return '';
}
if (tagName === 'img' && lowerKey === 'src' && !value) {
return '';
}
if (lowerKey === 'style' && /(?:expression\s*\(|javascript:|url\s*\()/i.test(String(value || ''))) {
return '';
}
if (lowerKey === 'target') {
var targetValue = String(value || '').trim();
if (targetValue === '_blank') {
attrs.push(' target="_blank"');
if (attrs.indexOf(' rel="noreferrer noopener"') === -1) {
attrs.push(' rel="noreferrer noopener"');
}
return '';
}
}
attrs.push(' ' + lowerKey + '="' + escapeHtml(value) + '"');
return '';
});
return attrs.join('');
}
function sanitizeImageSrc(value) {
var raw = String(value || '').trim();
if (!raw) {
return '';
}
if (/^(?:https?:|\/|\.\.?\/|\/\/)/i.test(raw)) {
return raw;
}
if (/^data:image\/(?:png|jpe?g|gif|webp|bmp);base64,/i.test(raw)) {
return raw;
}
return '';
}
function renderPreviewListItem(item, tag) {
if (item && typeof item === 'object') {
var content = item.content !== undefined ? item.content : (item.text !== undefined ? item.text : item.value !== undefined ? item.value : '');
var children = Array.isArray(item.items) ? item.items : Array.isArray(item.subItems) ? item.subItems : Array.isArray(item.children) ? item.children : [];
var nested = children.length ? '<' + tag + ' style="list-style-type:' + (tag === 'ol' ? 'decimal' : 'disc') + ';padding-left:1.4em;">' + children.map(function (child) { return renderPreviewListItem(child, tag); }).join('') + '</' + tag + '>' : '';
return '<li>' + sanitizePreviewHtml(content || '') + nested + '</li>';
}
return '<li>' + sanitizePreviewHtml(item || '') + '</li>';
}
function renderPreviewTable(data) {
var rows = Array.isArray(data.content) ? data.content : Array.isArray(data.rows) ? data.rows : [];
if (!rows.length) {
return '';
}
var hasHeadings = Boolean(data.withHeadings);
var body = rows.map(function (row, rowIndex) {
var cells = Array.isArray(row) ? row : [];
var cellTag = hasHeadings && rowIndex === 0 ? 'th' : 'td';
var cellAttrs = cellTag === 'th' ? ' scope="col"' : '';
return '<tr>' + cells.map(function (cell) {
return '<' + cellTag + cellAttrs + '>' + sanitizePreviewHtml(cell || '') + '</' + cellTag + '>';
}).join('') + '</tr>';
}).join('');
return '<table class="slide-preview-table">' + body + '</table>';
}
function renderPreviewBlock(block) {
if (!block || !block.type || !block.data) {
return '';
}
if (block.type === 'header') {
var level = Math.max(1, Math.min(6, Number(block.data.level || 2)));
return '<h' + level + '>' + sanitizePreviewHtml(block.data.text || '') + '</h' + level + '>';
}
if (block.type === 'list') {
var tag = block.data.style === 'ordered' ? 'ol' : 'ul';
var items = Array.isArray(block.data.items) ? block.data.items : [];
return '<' + tag + ' style="list-style-type:' + (tag === 'ol' ? 'decimal' : 'disc') + ';padding-left:1.4em;">' + items.map(function (item) { return renderPreviewListItem(item, tag); }).join('') + '</' + tag + '>';
}
if (block.type === 'delimiter') {
return '<hr />';
}
if (block.type === 'code') {
return '<pre><code>' + sanitizePreviewHtml(block.data.code || '') + '</code></pre>';
}
if (block.type === 'table') {
return renderPreviewTable(block.data);
}
if (block.type === 'paragraph') {
return '<p>' + sanitizePreviewHtml(block.data.text || '') + '</p>';
}
return '';
}
function renderPreviewText(value) {
var raw = String(value || '');
if (!raw) {
return '<div class="slide-preview-placeholder">Empty text</div>';
}
return sanitizePreviewHtml(raw);
}
function renderPreviewTextRegion(region, value, style, scale) {
var fontFamily = style.font_family ? 'font-family:' + escapeHtml(style.font_family) + ';' : '';
var color = style.font_color ? 'color:' + escapeHtml(style.font_color) + ';' : '';
var fontSize = style.font_size ? 'font-size:' + Math.max(1, Math.round(Number(style.font_size))) + 'px;' : '';
var width = Math.max(1, Math.round(Number(region && region.width ? region.width : 0) || 1));
var height = Math.max(1, Math.round(Number(region && region.height ? region.height : 0) || 1));
var wrapperStyle = 'width:' + width + 'px;height:' + height + 'px;overflow:hidden;' + fontFamily + fontSize + color;
return '<div class="slide-preview-text-content" style="' + wrapperStyle + '">' + renderPreviewText(value) + '</div>';
}
function renderPreviewWebpageRegion(value) {
var src = String(value || '').trim();
if (!src) {
return '<div class="slide-preview-placeholder">Webpage</div>';
}
return '<iframe class="slide-preview-webpage-frame" src="' + escapeHtml(src) + '" title="Webpage preview" loading="eager" referrerpolicy="no-referrer" scrolling="no" style="width:100%;height:100%;border:0;display:block;background:#fff;overflow:hidden;"></iframe>';
}
function renderPreviewHtmlRegion(value) {
var html = String(value || '').trim();
if (!html) {
return '<div class="slide-preview-placeholder">HTML</div>';
}
return '<iframe class="slide-preview-html-frame" sandbox="" scrolling="no" srcdoc="<!doctype html><html><head><style>html,body{margin:0;width:100%;height:100%;overflow:hidden;background:transparent;}</style></head><body>' + escapeHtml(html) + '</body></html>" title="HTML preview" loading="eager" style="width:100%;height:100%;border:0;display:block;background:transparent;overflow:hidden;"></iframe>';
}
function destroyEditors() {
editorInstances.forEach(function (editor) {
if (editor && typeof editor.destroy === 'function') {
editor.destroy();
}
});
editorInstances.clear();
}
function getEditorHiddenInput(regionId) {
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
return card ? card.querySelector('input[type="hidden"][name="region_text_' + regionId + '"]') : null;
}
function getEditorFontSizeHiddenInput(regionId) {
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
return card ? card.querySelector('input[name="region_font_size_' + regionId + '"]') : null;
}
function normalizeFontSizeValue(value) {
var size = Math.max(1, Math.round(Number(String(value || '').replace(/[^0-9.]/g, ''))));
return size ? String(size) : '';
}
function getCurrentTextStyle(region) {
var current = existingContent[region.region_key] || {};
return {
font_family: String(current.font_family || region.font_family || 'Arial').trim() || 'Arial',
font_size: Math.max(8, Number(current.font_size || DEFAULT_FONT_SIZE)),
font_color: String(current.font_color || region.font_color || '#000000').trim() || '#000000'
};
}
function getTextStyleFromCard(_card, region) {
return getCurrentTextStyle(region);
}
function getSelectedTemplate() {
return templates.find(function (item) { return Number(item.id) === Number(templateSelect.value); }) || null;
}
function clearImagePreviewUrls() {
templateFields.querySelectorAll('input[type="file"][name^="region_image_"]').forEach(function (input) {
if (input.dataset.previewUrl) {
URL.revokeObjectURL(input.dataset.previewUrl);
delete input.dataset.previewUrl;
}
});
}
function getRegionBox(region, canvasWidth, canvasHeight) {
return {
left: Math.max(0, Math.round(Number(region.x || 0))),
top: Math.max(0, Math.round(Number(region.y || 0))),
width: Math.max(1, Math.round(Number(region.width || 0) || 1)),
height: Math.max(1, Math.round(Number(region.height || 0) || 1))
};
}
function getPreviewCanvasDimensions(template) {
return {
width: Math.max(1, Number(template && template.canvas_size_width ? template.canvas_size_width : 1920)),
height: Math.max(1, Number(template && template.canvas_size_height ? template.canvas_size_height : 1080))
};
}
function buildPreviewPopupHtml() {
return '<!doctype html>' +
'<html>' +
'<head>' +
'<meta charset="utf-8" />' +
'<meta name="viewport" content="width=device-width, initial-scale=1" />' +
'<title>Slide preview</title>' +
'<style>' +
'html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#111;color:#fff;font-family:Arial,sans-serif;}' +
'#popup-preview-stage{position:relative;width:100%;height:100%;overflow:hidden;background:#111;}' +
'#popup-preview-canvas{position:absolute;top:0;left:0;transform-origin:top left;will-change:transform;}' +
'#popup-preview-canvas,#popup-preview-stage{box-sizing:border-box;}' +
'.slide-preview-background,.slide-preview-overlay,.slide-preview-empty{position:absolute;inset:0;}' +
'.slide-preview-background{width:100%;height:100%;object-fit:contain;display:block;z-index:1;}' +
'.slide-preview-overlay{z-index:2;}' +
'.slide-preview-empty{display:flex;align-items:center;justify-content:center;padding:1rem;text-align:center;color:rgba(255,255,255,0.75);z-index:0;}' +
'.slide-preview-region{position:absolute;overflow:hidden;box-sizing:border-box;}' +
'.slide-preview-image{width:100%;height:100%;object-fit:contain;display:block;}' +
'.slide-preview-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,0.65);background:rgba(255,255,255,0.06);font-size:0.9rem;}' +
'</style>' +
'</head>' +
'<body>' +
'<div id="popup-preview-stage">' +
'<div id="popup-preview-canvas"></div>' +
'</div>' +
'</body>' +
'</html>';
}
function syncPopupPreview() {
if (!previewPopupWindow || previewPopupWindow.closed) {
previewPopupWindow = null;
return;
}
var popupDocument = previewPopupWindow.document;
if (!popupDocument) {
return;
}
var popupStage = popupDocument.getElementById('popup-preview-stage');
var popupCanvas = popupDocument.getElementById('popup-preview-canvas');
var template = getSelectedTemplate();
if (!popupStage || !popupCanvas) {
if (previewPopupRenderFrame) {
cancelAnimationFrame(previewPopupRenderFrame);
}
previewPopupRenderFrame = requestAnimationFrame(syncPopupPreview);
return;
}
if (!template) {
popupStage.style.backgroundColor = '#111111';
popupCanvas.style.width = '';
popupCanvas.style.height = '';
popupCanvas.style.transform = '';
popupCanvas.innerHTML = slidePreviewCanvas.innerHTML;
var popupEmpty = popupCanvas.querySelector('#slide-preview-empty');
var popupOverlay = popupCanvas.querySelector('#slide-preview-overlay');
var popupBackground = popupCanvas.querySelector('#slide-preview-background');
if (popupBackground) {
popupBackground.style.display = 'none';
}
if (popupEmpty) {
popupEmpty.style.display = 'flex';
popupEmpty.textContent = 'Select a template to preview the slide.';
}
if (popupOverlay) {
popupOverlay.innerHTML = '';
}
return;
}
var canvasWidth = currentPreviewCanvasWidth || Math.max(1, Number(template.canvas_size_width || 1920));
var canvasHeight = currentPreviewCanvasHeight || Math.max(1, Number(template.canvas_size_height || 1080));
popupStage.style.backgroundColor = template.background_color || '#111111';
popupCanvas.style.width = canvasWidth + 'px';
popupCanvas.style.height = canvasHeight + 'px';
popupCanvas.innerHTML = slidePreviewCanvas.innerHTML;
var popupBackground = popupCanvas.querySelector('#slide-preview-background');
var popupEmpty = popupCanvas.querySelector('#slide-preview-empty');
var popupOverlay = popupCanvas.querySelector('#slide-preview-overlay');
if (popupBackground) {
popupBackground.style.display = template.background_image_path ? 'block' : 'none';
popupBackground.src = template.background_image_path || '';
}
if (popupEmpty) {
popupEmpty.style.display = 'none';
}
var stageRect = popupStage.getBoundingClientRect();
if (!stageRect.width || !stageRect.height) {
if (previewPopupRenderFrame) {
cancelAnimationFrame(previewPopupRenderFrame);
}
previewPopupRenderFrame = requestAnimationFrame(syncPopupPreview);
return;
}
var scale = Math.min(stageRect.width / canvasWidth, stageRect.height / canvasHeight) || 1;
popupCanvas.style.transform = 'translate3d(0, 0, 0) scale(' + scale + ')';
}
function openPreviewPopup() {
if (previewPopupWindow && !previewPopupWindow.closed) {
previewPopupWindow.focus();
syncPopupPreview();
return;
}
var popupWidth = 1280;
var popupHeight = 900;
var left = Math.max(0, Math.round((window.screen.width - popupWidth) / 2));
var top = Math.max(0, Math.round((window.screen.height - popupHeight) / 2));
var popupFeatures = [
'popup=yes',
'toolbar=no',
'location=no',
'menubar=no',
'directories=no',
'status=no',
'scrollbars=yes',
'resizable=yes',
'width=' + popupWidth,
'height=' + popupHeight,
'left=' + left,
'top=' + top
].join(',');
previewPopupWindow = window.open('', 'slide-preview-popup', popupFeatures);
if (!previewPopupWindow) {
return;
}
previewPopupWindow.document.open();
previewPopupWindow.document.write(buildPreviewPopupHtml());
previewPopupWindow.document.close();
previewPopupWindow.addEventListener('resize', syncPopupPreview);
previewPopupWindow.focus();
syncPopupPreview();
}
function syncSlideEditorSidebar() {
sidebarSyncFrame = 0;
if (!slideEditorShell || !slideEditorSidebar) {
return;
}
if (window.matchMedia && window.matchMedia('(max-width: 1199.98px)').matches) {
slideEditorSidebar.style.transform = '';
return;
}
var shellRect = slideEditorShell.getBoundingClientRect();
var sidebarHeight = slideEditorSidebar.offsetHeight;
var shellTop = window.scrollY + shellRect.top;
var shellBottom = shellTop + slideEditorShell.offsetHeight;
var maxTranslate = Math.max(0, shellBottom - shellTop - sidebarHeight - sidebarTopOffset);
var desiredTranslate = Math.max(0, Math.min(window.scrollY + sidebarTopOffset - shellTop, maxTranslate));
slideEditorSidebar.style.transform = desiredTranslate ? 'translate3d(0, ' + desiredTranslate + 'px, 0)' : '';
}
function requestSidebarSync() {
if (sidebarSyncFrame) {
return;
}
sidebarSyncFrame = window.requestAnimationFrame(syncSlideEditorSidebar);
}
function renderPreview() {
var template = getSelectedTemplate();
if (!template) {
slidePreviewMeta.textContent = '--';
slidePreviewBackground.style.display = 'none';
slidePreviewEmpty.style.display = 'flex';
slidePreviewEmpty.textContent = 'Select a template to preview the slide.';
slidePreviewCanvas.style.width = '';
slidePreviewCanvas.style.height = '';
slidePreviewOverlay.innerHTML = '';
slidePreviewOverlay.style.width = '';
slidePreviewOverlay.style.height = '';
slidePreviewOverlay.style.transform = '';
slidePreviewStage.style.aspectRatio = '16 / 9';
slidePreviewStage.style.backgroundColor = '#111111';
return;
}
var canvasSize = getPreviewCanvasDimensions(template);
var canvasWidth = canvasSize.width;
var canvasHeight = canvasSize.height;
currentPreviewCanvasWidth = canvasWidth;
currentPreviewCanvasHeight = canvasHeight;
slidePreviewStage.style.aspectRatio = canvasWidth + ' / ' + canvasHeight;
slidePreviewStage.style.backgroundColor = template.background_color || '#111111';
slidePreviewMeta.textContent = canvasWidth + 'x' + canvasHeight;
if (!(template.regions || []).length) {
slidePreviewEmpty.style.display = 'flex';
slidePreviewEmpty.textContent = 'This template has no regions.';
slidePreviewBackground.style.display = template.background_image_path ? 'block' : 'none';
slidePreviewBackground.src = template.background_image_path || '';
slidePreviewCanvas.style.width = canvasWidth + 'px';
slidePreviewCanvas.style.height = canvasHeight + 'px';
slidePreviewCanvas.style.transform = '';
slidePreviewOverlay.innerHTML = '';
slidePreviewOverlay.style.width = '';
slidePreviewOverlay.style.height = '';
slidePreviewOverlay.style.transform = '';
return;
}
slidePreviewEmpty.style.display = 'none';
slidePreviewBackground.style.display = template.background_image_path ? 'block' : 'none';
slidePreviewBackground.src = template.background_image_path || '';
var stageRect = slidePreviewStage.getBoundingClientRect();
if (!stageRect.width || !stageRect.height) {
if (previewRenderFrame) {
cancelAnimationFrame(previewRenderFrame);
}
previewRenderFrame = requestAnimationFrame(function () {
previewRenderFrame = 0;
renderPreview();
});
return;
}
var scale = Math.min(stageRect.width / canvasWidth, stageRect.height / canvasHeight) || 1;
slidePreviewCanvas.style.width = canvasWidth + 'px';
slidePreviewCanvas.style.height = canvasHeight + 'px';
slidePreviewCanvas.style.transform = 'translate3d(0, 0, 0) scale(' + scale + ')';
slidePreviewCanvas.style.transformOrigin = 'top left';
var regions = template.regions || [];
slidePreviewOverlay.innerHTML = regions.map(function (region) {
var box = getRegionBox(region, canvasWidth, canvasHeight);
var card = templateFields.querySelector('[data-region-id="' + region.id + '"]');
var hiddenInput = card && card.querySelector('input[type="hidden"][name="region_text_' + region.id + '"]');
var htmlInput = card && card.querySelector('textarea[name="region_html_' + region.id + '"]');
var imageHidden = card && card.querySelector('input[type="hidden"][name="existing_region_image_' + region.id + '"]');
var imageFileInput = card && card.querySelector('input[type="file"][name="region_image_' + region.id + '"]');
var webpageInput = card && card.querySelector('input[type="url"][name="region_webpage_' + region.id + '"]');
var value = region.region_type === 'image'
? ((imageFileInput && imageFileInput.dataset.previewUrl) ? imageFileInput.dataset.previewUrl : (imageHidden ? imageHidden.value : ''))
: region.region_type === 'webpage'
? (webpageInput ? webpageInput.value : '')
: region.region_type === 'html'
? (htmlInput ? htmlInput.value : '')
: (hiddenInput ? hiddenInput.value : '');
var style = region.region_type === 'image'
? null
: region.region_type === 'webpage'
? null
: region.region_type === 'html'
? null
: getTextStyleFromCard(card, region);
var content = region.region_type === 'image'
? (value ? '<img class="slide-preview-image" src="' + escapeHtml(value) + '" alt="" />' : '<div class="slide-preview-placeholder">Image</div>')
: region.region_type === 'webpage'
? renderPreviewWebpageRegion(value)
: region.region_type === 'html'
? renderPreviewHtmlRegion(value)
: renderPreviewTextRegion(region, value, style, scale);
var selected = region.region_type === 'image'
? ' slide-preview-image-region'
: region.region_type === 'webpage'
? ' slide-preview-webpage-region'
: region.region_type === 'html'
? ' slide-preview-html-region'
: ' slide-preview-text-region';
return '<div class="slide-preview-region' + selected + '" style="left:' + box.left + 'px;top:' + box.top + 'px;width:' + box.width + 'px;height:' + box.height + 'px;">' + content + '</div>';
}).join('');
syncPopupPreview();
}
function renderTextRegion(region) {
var current = getCurrentRegionValue(region);
return '' +
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '">' +
'<div class="card-header template-field-head">' +
'<strong>' + escapeHtml(region.label) + '</strong>' +
'<div class="template-field-actions">' +
'<button type="button" class="btn btn-sm btn-outline-secondary template-editor-toggle" data-toggle-editor-height="' + region.id + '">Expand</button>' +
'<span class="chip">Text</span>' +
'</div>' +
'</div>' +
'<div class="card-body p-3 d-grid gap-3">' +
'<div class="ckeditor-holder" data-region-id="' + region.id + '">' +
'<textarea class="ckeditor-source" rows="10">' + escapeHtml(current) + '</textarea>' +
'</div>' +
'<input type="hidden" name="region_font_size_' + region.id + '" value="' + escapeHtml(getCurrentTextStyle(region).font_size) + '" />' +
'<input type="hidden" name="region_text_' + region.id + '" value="' + escapeHtml(current) + '" />' +
'</div>' +
'</div>';
}
function renderWebpageRegion(region) {
var current = getCurrentRegionValue(region);
return '' +
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '">' +
'<div class="card-header template-field-head">' +
'<strong>' + escapeHtml(region.label) + '</strong>' +
'<span class="chip">Webpage</span>' +
'</div>' +
'<div class="card-body p-3 d-grid gap-3">' +
'<label style="display:block;">Webpage URL' +
'<input type="url" name="region_webpage_' + region.id + '" class="form-control" value="' + escapeHtml(current) + '" placeholder="https://example.com" />' +
'</label>' +
'<div class="muted slide-image-file">The webpage will be loaded in an iframe.</div>' +
'</div>' +
'</div>';
}
function renderHtmlRegion(region) {
var current = getCurrentRegionValue(region);
return '' +
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '">' +
'<div class="card-header template-field-head">' +
'<strong>' + escapeHtml(region.label) + '</strong>' +
'<span class="chip">HTML</span>' +
'</div>' +
'<div class="card-body p-3 d-grid gap-3">' +
'<label style="display:block;">HTML content' +
'<textarea name="region_html_' + region.id + '" class="form-control" rows="10" placeholder="<div>Hello</div>">' + escapeHtml(current) + '</textarea>' +
'</label>' +
'<div class="muted slide-image-file">HTML is rendered in a sandboxed iframe preview.</div>' +
'</div>' +
'</div>';
}
function renderImageRegion(region) {
var current = getCurrentRegionValue(region);
return '' +
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '">' +
'<div class="card-header template-field-head">' +
'<strong>' + escapeHtml(region.label) + '</strong>' +
'<span class="chip">Image</span>' +
'</div>' +
'<div class="card-body p-3 d-grid gap-3">' +
'<label style="display:block;">Image file' +
'<input type="file" name="region_image_' + region.id + '" class="form-control" accept="image/*" />' +
'</label>' +
'<input type="hidden" name="existing_region_image_' + region.id + '" value="' + escapeHtml(current) + '" />' +
(current
? '<div class="muted slide-image-file">Current: <a href="' + escapeHtml(current) + '" target="_blank" rel="noreferrer">' + escapeHtml(current) + '</a></div>'
: '<div class="muted slide-image-file">No image selected.</div>') +
'</div>' +
'</div>';
}
function renderTemplate() {
templateSelectorLock.reset();
clearImagePreviewUrls();
destroyEditors();
var template = getTemplateById(templateSelect.value);
if (!template) {
templateFields.innerHTML = '';
requestPreviewRender();
return;
}
templateFields.innerHTML = template.regions.length ? template.regions.map(function (region) {
if (region.region_type === 'image') {
return renderImageRegion(region);
}
if (region.region_type === 'webpage') {
return renderWebpageRegion(region);
}
if (region.region_type === 'html') {
return renderHtmlRegion(region);
}
return renderTextRegion(region);
}).join('') : '<div class="muted">This template has no regions.</div>';
templateFields.querySelectorAll('.ckeditor-holder').forEach(function (holder) {
var regionId = holder.getAttribute('data-region-id');
var source = holder.querySelector('.ckeditor-source');
var hidden = getEditorHiddenInput(regionId);
var editorConfig = {
licenseKey: 'GPL',
placeholder: 'Write content...',
toolbar: {
items: [
'undo',
'redo',
'|',
'fontSizeInput',
'fontFamily',
'fontColor',
'fontBackgroundColor',
'|',
'bold',
'italic',
'underline',
'strikethrough',
'subscript',
'superscript',
'|',
'horizontalLine',
'blockQuote',
'|',
'alignment',
'|',
'outdent',
'indent',
'|',
'imageInsert',
'imageInsertViaUrl',
'imageStyle:block',
'imageStyle:inline',
'imageTextAlternative',
'imageResize'
],
shouldNotGroupWhenFull: false
},
plugins: [
Alignment,
Base64UploadAdapter,
BlockQuote,
Bold,
Essentials,
ForcePlainTextPaste,
FontBackgroundColor,
FontColor,
FontFamily,
FontSize,
FontSizeInputUI,
HorizontalLine,
Indent,
IndentBlock,
Italic,
Image,
ImageBlockEditing,
ImageCaption,
ImageInlineEditing,
ImageInsert,
ImageInsertUI,
ImageInsertViaUrl,
ImageInsertViaUrlUI,
ImageResize,
ImageResizeEditing,
ImageResizeHandles,
ImageStyle,
ImageTextAlternative,
ImageUpload,
Paragraph,
Strikethrough,
Subscript,
Superscript,
Underline
],
fontFamily: {
supportAllValues: true
},
fontSize: {
options: [20, 24, 28, 32, 36, 40, 44],
supportAllValues: true
},
initialData: normalizeEditorData((source && source.value) || (hidden ? hidden.value : ''))
};
ClassicEditor.create(source || holder, editorConfig).then(function (editor) {
editorInstances.set(regionId, editor);
var fontSizeHidden = getEditorFontSizeHiddenInput(regionId);
var fontSizeCommand = editor.commands.get('fontSize');
if (hidden) {
hidden.value = editor.getData();
}
if (fontSizeHidden && fontSizeCommand) {
fontSizeHidden.value = normalizeFontSizeValue(fontSizeCommand.value) || fontSizeHidden.value;
fontSizeCommand.on('change:value', function () {
fontSizeHidden.value = normalizeFontSizeValue(fontSizeCommand.value) || fontSizeHidden.value;
});
}
editor.model.document.on('change:data', function () {
var instance = editorInstances.get(regionId);
if (instance !== editor) {
return;
}
if (hidden) {
hidden.value = editor.getData();
}
templateSelectorLock.markEdited();
requestPreviewRender();
});
requestPreviewRender();
}).catch(function (error) {
console.error('Failed to initialize CKEditor.', error);
});
});
templateFields.querySelectorAll('[data-toggle-editor-height]').forEach(function (button) {
button.addEventListener('click', function () {
var regionId = button.getAttribute('data-toggle-editor-height');
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
if (!card) {
return;
}
var expanded = card.classList.toggle('is-expanded');
button.textContent = expanded ? 'Collapse' : 'Expand';
});
});
templateFields.querySelectorAll('input[type="file"][name^="region_image_"]').forEach(function (input) {
input.addEventListener('change', function () {
if (input.dataset.previewUrl) {
URL.revokeObjectURL(input.dataset.previewUrl);
delete input.dataset.previewUrl;
}
if (input.files && input.files.length) {
input.dataset.previewUrl = URL.createObjectURL(input.files[0]);
}
templateSelectorLock.markEdited();
requestPreviewRender();
});
});
window.requestAnimationFrame(function () {
templateSelectorLock.arm();
});
requestPreviewRender();
}
slideForm.addEventListener('submit', async function (event) {
if (submitting) {
return;
}
event.preventDefault();
var submitterValue = event.submitter && event.submitter.name === 'save_action'
? String(event.submitter.value || '').trim().toLowerCase()
: '';
var saves = Array.prototype.map.call(templateFields.querySelectorAll('.ckeditor-holder'), function (holder) {
var regionId = holder.getAttribute('data-region-id');
var editor = editorInstances.get(regionId);
var hidden = getEditorHiddenInput(regionId);
if (!editor || !hidden) {
return Promise.resolve();
}
hidden.value = editor.getData();
return Promise.resolve();
});
submitting = true;
try {
await Promise.all(saves);
var formData = new FormData(slideForm);
if (event.submitter && event.submitter.name) {
formData.set(event.submitter.name, event.submitter.value || '');
}
var response = await fetch(slideForm.action, {
method: (slideForm.method || 'POST').toUpperCase(),
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'text/html, application/json, text/plain, */*'
},
body: formData,
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error(await response.text() || 'Unable to save slide.');
}
if (submitterValue === 'close' || submitterValue === 'new') {
var redirectUrl = submitterValue === 'close'
? String(slideForm.dataset.asyncSaveCloseUrl || response.url || slideForm.action)
: String(slideForm.dataset.asyncSaveNewUrl || response.url || slideForm.action);
window.location.replace(redirectUrl);
return;
}
var responseText = await response.text();
var savedMessage = '';
try {
var doc = new DOMParser().parseFromString(responseText || '', 'text/html');
var toastBody = doc.querySelector('.toast-body');
if (toastBody && toastBody.textContent) {
savedMessage = toastBody.textContent.trim();
}
} catch (_error) {
savedMessage = '';
}
showToast(savedMessage || 'Saved slide.', 'success');
slideForm.dataset.dirty = 'false';
} finally {
submitting = false;
}
});
slideForm.addEventListener('input', function () {
slideForm.dataset.dirty = 'true';
}, true);
slideForm.addEventListener('change', function () {
slideForm.dataset.dirty = 'true';
}, true);
if (openPreviewPopupButton) {
openPreviewPopupButton.addEventListener('click', function () {
openPreviewPopup();
});
}
templateSelect.addEventListener('change', renderTemplate);
templateFields.addEventListener('change', function () {
templateSelectorLock.markEdited();
requestPreviewRender();
});
templateFields.addEventListener('input', function () {
templateSelectorLock.markEdited();
requestPreviewRender();
});
if (existingTemplateId) {
templateSelect.value = String(existingTemplateId);
}
renderTemplate();
if (window.ResizeObserver && slidePreviewStage) {
var previewResizeObserver = new ResizeObserver(function () {
requestPreviewRender();
});
previewResizeObserver.observe(slidePreviewStage);
}
requestSidebarSync();
window.addEventListener('scroll', requestSidebarSync, { passive: true });
window.addEventListener('resize', requestPreviewRender);
window.addEventListener('resize', requestSidebarSync);
window.addEventListener('beforeunload', function () {
if (previewPopupWindow && !previewPopupWindow.closed) {
previewPopupWindow.close();
}
});
})();