This PR breaks the large web and player bootstrap files into smaller modules with clearer ownership.
Web changes: Split shared helpers, bootstrap logic, route groups, and upload-sync behavior out of web.js. Kept web.js focused on wiring and server startup. Fixed screen playlist reassignment so changing a screen’s playlist now triggers a refresh. Fixed single-slide playlist refresh behavior so updates do not get stuck behind the current slide. Player changes: Split websocket/runtime handling into runtime.js. Split playlist assembly and revision hashing into playlist.js. Split onboarding and player HTTP routes into dedicated modules. Split render utilities and template loading into render-helpers.js. Kept player.js mostly as startup/orchestration. Validation: Rebuilt both services with Docker Compose. Smoke-checked web and player routes after the refactor. Verified get_errors was clean on the touched modules.
This commit is contained in:
+358
-48
@@ -21,7 +21,8 @@ import {
|
||||
Subscript,
|
||||
Superscript,
|
||||
Underline
|
||||
} from '/assets/js/ckeditor5/ckeditor5.js';
|
||||
} from '/assets/js/vendor/ckeditor5/ckeditor5.js';
|
||||
import { createTemplateSelectorLockController } from '/assets/js/slides/slide-form-template-lock.js';
|
||||
|
||||
(function () {
|
||||
var dataElement = document.getElementById('slide-editor-data');
|
||||
@@ -42,13 +43,25 @@ import {
|
||||
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() {
|
||||
@@ -211,6 +224,21 @@ import {
|
||||
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 output = String(html || '');
|
||||
output = output.replace(/<script[\s\S]*?<\/script>/gi, '');
|
||||
@@ -445,30 +473,221 @@ import {
|
||||
};
|
||||
}
|
||||
|
||||
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 = 'Select a template to preview the slide.';
|
||||
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 canvasWidth = Math.max(1, Number(template.canvas_size_width || 1920));
|
||||
var canvasHeight = Math.max(1, Number(template.canvas_size_height || 1080));
|
||||
var canvasSize = getPreviewCanvasDimensions(template);
|
||||
var canvasWidth = canvasSize.width;
|
||||
var canvasHeight = canvasSize.height;
|
||||
currentPreviewCanvasWidth = canvasWidth;
|
||||
currentPreviewCanvasHeight = canvasHeight;
|
||||
slidePreviewStage.style.aspectRatio = canvasWidth + ' / ' + canvasHeight;
|
||||
slidePreviewMeta.textContent = template.name + ' • ' + canvasWidth + 'x' + 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 = '';
|
||||
@@ -481,11 +700,21 @@ import {
|
||||
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;
|
||||
slidePreviewOverlay.style.width = canvasWidth + 'px';
|
||||
slidePreviewOverlay.style.height = canvasHeight + 'px';
|
||||
slidePreviewOverlay.style.transform = 'scale(' + scale + ')';
|
||||
slidePreviewOverlay.style.transformOrigin = 'top left';
|
||||
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);
|
||||
@@ -525,79 +754,93 @@ import {
|
||||
: ' 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 template-field-card" data-region-id="' + region.id + '">' +
|
||||
'<div class="template-field-head">' +
|
||||
'<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">Text</span>' +
|
||||
'<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="ckeditor-holder" data-region-id="' + region.id + '">' +
|
||||
'<textarea class="ckeditor-source" rows="10">' + escapeHtml(current) + '</textarea>' +
|
||||
'<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>' +
|
||||
'<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>';
|
||||
}
|
||||
|
||||
function renderWebpageRegion(region) {
|
||||
var current = getCurrentRegionValue(region);
|
||||
return '' +
|
||||
'<div class="card template-field-card" data-region-id="' + region.id + '">' +
|
||||
'<div class="template-field-head">' +
|
||||
'<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>' +
|
||||
'<label style="display:block; margin-top:12px;">Webpage URL' +
|
||||
'<input type="url" name="region_webpage_' + region.id + '" value="' + escapeHtml(current) + '" placeholder="https://example.com" />' +
|
||||
'</label>' +
|
||||
'<div class="muted slide-image-file">The webpage will be loaded in an iframe.</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 template-field-card" data-region-id="' + region.id + '">' +
|
||||
'<div class="template-field-head">' +
|
||||
'<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>' +
|
||||
'<label style="display:block; margin-top:12px;">HTML content' +
|
||||
'<textarea name="region_html_' + region.id + '" 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 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 template-field-card" data-region-id="' + region.id + '">' +
|
||||
'<div class="template-field-head">' +
|
||||
'<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>' +
|
||||
'<label style="display:block; margin-top:12px;">Image file' +
|
||||
'<input type="file" name="region_image_' + region.id + '" 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 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 = '';
|
||||
renderPreview();
|
||||
requestPreviewRender();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -712,14 +955,28 @@ import {
|
||||
if (hidden) {
|
||||
hidden.value = editor.getData();
|
||||
}
|
||||
renderPreview();
|
||||
templateSelectorLock.markEdited();
|
||||
requestPreviewRender();
|
||||
});
|
||||
renderPreview();
|
||||
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) {
|
||||
@@ -729,10 +986,14 @@ import {
|
||||
if (input.files && input.files.length) {
|
||||
input.dataset.previewUrl = URL.createObjectURL(input.files[0]);
|
||||
}
|
||||
renderPreview();
|
||||
templateSelectorLock.markEdited();
|
||||
requestPreviewRender();
|
||||
});
|
||||
});
|
||||
renderPreview();
|
||||
window.requestAnimationFrame(function () {
|
||||
templateSelectorLock.arm();
|
||||
});
|
||||
requestPreviewRender();
|
||||
}
|
||||
|
||||
slideForm.addEventListener('submit', async function (event) {
|
||||
@@ -740,6 +1001,9 @@ import {
|
||||
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);
|
||||
@@ -753,13 +1017,17 @@ import {
|
||||
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: new FormData(slideForm),
|
||||
body: formData,
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
@@ -767,6 +1035,14 @@ import {
|
||||
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 {
|
||||
@@ -779,19 +1055,53 @@ import {
|
||||
savedMessage = '';
|
||||
}
|
||||
|
||||
showToast(savedMessage || 'Saved slide.');
|
||||
window.location.href = response.url || slideForm.action;
|
||||
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', renderPreview);
|
||||
templateFields.addEventListener('input', renderPreview);
|
||||
templateFields.addEventListener('change', function () {
|
||||
templateSelectorLock.markEdited();
|
||||
requestPreviewRender();
|
||||
});
|
||||
templateFields.addEventListener('input', function () {
|
||||
templateSelectorLock.markEdited();
|
||||
requestPreviewRender();
|
||||
});
|
||||
if (existingTemplateId) {
|
||||
templateSelect.value = String(existingTemplateId);
|
||||
}
|
||||
renderTemplate();
|
||||
window.addEventListener('resize', renderPreview);
|
||||
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();
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user