Publish Docker Image / build-and-push (./build/Dockerfile, git.lzstealth.com/lzstealth/pulse-signage-web, web) (push) Successful in 1m16s
Publish Docker Image / build-and-push (./build/Dockerfile.player, git.lzstealth.com/lzstealth/pulse-signage-player, player) (push) Successful in 32s
977 lines
39 KiB
JavaScript
977 lines
39 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;
|
|
|
|
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 uploadMaxBytes = 100 * 1024 * 1024;
|
|
var uploadMaxLabel = '100 MB';
|
|
var uploadVideoMaxBytes = 1024 * 1024 * 1024;
|
|
var uploadVideoMaxLabel = '1 GB';
|
|
var wysiwygImageUploadMaxBytes = 2 * 1024 * 1024;
|
|
var wysiwygImageUploadLimitLabel = '2 MB';
|
|
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, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
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',
|
|
getRegionTypeModule: getRegionTypeModule,
|
|
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,
|
|
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();
|
|
}
|
|
});
|
|
})();
|
|
|