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, '>') .replace(/"/g, '"') .replace(/'/g, '''); } 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('

' + currentParagraph.join('
') + '

'); currentParagraph = []; } else { paragraphs.push('

'); } return; } currentParagraph.push(escapeHtml(line)); }); if (currentParagraph.length) { paragraphs.push('

' + currentParagraph.join('
') + '

'); } return paragraphs.length ? paragraphs.join('') : '

'; } 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 /]*\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 += ''; return; } output += '<' + name + attrs + '>' + sanitizePreviewNode(child) + ''; }); 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('') + '' : ''; return '
  • ' + sanitizePreviewHtml(content || '') + nested + '
  • '; } return '
  • ' + sanitizePreviewHtml(item || '') + '
  • '; } 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 '' + cells.map(function (cell) { return '<' + cellTag + cellAttrs + '>' + sanitizePreviewHtml(cell || '') + ''; }).join('') + ''; }).join(''); return '' + body + '
    '; } 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 '' + sanitizePreviewHtml(block.data.text || '') + ''; } 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('') + ''; } if (block.type === 'delimiter') { return '
    '; } if (block.type === 'code') { return '
    ' + sanitizePreviewHtml(block.data.code || '') + '
    '; } if (block.type === 'table') { return renderPreviewTable(block.data); } if (block.type === 'paragraph') { return '

    ' + sanitizePreviewHtml(block.data.text || '') + '

    '; } return ''; } function renderPreviewText(value) { var raw = String(value || ''); if (!raw) { return '
    Empty text
    '; } 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 '
    ' + renderPreviewText(value) + '
    '; } function renderPreviewWebpageRegion(value) { var src = String(value || '').trim(); if (!src) { return '
    Webpage
    '; } return ''; } function renderPreviewHtmlRegion(value) { var html = String(value || '').trim(); if (!html) { return '
    HTML
    '; } return ''; } 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 '' + '' + '' + '' + '' + 'Slide preview' + '' + '' + '' + '' + '' + ''; } 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 ? '' : '
    Image
    ') : 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 '
    ' + content + '
    '; }).join(''); syncPopupPreview(); } function renderTextRegion(region) { var current = getCurrentRegionValue(region); return '' + '
    ' + '
    ' + '' + escapeHtml(region.label) + '' + '
    ' + '' + 'Text' + '
    ' + '
    ' + '
    ' + '
    ' + '' + '
    ' + '' + '' + '
    ' + '
    '; } function renderWebpageRegion(region) { var current = getCurrentRegionValue(region); return '' + '
    ' + '
    ' + '' + escapeHtml(region.label) + '' + 'Webpage' + '
    ' + '
    ' + '' + '
    The webpage will be loaded in an iframe.
    ' + '
    ' + '
    '; } function renderHtmlRegion(region) { var current = getCurrentRegionValue(region); return '' + '
    ' + '
    ' + '' + escapeHtml(region.label) + '' + 'HTML' + '
    ' + '
    ' + '' + '
    HTML is rendered in a sandboxed iframe preview.
    ' + '
    ' + '
    '; } function renderImageRegion(region) { var current = getCurrentRegionValue(region); return '' + '
    ' + '
    ' + '' + escapeHtml(region.label) + '' + 'Image' + '
    ' + '
    ' + '' + '' + (current ? '' : '
    No image selected.
    ') + '
    ' + '
    '; } 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('') : '
    This template has no regions.
    '; 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(); } }); })();