Files
pulse-signage/src/web/public/js/slides/slide-form.js
T
lzstealth 203d0bfc01
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m49s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 32s
Release 2.8.0
2026-08-16 17:15:31 +01:00

994 lines
40 KiB
JavaScript

import { createTemplateSelectorLockController } from '/assets/js/slides/slide-form-template-lock.js';
import { createSlideFormEditorController } from '/assets/js/slides/slide-form-editor.js';
import { createSlideFormRegionHelpers } from '/assets/js/slides/slide-form-regions.js';
import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-preview.js';
(function () {
var DEFAULT_FONT_SIZE = 32;
function formatUploadLimitLabel(bytes) {
var megabytes = Number(bytes || 0) / 1024 / 1024;
if (megabytes >= 1024 && megabytes % 1024 === 0) {
return String(megabytes / 1024) + ' GB';
}
return (Number.isInteger(megabytes) ? String(megabytes) : megabytes.toFixed(2).replace(/0+$/, '').replace(/\.$/, '')) + ' MB';
}
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 rssFeeds = Array.isArray(slideEditorData.rssFeeds) ? slideEditorData.rssFeeds : [];
var apiSources = Array.isArray(slideEditorData.apiSources) ? slideEditorData.apiSources : [];
var timetableGroups = Array.isArray(slideEditorData.timetableGroups) ? slideEditorData.timetableGroups : [];
var fontStylesheetHref = String(slideEditorData.fontStylesheetHref || '').trim();
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 uploadLimits = slideEditorData.uploadLimits || {};
var uploadMaxBytes = Number(uploadLimits.imageMaxBytes || 100 * 1024 * 1024);
var uploadMaxLabel = formatUploadLimitLabel(uploadMaxBytes);
var uploadVideoMaxBytes = Number(uploadLimits.videoMaxBytes || 1024 * 1024 * 1024);
var uploadVideoMaxLabel = formatUploadLimitLabel(uploadVideoMaxBytes);
var wysiwygImageUploadMaxBytes = Number(uploadLimits.wysiwygImageMaxBytes || 2 * 1024 * 1024);
var wysiwygImageUploadLimitLabel = formatUploadLimitLabel(wysiwygImageUploadMaxBytes);
var uploadMimeTypes = Array.isArray(uploadLimits.allowedMimeTypes) ? uploadLimits.allowedMimeTypes : [];
var videoDurationCache = Object.create(null);
var previewRenderFrame = 0;
var previewPopupWindow = null;
var previewPopupRenderFrame = 0;
var previewPopupFeatures = '';
var currentPreviewCanvasWidth = 0;
var currentPreviewCanvasHeight = 0;
var sidebarSyncFrame = 0;
var sidebarBaseOffset = 16;
var templateSelectorLock = createTemplateSelectorLockController(templateSelect);
var regionTypes = window.pulseRegionTypes || {};
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function getRegionTypeModule(regionType) {
if (regionTypes && typeof regionTypes.get === 'function') {
return regionTypes.get(regionType);
}
return regionTypes && regionTypes[regionType] ? regionTypes[regionType] : null;
}
function getTemplateById(id) {
return templates.find(function (item) { return Number(item.id) === Number(id); }) || null;
}
function applyBackdropStyle(element, backgroundColor, backgroundImagePath) {
if (!element) {
return;
}
var color = String(backgroundColor || '#111111').trim() || '#111111';
element.style.backgroundColor = color;
element.style.backgroundImage = backgroundImagePath ? 'url("' + encodeURI(String(backgroundImagePath)) + '")' : 'none';
element.style.backgroundPosition = 'center';
element.style.backgroundSize = 'cover';
element.style.backgroundRepeat = 'no-repeat';
}
function applyPopupBackdropStyle(element, backgroundColor) {
if (!element) {
return;
}
var color = String(backgroundColor || '#111111').trim() || '#111111';
element.style.backgroundColor = color;
element.style.backgroundImage = 'none';
element.style.backgroundPosition = 'center';
element.style.backgroundSize = '100% 100%';
element.style.backgroundRepeat = 'no-repeat';
}
var slideFormEditorController = createSlideFormEditorController({
templateFields: templateFields,
templateSelectorLock: templateSelectorLock,
defaultFontSize: DEFAULT_FONT_SIZE,
fontFamilyFormats: slideEditorData.fontFamilyFormats || '',
fontStylesheetHref: fontStylesheetHref,
imageUploadMaxBytes: wysiwygImageUploadMaxBytes,
imageUploadLimitLabel: wysiwygImageUploadLimitLabel,
imageUploadContext: 'wysiwyg',
uploadMimeTypes: uploadMimeTypes,
getRegionTypeModule: getRegionTypeModule,
markFormDirty: function () {
slideForm.dataset.dirty = 'true';
},
getEditorBackgroundColor: function () {
var template = getTemplateById(templateSelect.value);
return template && template.background_color ? String(template.background_color) : '#111111';
},
requestPreviewRender: requestPreviewRender
});
var regionUtils = window.pulseRegionUtils || {};
var slideRegionHelperOptions = {
templateFields: templateFields,
existingContent: existingContent,
rssFeeds: rssFeeds,
apiSources: apiSources,
timetableGroups: timetableGroups,
defaultFontSize: DEFAULT_FONT_SIZE,
uploadMaxLabel: uploadMaxLabel,
uploadVideoMaxLabel: uploadVideoMaxLabel,
uploadMaxBytes: uploadMaxBytes,
uploadVideoMaxBytes: uploadVideoMaxBytes,
uploadMimeTypes: uploadMimeTypes,
escapeHtml: escapeHtml,
getRegionTypeModule: getRegionTypeModule,
requestPreviewRender: requestPreviewRender
};
var slideRegionHelpers = createSlideFormRegionHelpers(slideRegionHelperOptions);
var slidePreviewHelpers = createSlideFormPreviewHelpers({
getRegionTypeModule: getRegionTypeModule,
escapeHtml: escapeHtml
});
var sanitizePreviewHtml = slidePreviewHelpers.sanitizePreviewHtml;
var sanitizeTagAttributes = slidePreviewHelpers.sanitizeTagAttributes;
var renderPreviewListItem = slidePreviewHelpers.renderPreviewListItem;
var renderPreviewTable = slidePreviewHelpers.renderPreviewTable;
var renderPreviewBlock = slidePreviewHelpers.renderPreviewBlock;
var renderPreviewText = slidePreviewHelpers.renderPreviewText;
var renderPreviewTextRegion = slidePreviewHelpers.renderPreviewTextRegion;
var buildRegionEditorCardContext = slideRegionHelpers.buildRegionEditorCardContext;
var getRegionEditorCardHtml = slideRegionHelpers.getRegionEditorCardHtml;
var bindRegionCards = slideRegionHelpers.bindRegionCards;
var syncRegionCards = slideRegionHelpers.syncRegionCards;
var getPreviewRegionContent = slideRegionHelpers.getPreviewRegionContent;
var buildPreviewRenderContext = slideRegionHelpers.buildPreviewRenderContext;
var startPreviewVideoPlayback = slidePreviewHelpers.startPreviewVideoPlayback;
function getSelectedTemplate() {
return templates.find(function (item) { return Number(item.id) === Number(templateSelect.value); }) || null;
}
function hasMeaningfulValue(value) {
if (value === null || value === undefined) {
return false;
}
if (Array.isArray(value)) {
return value.some(hasMeaningfulValue);
}
if (typeof value === 'object') {
return Object.keys(value).some(function (key) {
return hasMeaningfulValue(value[key]);
});
}
return String(value).trim() !== '';
}
function hasExistingSlideContent() {
return hasMeaningfulValue(existingContent);
}
function clearImagePreviewUrls() {
templateFields.querySelectorAll('input[type="file"][name^="region_image_"], input[type="file"][name^="region_video_"]').forEach(function (input) {
if (input.dataset.previewUrl) {
URL.revokeObjectURL(input.dataset.previewUrl);
delete input.dataset.previewUrl;
}
});
}
function getRegionVideoDurationHiddenInput(regionId) {
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
return card ? card.querySelector('input[type="hidden"][name="existing_region_video_duration_' + regionId + '"]') : null;
}
function getRegionEditorRegionType(card) {
if (!card) {
return '';
}
if (card.dataset && String(card.dataset.regionMediaType || '').trim()) {
return String(card.dataset.regionMediaType || '').trim();
}
if (card.querySelector && card.querySelector('input[type="file"][name^="region_qr_image_"], input[type="url"][name^="region_qr_code_"]')) {
return 'qr-code';
}
var regionTypeInput = card.querySelector ? card.querySelector('input[type="hidden"][name="region_type[]"]') : null;
return String(regionTypeInput && regionTypeInput.value || '').trim();
}
function getRegionEditorModule(card) {
return getRegionTypeModule(getRegionEditorRegionType(card));
}
var regionMediaController = regionUtils.createRegionMediaUploadController ? regionUtils.createRegionMediaUploadController(templateFields, {
getCardByRegionId: function (regionId) {
return templateFields.querySelector('[data-region-id="' + regionId + '"]');
},
handleMediaSelection: function (input, file) {
return handleRegionMediaFileSelection(input, file);
},
renderPreview: function (card, value) {
renderRegionMediaPreview(card, value);
},
syncDuration: function (card, source) {
return syncVideoRegionDuration(card, source);
},
markEdited: function () {
templateSelectorLock.arm();
templateSelectorLock.markEdited();
requestPreviewRender();
},
requestPreviewRender: function () {
requestPreviewRender();
}
}) : null;
function buildRegionMediaHookContext(input, file) {
var regionId = input && input.name ? input.name.replace(/^region_(?:image|video|qr_image)_/, '') : '';
var card = regionId ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null;
return {
input: input,
file: file || null,
card: card,
regionId: regionId,
regionType: getRegionEditorRegionType(card),
setInputFile: function (nextFile) {
return regionMediaController ? regionMediaController.setInputFile(input, nextFile) : null;
},
uploadFile: function (nextFile) {
return regionMediaController ? regionMediaController.uploadFile(input, nextFile || file) : Promise.resolve(null);
}
};
}
function handleRegionMediaFileSelection(input, file) {
var card = input && input.name ? templateFields.querySelector('[data-region-id="' + input.name.replace(/^region_(?:image|video|qr_image)_/, '') + '"]') : null;
var module = getRegionEditorModule(card);
var hookContext = buildRegionMediaHookContext(input, file);
if (module && typeof module.handleMediaInputChange === 'function') {
return module.handleMediaInputChange(hookContext);
}
return false;
}
function renderRegionMediaPreview(card, value) {
if (!card) {
return;
}
var previewBox = card.querySelector('.slide-image-region-preview-box');
if (!previewBox) {
return;
}
var mediaType = card && card.dataset && String(card.dataset.regionMediaType || '').trim() === 'video'
? 'video'
: card && card.querySelector && card.querySelector('input[type="file"][name^="region_qr_image_"]')
? 'qr-image'
: 'image';
var isVideo = mediaType === 'video';
var isQrImage = mediaType === 'qr-image';
previewBox.innerHTML = value
? '<div class="slide-image-region-preview-shell" data-remove-' + (isVideo ? 'region-video' : isQrImage ? 'region-qr-image' : 'region-image') + '="' + escapeHtml(card.getAttribute('data-region-id') || '') + '" role="button" tabindex="0" aria-label="Remove ' + (isVideo ? 'video' : isQrImage ? 'QR image' : 'image') + '">' +
(isVideo
? '<video class="slide-image-region-preview" src="' + escapeHtml(value) + '" autoplay loop muted playsinline preload="metadata"></video>'
: '<img class="slide-image-region-preview" src="' + escapeHtml(value) + '" alt="' + (isQrImage ? 'Current QR image preview' : 'Current image preview') + '" />') +
'<span class="slide-image-region-preview-remove" aria-hidden="true"><i class="bi bi-trash3" aria-hidden="true"></i></span>' +
'</div>'
: '<div class="slide-image-region-preview slide-image-region-preview-empty">No ' + (isVideo ? 'video' : 'image') + '</div>';
if (isVideo) {
window.requestAnimationFrame(function () {
var video = previewBox.querySelector('video.slide-image-region-preview');
if (!video) {
return;
}
try {
video.load();
} catch (_error) {
// Ignore load failures; play() will retry if the browser allows it.
}
var playPromise = video.play && video.play();
if (playPromise && typeof playPromise.catch === 'function') {
playPromise.catch(function () {
return null;
});
}
});
}
}
function loadVideoDuration(mediaPath) {
var cacheKey = String(mediaPath || '').trim();
var cachedPromise;
if (!cacheKey) {
return Promise.reject(new Error('No video source is available for this slide.'));
}
cachedPromise = videoDurationCache[cacheKey];
if (cachedPromise) {
return cachedPromise;
}
cachedPromise = new Promise(function (resolve, reject) {
var video = document.createElement('video');
var timeoutId = window.setTimeout(function () {
cleanup();
reject(new Error('Timed out loading the video metadata.'));
}, 15000);
function cleanup() {
window.clearTimeout(timeoutId);
video.removeAttribute('src');
video.load();
if (video.parentNode) {
video.parentNode.removeChild(video);
}
}
video.preload = 'metadata';
video.muted = true;
video.playsInline = true;
video.setAttribute('playsinline', '');
video.setAttribute('muted', '');
video.style.position = 'absolute';
video.style.left = '-9999px';
video.style.top = '0';
video.style.width = '1px';
video.style.height = '1px';
video.style.opacity = '0';
video.style.pointerEvents = 'none';
video.addEventListener('loadedmetadata', function () {
var duration = Number(video.duration);
cleanup();
if (Number.isFinite(duration) && duration > 0) {
resolve(Math.max(0.001, Math.round(duration * 1000) / 1000));
} else {
reject(new Error('The video duration could not be read.'));
}
}, { once: true });
video.addEventListener('error', function () {
cleanup();
reject(new Error('The video duration could not be read.'));
}, { once: true });
video.addEventListener('abort', function () {
cleanup();
reject(new Error('The video duration could not be read.'));
}, { once: true });
document.body.appendChild(video);
video.src = cacheKey;
video.load();
}).catch(function (error) {
delete videoDurationCache[cacheKey];
throw error;
});
videoDurationCache[cacheKey] = cachedPromise;
return cachedPromise;
}
async function syncVideoRegionDuration(card, source) {
var module = getRegionTypeModule('video');
if (module && typeof module.syncDuration === 'function') {
return module.syncDuration(card, source, {
getHiddenInput: function (targetCard) {
var regionId = targetCard && targetCard.getAttribute('data-region-id');
return regionId ? getRegionVideoDurationHiddenInput(regionId) : null;
},
loadDuration: loadVideoDuration
});
}
if (!card) {
return;
}
var regionId = card.getAttribute('data-region-id');
var hidden = regionId ? getRegionVideoDurationHiddenInput(regionId) : null;
if (!hidden) {
return;
}
var mediaPath = String(source || '').trim();
if (!mediaPath) {
hidden.value = '';
return;
}
try {
hidden.value = String(await loadVideoDuration(mediaPath));
} catch (_error) {
hidden.value = '';
}
}
function renderImageRegionPreview(card, value) {
return renderRegionMediaPreview(card, value);
}
function getRegionImageInputs() {
return Array.prototype.slice.call(templateFields.querySelectorAll('input[type="file"][name^="region_image_"], input[type="file"][name^="region_video_"], input[type="file"][name^="region_qr_image_"]'));
}
function getRegionMediaInputs() {
return Array.prototype.slice.call(templateFields.querySelectorAll('input[type="file"][name^="region_image_"], input[type="file"][name^="region_video_"], input[type="file"][name^="region_qr_image_"]'));
}
function hasPendingRegionImageUpload() {
return getRegionMediaInputs().some(function (input) {
return Boolean(input && input.files && input.files.length);
});
}
function hasPendingRegionMediaUpload() {
return hasPendingRegionImageUpload();
}
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 buildPreviewPopupPayload() {
var template = getSelectedTemplate();
return {
html: slidePreviewCanvas.innerHTML,
canvasWidth: currentPreviewCanvasWidth || Math.max(1, Number(template && template.canvas_size_width ? template.canvas_size_width : 1920)),
canvasHeight: currentPreviewCanvasHeight || Math.max(1, Number(template && template.canvas_size_height ? template.canvas_size_height : 1080)),
backgroundColor: template && template.background_color ? String(template.background_color) : '#111111',
backgroundImagePath: template && template.background_image_path ? String(template.background_image_path) : '',
fontStylesheetHref: fontStylesheetHref
};
}
function buildPreviewPopupHash() {
return encodeURIComponent(JSON.stringify(buildPreviewPopupPayload()));
}
function buildPreviewPopupUrl() {
return '/slides/popup-preview#' + buildPreviewPopupHash();
}
function syncPopupPreview() {
if (!previewPopupWindow || previewPopupWindow.closed) {
previewPopupWindow = null;
return;
}
try {
if (previewPopupWindow.location && previewPopupWindow.location.pathname !== '/slides/popup-preview') {
return;
}
} catch (_error) {
return;
}
previewPopupWindow.location.hash = buildPreviewPopupHash();
}
function openPreviewPopup() {
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));
previewPopupFeatures = [
'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('about:blank', 'slide-preview-popup', previewPopupFeatures);
if (!previewPopupWindow) {
return;
}
try {
previewPopupWindow.location.replace(buildPreviewPopupUrl());
} catch (_error) {
previewPopupWindow.location.href = buildPreviewPopupUrl();
}
previewPopupWindow.focus();
}
function syncSlideEditorSidebar() {
sidebarSyncFrame = 0;
if (!slideEditorShell || !slideEditorSidebar) {
return;
}
if (window.matchMedia && window.matchMedia('(max-width: 1199.98px)').matches) {
slideEditorSidebar.style.transform = '';
return;
}
var stickyHeader = document.querySelector('.app-header');
var stickyHeaderHeight = stickyHeader ? Math.max(0, Math.round(stickyHeader.getBoundingClientRect().height || stickyHeader.offsetHeight || 0)) : 0;
var sidebarTopOffset = sidebarBaseOffset + stickyHeaderHeight + 12;
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';
applyBackdropStyle(slidePreviewStage, '#111111');
return;
}
var canvasSize = getPreviewCanvasDimensions(template);
var canvasWidth = canvasSize.width;
var canvasHeight = canvasSize.height;
currentPreviewCanvasWidth = canvasWidth;
currentPreviewCanvasHeight = canvasHeight;
slidePreviewStage.style.aspectRatio = canvasWidth + ' / ' + canvasHeight;
applyBackdropStyle(slidePreviewStage, template.background_color || '#111111', template.background_image_path);
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 typeModule = getRegionTypeModule(region.region_type);
var previewContext = buildPreviewRenderContext(region, card);
var content = typeModule && typeof typeModule.renderPreview === 'function'
? typeModule.renderPreview(region, previewContext, previewContext)
: renderPreviewTextRegion(region, previewContext.value, previewContext.style, scale);
if (typeModule && typeof typeModule.renderPreview === 'function') {
content = content || renderPreviewTextRegion(region, previewContext.value, previewContext.style, scale);
}
var selected = region.region_type === 'image'
? ' slide-preview-image-region'
: region.region_type === 'video'
? ' slide-preview-video-region'
: region.region_type === 'webpage'
? ' slide-preview-webpage-region'
: region.region_type === 'qr-code'
? ' slide-preview-qr-code-region'
: region.region_type === 'rtmp'
? ' slide-preview-rtmp-region'
: region.region_type === 'rss'
? ' slide-preview-rss-region'
: region.region_type === 'api'
? ' slide-preview-api-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('');
startPreviewVideoPlayback(slidePreviewOverlay);
syncPopupPreview();
}
function requestPreviewRender() {
if (previewRenderFrame) {
return;
}
previewRenderFrame = window.requestAnimationFrame(function () {
previewRenderFrame = 0;
renderPreview();
});
}
var reduceAspectRatio = slideRegionHelpers.reduceAspectRatio;
var getCurrentTextStyle = slideRegionHelpers.getCurrentTextStyle;
var getRssFieldList = slideRegionHelpers.getRssFieldList;
var updateRssPlaceholderChips = slideRegionHelpers.updateRssPlaceholderChips;
var getCurrentRssConfig = slideRegionHelpers.getCurrentRssConfig;
var getApiFieldList = slideRegionHelpers.getApiFieldList;
var updateApiPlaceholderChips = slideRegionHelpers.updateApiPlaceholderChips;
var updateApiSampleDataPanel = slideRegionHelpers.updateApiSampleDataPanel;
var updateApiItemNumberLimit = slideRegionHelpers.updateApiItemNumberLimit;
var getCurrentApiConfig = slideRegionHelpers.getCurrentApiConfig;
var updateRssItemNumberLimit = slideRegionHelpers.updateRssItemNumberLimit;
function renderTemplate() {
templateSelectorLock.reset();
clearImagePreviewUrls();
slideFormEditorController.destroyEditors();
var template = getTemplateById(templateSelect.value);
if (!template) {
templateFields.innerHTML = '';
requestPreviewRender();
return;
}
templateFields.innerHTML = template.regions.length ? template.regions.map(function (region) {
return getRegionEditorCardHtml(region) || '<div class="muted">Region editor unavailable.</div>';
}).join('') : '<div class="muted">This template has no regions.</div>';
if (window.adminlte && typeof window.adminlte.CardWidget === 'function') {
templateFields.querySelectorAll('.card-widget[data-region-id]').forEach(function (card) {
window.adminlte.CardWidget.getOrCreateInstance(card);
});
}
templateFields.querySelectorAll('[data-region-media-type="video"]').forEach(function (card) {
var regionId = card.getAttribute('data-region-id');
var hidden = regionId ? card.querySelector('input[type="hidden"][name="existing_region_video_' + regionId + '"]') : null;
if (hidden && hidden.value) {
void syncVideoRegionDuration(card, hidden.value);
}
});
slideFormEditorController.renderEditors();
window.requestAnimationFrame(function () {
bindRegionCards();
});
templateFields.querySelectorAll('input[name^="region_api_item_number_"]').forEach(function (input) {
var regionId = input.name.replace(/^region_api_item_number_/, '');
var sourceInput = templateFields.querySelector('select[name="region_api_source_id_' + regionId + '"]');
var pathInput = templateFields.querySelector('input[name="region_api_items_path_' + regionId + '"]');
if (sourceInput) {
updateApiItemNumberLimit(regionId, sourceInput.value, pathInput ? pathInput.value : '');
}
});
templateFields.querySelectorAll('input[name^="region_rss_item_number_"]').forEach(function (input) {
var regionId = input.name.replace(/^region_rss_item_number_/, '');
var feedInput = templateFields.querySelector('select[name="region_rss_feed_id_' + regionId + '"]');
if (feedInput) {
updateRssItemNumberLimit(regionId, feedInput.value);
}
});
if (typeof regionUtils.bindRegionMediaRemoveControls === 'function') {
regionUtils.bindRegionMediaRemoveControls(templateFields, {
getCardByRegionId: function (regionId) {
return templateFields.querySelector('[data-region-id="' + regionId + '"]');
},
getMediaType: function (card, button) {
if (button && button.hasAttribute && button.hasAttribute('data-remove-region-qr-image')) {
return 'qr-image';
}
if (button && button.hasAttribute && button.hasAttribute('data-remove-region-video')) {
return 'video';
}
return card && card.dataset && String(card.dataset.regionMediaType || '').trim() === 'video' ? 'video' : 'image';
},
getExistingMediaPrefix: function (mediaType) {
if (String(mediaType || '').trim() === 'qr-image') {
return 'existing_region_qr_image_';
}
return String(mediaType || '').trim() === 'video' ? 'existing_region_video_' : 'existing_region_image_';
},
getMediaPrefix: function (mediaType) {
if (String(mediaType || '').trim() === 'qr-image') {
return 'region_qr_image_';
}
return String(mediaType || '').trim() === 'video' ? 'region_video_' : 'region_image_';
},
getVideoDurationHiddenInput: function (regionId) {
return getRegionVideoDurationHiddenInput(regionId);
},
queueUploadCleanup: function (uploadPaths) {
if (regionMediaController) {
regionMediaController.queueUploadCleanup(uploadPaths);
}
},
renderPreview: function (card, value) {
renderRegionMediaPreview(card, value);
},
markEdited: function () {
templateSelectorLock.markEdited();
},
requestPreviewRender: function () {
requestPreviewRender();
}
});
}
templateFields.querySelectorAll('select[name^="region_rss_feed_id_"], input[name^="region_rss_item_number_"], select[name^="region_api_source_id_"], input[name^="region_api_item_number_"], input[name^="region_api_items_path_"]').forEach(function (input) {
input.addEventListener('input', function () {
var regionId = input.name.replace(/^region_(?:rss_feed_id|rss_item_number|api_source_id|api_item_number|api_items_path)_/, '');
if (input.name.indexOf('region_rss_') === 0) {
var rssFeed = input.name.indexOf('region_rss_feed_id_') === 0 ? input.value : (templateFields.querySelector('select[name="region_rss_feed_id_' + regionId + '"]') || {}).value;
updateRssPlaceholderChips(regionId, rssFeed);
updateRssItemNumberLimit(regionId, rssFeed);
}
if (input.name.indexOf('region_api_') === 0) {
var apiSource = input.name.indexOf('region_api_source_id_') === 0 ? input.value : (templateFields.querySelector('select[name="region_api_source_id_' + regionId + '"]') || {}).value;
var apiItemNumber = input.name.indexOf('region_api_item_number_') === 0 ? input.value : (templateFields.querySelector('input[name="region_api_item_number_' + regionId + '"]') || {}).value;
var apiItemsPath = input.name.indexOf('region_api_items_path_') === 0 ? input.value : (templateFields.querySelector('input[name="region_api_items_path_' + regionId + '"]') || {}).value;
updateApiItemNumberLimit(regionId, apiSource, apiItemsPath);
updateApiPlaceholderChips(regionId, apiSource, apiItemsPath);
updateApiSampleDataPanel(regionId, apiSource, apiItemNumber, apiItemsPath);
}
templateSelectorLock.markEdited();
requestPreviewRender();
});
input.addEventListener('change', function () {
var regionId = input.name.replace(/^region_(?:rss_feed_id|rss_item_number|api_source_id|api_item_number|api_items_path)_/, '');
if (input.name.indexOf('region_rss_') === 0) {
var rssFeed = input.name.indexOf('region_rss_feed_id_') === 0 ? input.value : (templateFields.querySelector('select[name="region_rss_feed_id_' + regionId + '"]') || {}).value;
updateRssPlaceholderChips(regionId, rssFeed);
updateRssItemNumberLimit(regionId, rssFeed);
}
if (input.name.indexOf('region_api_') === 0) {
var apiSource = input.name.indexOf('region_api_source_id_') === 0 ? input.value : (templateFields.querySelector('select[name="region_api_source_id_' + regionId + '"]') || {}).value;
var apiItemNumber = input.name.indexOf('region_api_item_number_') === 0 ? input.value : (templateFields.querySelector('input[name="region_api_item_number_' + regionId + '"]') || {}).value;
var apiItemsPath = input.name.indexOf('region_api_items_path_') === 0 ? input.value : (templateFields.querySelector('input[name="region_api_items_path_' + regionId + '"]') || {}).value;
updateApiItemNumberLimit(regionId, apiSource, apiItemsPath);
updateApiPlaceholderChips(regionId, apiSource, apiItemsPath);
updateApiSampleDataPanel(regionId, apiSource, apiItemNumber, apiItemsPath);
}
templateSelectorLock.markEdited();
requestPreviewRender();
});
});
templateFields.querySelectorAll('input[type="url"][name^="region_webpage_"]').forEach(function (input) {
input.addEventListener('input', function () {
templateSelectorLock.markEdited();
});
input.addEventListener('change', function () {
templateSelectorLock.markEdited();
});
});
if (existingTemplateId && hasExistingSlideContent()) {
templateSelectorLock.arm();
templateSelectorLock.markEdited();
requestPreviewRender();
return;
}
requestPreviewRender();
}
slideForm.addEventListener('submit', function (event) {
if (!window.webAsyncSaveForm || typeof window.webAsyncSaveForm.submit !== 'function') {
return;
}
window.webAsyncSaveForm.submit(slideForm, event, {
beforeSubmit: async function () {
if (hasPendingRegionImageUpload()) {
throw new Error('Wait for the image upload to finish before saving.');
}
await slideFormEditorController.syncEditors();
await new Promise(function (resolve) {
window.requestAnimationFrame(function () {
resolve();
});
});
await new Promise(function (resolve) {
window.setTimeout(resolve, 0);
});
await syncRegionCards();
},
fallbackSuccessMessage: 'Saved slide.',
afterSuccess: function () {
if (regionMediaController) {
regionMediaController.clearPendingUploadCleanupPaths();
}
}
});
});
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 () {
var webpageInput = event.target && typeof event.target.matches === 'function' && event.target.matches('input[type="url"][name^="region_webpage_"]');
if (webpageInput) {
templateSelectorLock.arm();
templateSelectorLock.markEdited();
return;
}
templateSelectorLock.arm();
templateSelectorLock.markEdited();
requestPreviewRender();
});
templateFields.addEventListener('input', function () {
var webpageInput = event.target && typeof event.target.matches === 'function' && event.target.matches('input[type="url"][name^="region_webpage_"]');
if (webpageInput) {
templateSelectorLock.arm();
templateSelectorLock.markEdited();
return;
}
templateSelectorLock.arm();
templateSelectorLock.markEdited();
requestPreviewRender();
});
templateFields.addEventListener('click', function (event) {
var button = event.target && typeof event.target.closest === 'function' ? event.target.closest('[data-api-items-path-reset]') : null;
var webpageUpdateButton = event.target && typeof event.target.closest === 'function' ? event.target.closest('[data-webpage-preview-update]') : null;
if (!button) {
if (!webpageUpdateButton) {
return;
}
}
if (webpageUpdateButton) {
templateSelectorLock.arm();
templateSelectorLock.markEdited();
requestPreviewRender();
return;
}
var card = button.closest('[data-region-id]');
if (!card) {
return;
}
var regionId = card.getAttribute('data-region-id');
var input = card.querySelector('input[name="region_api_items_path_' + regionId + '"]');
if (!input) {
return;
}
input.value = String(button.dataset && Object.prototype.hasOwnProperty.call(button.dataset, 'apiDefaultItemsPath') ? button.dataset.apiDefaultItemsPath : '');
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
});
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 (regionMediaController) {
regionMediaController.queueUploadCleanup(regionMediaController.getPendingUploadCleanupPaths());
}
if (slideFormEditorController && typeof slideFormEditorController.getCommittedImageUploadCleanupPaths === 'function' && regionMediaController) {
regionMediaController.queueUploadCleanup(slideFormEditorController.getCommittedImageUploadCleanupPaths());
}
if (slideFormEditorController && typeof slideFormEditorController.getImageUploadCleanupPaths === 'function' && regionMediaController) {
regionMediaController.queueUploadCleanup(slideFormEditorController.getImageUploadCleanupPaths());
}
if (previewPopupWindow && !previewPopupWindow.closed) {
previewPopupWindow.close();
}
});
})();