1599 lines
63 KiB
JavaScript
1599 lines
63 KiB
JavaScript
import {
|
|
Alignment,
|
|
BlockQuote,
|
|
Bold,
|
|
ClassicEditor,
|
|
ClipboardPipeline,
|
|
Essentials,
|
|
FontBackgroundColor,
|
|
FontColor,
|
|
FontFamily,
|
|
FontSize,
|
|
Heading,
|
|
HorizontalLine,
|
|
Indent,
|
|
IndentBlock,
|
|
Italic,
|
|
InputTextView,
|
|
Plugin,
|
|
Paragraph,
|
|
Strikethrough,
|
|
Subscript,
|
|
Superscript,
|
|
Underline
|
|
} from '/assets/js/vendor/ckeditor5/ckeditor5.js';
|
|
import { createTemplateSelectorLockController } from '/assets/js/slides/slide-form-template-lock.js';
|
|
|
|
(function () {
|
|
var DEFAULT_FONT_SIZE = 32;
|
|
|
|
var dataElement = document.getElementById('slide-editor-data');
|
|
if (!dataElement) {
|
|
return;
|
|
}
|
|
|
|
var slideEditorData = {};
|
|
try {
|
|
slideEditorData = JSON.parse(dataElement.getAttribute('data-json') || dataElement.textContent || '{}') || {};
|
|
} catch (_error) {
|
|
slideEditorData = {};
|
|
}
|
|
|
|
var templates = Array.isArray(slideEditorData.templates) ? slideEditorData.templates : [];
|
|
var rssFeeds = Array.isArray(slideEditorData.rssFeeds) ? slideEditorData.rssFeeds : [];
|
|
var apiSources = Array.isArray(slideEditorData.apiSources) ? slideEditorData.apiSources : [];
|
|
var existingTemplateId = slideEditorData.existingTemplateId !== undefined ? slideEditorData.existingTemplateId : null;
|
|
var existingContent = slideEditorData.existingContent || {};
|
|
var templateSelect = document.getElementById('template-select');
|
|
var templateFields = document.getElementById('template-fields');
|
|
var slideForm = document.getElementById('slide-form');
|
|
var slideEditorShell = document.querySelector('.slide-editor-shell');
|
|
var slideEditorSidebar = document.querySelector('.slide-editor-sidebar');
|
|
var slidePreviewMeta = document.getElementById('slide-preview-meta');
|
|
var slidePreviewStage = document.getElementById('slide-preview-stage');
|
|
var slidePreviewCanvas = document.getElementById('slide-preview-canvas');
|
|
var slidePreviewBackground = document.getElementById('slide-preview-background');
|
|
var slidePreviewEmpty = document.getElementById('slide-preview-empty');
|
|
var slidePreviewOverlay = document.getElementById('slide-preview-overlay');
|
|
var openPreviewPopupButton = document.getElementById('open-preview-popup');
|
|
var editorInstances = new Map();
|
|
var submitting = false;
|
|
var previewRenderFrame = 0;
|
|
var previewPopupWindow = null;
|
|
var previewPopupRenderFrame = 0;
|
|
var currentPreviewCanvasWidth = 0;
|
|
var currentPreviewCanvasHeight = 0;
|
|
var sidebarSyncFrame = 0;
|
|
var sidebarTopOffset = 16;
|
|
var templateSelectorLock = createTemplateSelectorLockController(templateSelect);
|
|
|
|
class FontSizeInputUI extends Plugin {
|
|
static get pluginName() {
|
|
return 'FontSizeInputUI';
|
|
}
|
|
|
|
init() {
|
|
var editor = this.editor;
|
|
var fontSizeCommand = editor.commands.get('fontSize');
|
|
var plugin = this;
|
|
|
|
if (!fontSizeCommand) {
|
|
return;
|
|
}
|
|
|
|
editor.ui.componentFactory.add('fontSizeInput', function (locale) {
|
|
var view = new InputTextView(locale);
|
|
|
|
view.extendTemplate({
|
|
attributes: {
|
|
class: ['ck-font-size-input']
|
|
}
|
|
});
|
|
|
|
view.set({
|
|
placeholder: 'Font size',
|
|
ariaLabel: 'Font size'
|
|
});
|
|
|
|
function getCurrentFontSizeValue() {
|
|
var value = fontSizeCommand.value;
|
|
if (value === null || value === undefined || value === '') {
|
|
return String(DEFAULT_FONT_SIZE);
|
|
}
|
|
|
|
var parsed = Math.round(Number(String(value).replace(/[^0-9.]/g, '')));
|
|
return Number.isFinite(parsed) && parsed > 0 ? String(parsed) : '';
|
|
}
|
|
|
|
function syncValue() {
|
|
if (view.element && document.activeElement === view.element) {
|
|
return;
|
|
}
|
|
|
|
view.value = getCurrentFontSizeValue();
|
|
}
|
|
|
|
view.on('render', function () {
|
|
if (!view.element) {
|
|
return;
|
|
}
|
|
|
|
view.element.type = 'text';
|
|
view.element.inputMode = 'numeric';
|
|
view.element.autocomplete = 'off';
|
|
view.element.spellcheck = false;
|
|
view.element.style.width = '3.5em';
|
|
syncValue();
|
|
});
|
|
|
|
function applyValue() {
|
|
if (!view.element) {
|
|
return;
|
|
}
|
|
|
|
var size = Math.max(1, Math.round(Number(view.element.value || 0)));
|
|
if (!size) {
|
|
return;
|
|
}
|
|
|
|
editor.execute('fontSize', { value: size + 'px' });
|
|
}
|
|
|
|
view.on('input', applyValue);
|
|
view.on('blur', applyValue);
|
|
|
|
view.on('keydown', function (_eventInfo, domEvent) {
|
|
if (domEvent.key === 'Enter') {
|
|
domEvent.preventDefault();
|
|
if (view.element) {
|
|
view.element.blur();
|
|
}
|
|
}
|
|
});
|
|
|
|
plugin.listenTo(editor.model.document.selection, 'change:range', syncValue);
|
|
plugin.listenTo(fontSizeCommand, 'change:value', syncValue);
|
|
|
|
syncValue();
|
|
|
|
return view;
|
|
});
|
|
}
|
|
}
|
|
|
|
class ForcePlainTextPaste extends Plugin {
|
|
static get pluginName() {
|
|
return 'ForcePlainTextPaste';
|
|
}
|
|
|
|
static get requires() {
|
|
return [ClipboardPipeline];
|
|
}
|
|
|
|
init() {
|
|
var editor = this.editor;
|
|
var clipboardPipeline = editor.plugins.get(ClipboardPipeline);
|
|
|
|
this.listenTo(clipboardPipeline, 'inputTransformation', function (_event, data) {
|
|
var plainText = data.dataTransfer.getData('text/plain');
|
|
data.content = editor.data.processor.toView(plainTextToHtml(plainText));
|
|
}, { priority: 'high' });
|
|
}
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function plainTextToHtml(value) {
|
|
var lines = String(value ?? '').replace(/\r\n?/g, '\n').split('\n');
|
|
var paragraphs = [];
|
|
var currentParagraph = [];
|
|
|
|
lines.forEach(function (line) {
|
|
if (line === '') {
|
|
if (currentParagraph.length) {
|
|
paragraphs.push('<p>' + currentParagraph.join('<br />') + '</p>');
|
|
currentParagraph = [];
|
|
} else {
|
|
paragraphs.push('<p></p>');
|
|
}
|
|
return;
|
|
}
|
|
|
|
currentParagraph.push(escapeHtml(line));
|
|
});
|
|
|
|
if (currentParagraph.length) {
|
|
paragraphs.push('<p>' + currentParagraph.join('<br />') + '</p>');
|
|
}
|
|
|
|
return paragraphs.length ? paragraphs.join('') : '<p></p>';
|
|
}
|
|
|
|
function getTemplateById(id) {
|
|
return templates.find(function (item) { return Number(item.id) === Number(id); }) || null;
|
|
}
|
|
|
|
function getCurrentRegionValue(region) {
|
|
var current = existingContent[region.region_key];
|
|
return current && current.value !== undefined ? current.value : '';
|
|
}
|
|
|
|
function normalizeEditorData(value) {
|
|
return String(value || '');
|
|
}
|
|
|
|
function requestPreviewRender() {
|
|
if (previewRenderFrame) {
|
|
return;
|
|
}
|
|
|
|
previewRenderFrame = window.requestAnimationFrame(function () {
|
|
previewRenderFrame = 0;
|
|
renderPreview();
|
|
});
|
|
}
|
|
|
|
function getTemplateFieldControls() {
|
|
return templateFields ? templateFields.querySelectorAll('input, textarea, select') : [];
|
|
}
|
|
|
|
function sanitizePreviewHtml(html) {
|
|
var output = String(html || '');
|
|
output = output.replace(/<script[\s\S]*?<\/script>/gi, '');
|
|
output = output.replace(/<style[\s\S]*?<\/style>/gi, '');
|
|
return output.replace(/<[^>]+>/g, function (tag) {
|
|
var match = tag.match(/^<\s*(\/?)\s*([a-z0-9]+)([\s\S]*?)(\/?)>$/i);
|
|
if (!match) {
|
|
return '';
|
|
}
|
|
var closing = Boolean(match[1]);
|
|
var name = String(match[2] || '').toLowerCase();
|
|
var attrText = String(match[3] || '');
|
|
var selfClosing = Boolean(match[4]) || name === 'br' || name === 'hr';
|
|
var allowed = ['a', 'b', 'blockquote', 'br', 'code', 'div', 'em', 'figure', 'figcaption', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'li', 'ol', 'p', 'pre', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'th', 'thead', 'tr', 'u', 'ul'];
|
|
if (allowed.indexOf(name) === -1) {
|
|
return '';
|
|
}
|
|
if (closing) {
|
|
return '</' + name + '>';
|
|
}
|
|
if (selfClosing) {
|
|
return '<' + name + sanitizeTagAttributes(name, attrText) + '>';
|
|
}
|
|
return '<' + name + sanitizeTagAttributes(name, attrText) + '>';
|
|
});
|
|
}
|
|
|
|
function sanitizeTagAttributes(tagName, attrText) {
|
|
var allowedAttributes = {
|
|
a: ['href', 'title', 'target', 'rel', 'class', 'style'],
|
|
blockquote: ['class', 'style'],
|
|
div: ['class', 'style'],
|
|
figure: ['class', 'style'],
|
|
figcaption: ['class', 'style'],
|
|
h1: ['class', 'style'],
|
|
h2: ['class', 'style'],
|
|
h3: ['class', 'style'],
|
|
h4: ['class', 'style'],
|
|
h5: ['class', 'style'],
|
|
h6: ['class', 'style'],
|
|
li: ['class', 'style'],
|
|
ol: ['class', 'style', 'start'],
|
|
p: ['class', 'style'],
|
|
pre: ['class', 'style'],
|
|
span: ['class', 'style'],
|
|
table: ['class', 'style'],
|
|
td: ['class', 'style', 'colspan', 'rowspan'],
|
|
th: ['class', 'style', 'colspan', 'rowspan', 'scope'],
|
|
tr: ['class', 'style'],
|
|
ul: ['class', 'style']
|
|
};
|
|
var allowed = allowedAttributes[tagName] || [];
|
|
if (!allowed.length) {
|
|
return '';
|
|
}
|
|
|
|
var attrs = [];
|
|
attrText.replace(/([a-zA-Z0-9:-]+)(?:\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>/=`]+)))?/g, function (_full, key, _valuePart, doubleQuoted, singleQuoted, bareValue) {
|
|
var lowerKey = String(key || '').toLowerCase();
|
|
if (allowed.indexOf(lowerKey) === -1) {
|
|
return '';
|
|
}
|
|
var value = doubleQuoted !== undefined ? doubleQuoted : singleQuoted !== undefined ? singleQuoted : bareValue !== undefined ? bareValue : '';
|
|
if (lowerKey === 'href' && /^(?:\s*javascript:|\s*data:)/i.test(String(value || ''))) {
|
|
return '';
|
|
}
|
|
if (lowerKey === 'style' && /(?:expression\s*\(|javascript:|url\s*\()/i.test(String(value || ''))) {
|
|
return '';
|
|
}
|
|
if (lowerKey === 'target') {
|
|
var targetValue = String(value || '').trim();
|
|
if (targetValue === '_blank') {
|
|
attrs.push(' target="_blank"');
|
|
if (attrs.indexOf(' rel="noreferrer noopener"') === -1) {
|
|
attrs.push(' rel="noreferrer noopener"');
|
|
}
|
|
return '';
|
|
}
|
|
}
|
|
attrs.push(' ' + lowerKey + '="' + escapeHtml(value) + '"');
|
|
return '';
|
|
});
|
|
|
|
return attrs.join('');
|
|
}
|
|
|
|
function renderPreviewListItem(item, tag) {
|
|
if (item && typeof item === 'object') {
|
|
var content = item.content !== undefined ? item.content : (item.text !== undefined ? item.text : item.value !== undefined ? item.value : '');
|
|
var children = Array.isArray(item.items) ? item.items : Array.isArray(item.subItems) ? item.subItems : Array.isArray(item.children) ? item.children : [];
|
|
var nested = children.length ? '<' + tag + ' style="list-style-type:' + (tag === 'ol' ? 'decimal' : 'disc') + ';padding-left:1.4em;">' + children.map(function (child) { return renderPreviewListItem(child, tag); }).join('') + '</' + tag + '>' : '';
|
|
return '<li>' + sanitizePreviewHtml(content || '') + nested + '</li>';
|
|
}
|
|
return '<li>' + sanitizePreviewHtml(item || '') + '</li>';
|
|
}
|
|
|
|
function renderPreviewTable(data) {
|
|
var rows = Array.isArray(data.content) ? data.content : Array.isArray(data.rows) ? data.rows : [];
|
|
if (!rows.length) {
|
|
return '';
|
|
}
|
|
var hasHeadings = Boolean(data.withHeadings);
|
|
var body = rows.map(function (row, rowIndex) {
|
|
var cells = Array.isArray(row) ? row : [];
|
|
var cellTag = hasHeadings && rowIndex === 0 ? 'th' : 'td';
|
|
var cellAttrs = cellTag === 'th' ? ' scope="col"' : '';
|
|
return '<tr>' + cells.map(function (cell) {
|
|
return '<' + cellTag + cellAttrs + '>' + sanitizePreviewHtml(cell || '') + '</' + cellTag + '>';
|
|
}).join('') + '</tr>';
|
|
}).join('');
|
|
return '<table class="slide-preview-table">' + body + '</table>';
|
|
}
|
|
|
|
function renderPreviewBlock(block) {
|
|
if (!block || !block.type || !block.data) {
|
|
return '';
|
|
}
|
|
if (block.type === 'header') {
|
|
var level = Math.max(1, Math.min(6, Number(block.data.level || 2)));
|
|
return '<h' + level + '>' + sanitizePreviewHtml(block.data.text || '') + '</h' + level + '>';
|
|
}
|
|
if (block.type === 'list') {
|
|
var tag = block.data.style === 'ordered' ? 'ol' : 'ul';
|
|
var items = Array.isArray(block.data.items) ? block.data.items : [];
|
|
return '<' + tag + ' style="list-style-type:' + (tag === 'ol' ? 'decimal' : 'disc') + ';padding-left:1.4em;">' + items.map(function (item) { return renderPreviewListItem(item, tag); }).join('') + '</' + tag + '>';
|
|
}
|
|
if (block.type === 'delimiter') {
|
|
return '<hr />';
|
|
}
|
|
if (block.type === 'code') {
|
|
return '<pre><code>' + sanitizePreviewHtml(block.data.code || '') + '</code></pre>';
|
|
}
|
|
if (block.type === 'table') {
|
|
return renderPreviewTable(block.data);
|
|
}
|
|
if (block.type === 'paragraph') {
|
|
return '<p>' + sanitizePreviewHtml(block.data.text || '') + '</p>';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function renderPreviewText(value) {
|
|
var raw = String(value || '');
|
|
if (!raw) {
|
|
return '<div class="slide-preview-placeholder">Empty text</div>';
|
|
}
|
|
return sanitizePreviewHtml(raw);
|
|
}
|
|
|
|
function renderPreviewTextRegion(region, value, style, scale) {
|
|
var fontFamily = style.font_family ? 'font-family:' + escapeHtml(style.font_family) + ';' : '';
|
|
var color = style.font_color ? 'color:' + escapeHtml(style.font_color) + ';' : '';
|
|
var fontSize = style.font_size ? 'font-size:' + Math.max(1, Math.round(Number(style.font_size))) + 'px;' : '';
|
|
var width = Math.max(1, Math.round(Number(region && region.width ? region.width : 0) || 1));
|
|
var height = Math.max(1, Math.round(Number(region && region.height ? region.height : 0) || 1));
|
|
var wrapperStyle = 'width:' + width + 'px;height:' + height + 'px;overflow:hidden;' + fontFamily + fontSize + color;
|
|
return '<div class="slide-preview-text-content" style="' + wrapperStyle + '">' + renderPreviewText(value) + '</div>';
|
|
}
|
|
|
|
function renderPreviewWebpageRegion(value) {
|
|
var src = String(value || '').trim();
|
|
if (!src) {
|
|
return '<div class="slide-preview-placeholder">Webpage</div>';
|
|
}
|
|
return '<iframe class="slide-preview-webpage-frame" src="' + escapeHtml(src) + '" title="Webpage preview" loading="eager" referrerpolicy="no-referrer" scrolling="no"></iframe>';
|
|
}
|
|
|
|
function renderPreviewRtmpRegion(value, disableAudio) {
|
|
var src = String(value || '').trim();
|
|
var label = src ? 'RTMP' : 'RTMP stream';
|
|
if (disableAudio) {
|
|
label += ' (muted)';
|
|
}
|
|
return '<div class="slide-preview-placeholder">' + label + '</div>';
|
|
}
|
|
|
|
function renderPreviewHtmlRegion(value) {
|
|
var html = String(value || '').trim();
|
|
if (!html) {
|
|
return '<div class="slide-preview-placeholder">HTML</div>';
|
|
}
|
|
return '<iframe class="slide-preview-html-frame" sandbox="" scrolling="no" srcdoc="<!doctype html><html><head><style>html,body{margin:0;width:100%;height:100%;overflow:hidden;background:transparent;}</style></head><body>' + escapeHtml(html) + '</body></html>" title="HTML preview" loading="eager"></iframe>';
|
|
}
|
|
|
|
function destroyEditors() {
|
|
editorInstances.forEach(function (editor) {
|
|
if (editor && typeof editor.destroy === 'function') {
|
|
editor.destroy();
|
|
}
|
|
});
|
|
editorInstances.clear();
|
|
}
|
|
|
|
function getEditorHiddenInput(regionId) {
|
|
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
|
|
return card ? card.querySelector('input[type="hidden"][name="region_text_' + regionId + '"]') : null;
|
|
}
|
|
|
|
function getEditorFontSizeHiddenInput(regionId) {
|
|
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
|
|
return card ? card.querySelector('input[name="region_font_size_' + regionId + '"]') : null;
|
|
}
|
|
|
|
function normalizeFontSizeValue(value) {
|
|
var raw = String(value || '').trim().toLowerCase();
|
|
if (/^\d+(?:\.\d+)?px$/.test(raw)) {
|
|
return String(Math.max(1, Math.round(Number(raw.replace(/px$/, '')))));
|
|
}
|
|
if (/^\d+(?:\.\d+)?$/.test(raw)) {
|
|
return String(Math.max(1, Math.round(Number(raw))));
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function syncEditorFontSizeHidden(regionId, editor) {
|
|
var fontSizeHidden = getEditorFontSizeHiddenInput(regionId);
|
|
if (!fontSizeHidden || !editor) {
|
|
return;
|
|
}
|
|
|
|
var fontSizeCommand = editor.commands.get('fontSize');
|
|
fontSizeHidden.value = normalizeFontSizeValue(fontSizeCommand && fontSizeCommand.value) || String(DEFAULT_FONT_SIZE);
|
|
}
|
|
|
|
function getCurrentTextStyle(region) {
|
|
var current = existingContent[region.region_key] || {};
|
|
return {
|
|
font_family: String(current.font_family || region.font_family || 'Arial').trim() || 'Arial',
|
|
font_size: Math.max(8, Number(current.font_size || DEFAULT_FONT_SIZE)),
|
|
font_color: String(current.font_color || region.font_color || '#000000').trim() || '#000000'
|
|
};
|
|
}
|
|
|
|
function getRssFeedById(feedId) {
|
|
var normalizedId = Number(feedId || 0);
|
|
return rssFeeds.find(function (feed) {
|
|
return Number(feed.id) === normalizedId;
|
|
}) || null;
|
|
}
|
|
|
|
function getRssFieldList(feedId) {
|
|
var feed = getRssFeedById(feedId);
|
|
var sampleItem = feed && Array.isArray(feed.items) ? feed.items[0] : null;
|
|
var sampleJson = sampleItem && sampleItem.itemJson && typeof sampleItem.itemJson === 'object' ? sampleItem.itemJson : null;
|
|
|
|
function walkFields(value, prefix, output) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
return;
|
|
}
|
|
|
|
Object.keys(value).forEach(function (key) {
|
|
if (key === 'rawXml') {
|
|
return;
|
|
}
|
|
|
|
var nextPath = prefix ? prefix + '.' + key : key;
|
|
var nextValue = value[key];
|
|
if (nextValue && typeof nextValue === 'object' && !Array.isArray(nextValue)) {
|
|
walkFields(nextValue, nextPath, output);
|
|
return;
|
|
}
|
|
|
|
if (output.indexOf(nextPath) === -1) {
|
|
output.push(nextPath);
|
|
}
|
|
});
|
|
}
|
|
|
|
var fields = [];
|
|
walkFields(sampleJson, '', fields);
|
|
return fields;
|
|
}
|
|
|
|
function updatePlaceholderChipList(card, fieldList, emptyLabel) {
|
|
if (!card) {
|
|
return;
|
|
}
|
|
|
|
var container = card.querySelector('[data-placeholder-chips]');
|
|
if (!container) {
|
|
return;
|
|
}
|
|
|
|
var chips = Array.isArray(fieldList) ? fieldList.map(function (field) {
|
|
return '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
|
|
}).join('') : '';
|
|
|
|
container.innerHTML = chips || '<span class="muted slide-image-file">' + escapeHtml(emptyLabel) + '</span>';
|
|
}
|
|
|
|
function updateRssPlaceholderChips(regionId, feedId) {
|
|
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
|
|
updatePlaceholderChipList(card, getRssFieldList(feedId), 'No RSS fields available.');
|
|
}
|
|
|
|
function getCurrentRssConfig(region) {
|
|
var current = existingContent[region.region_key] || {};
|
|
var parsedItemNumber = Math.max(1, Number(current.item_number || 1));
|
|
return {
|
|
feed_id: current.feed_id === undefined || current.feed_id === null || current.feed_id === '' ? '' : Number(current.feed_id),
|
|
item_number: Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber : 1,
|
|
value: current.value !== undefined ? current.value : ''
|
|
};
|
|
}
|
|
|
|
function resolveRssPath(value, path) {
|
|
var current = value;
|
|
if (!path) {
|
|
return current;
|
|
}
|
|
String(path).split('.').forEach(function (segment) {
|
|
if (current === undefined || current === null) {
|
|
current = '';
|
|
return;
|
|
}
|
|
current = current[segment];
|
|
});
|
|
return current === undefined || current === null ? '' : current;
|
|
}
|
|
|
|
function normalizeRssVariableName(value) {
|
|
var next = String(value || '').trim();
|
|
return next || 'item';
|
|
}
|
|
|
|
function substituteRssVariables(html, item) {
|
|
var source = String(html || '');
|
|
return source.replace(/\{\{\s*([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_.]+))?\s*\}\}/g, function (_match, tokenName, tokenPath) {
|
|
if (!item || typeof item !== 'object') {
|
|
return '';
|
|
}
|
|
var key = tokenName === 'item' && tokenPath ? tokenPath : tokenName;
|
|
return escapeHtml(resolveRssPath(item, key || ''));
|
|
});
|
|
}
|
|
|
|
function getRssItem(region, rssConfig) {
|
|
var feed = getRssFeedById(rssConfig.feed_id);
|
|
var items = feed && Array.isArray(feed.items) ? feed.items : [];
|
|
var parsedItemNumber = Math.max(1, Number(rssConfig.item_number || 1));
|
|
var index = Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber - 1 : 0;
|
|
return items[index] || null;
|
|
}
|
|
|
|
function getRssPreviewFallback(item) {
|
|
if (!item || typeof item !== 'object') {
|
|
return '<div class="slide-preview-placeholder">RSS item</div>';
|
|
}
|
|
|
|
var title = String(item.title || '').trim();
|
|
var description = String(item.description || '').trim();
|
|
var summary = [];
|
|
if (title) {
|
|
summary.push('<h3>' + escapeHtml(title) + '</h3>');
|
|
}
|
|
if (description) {
|
|
summary.push('<p>' + sanitizePreviewHtml(description) + '</p>');
|
|
}
|
|
if (!summary.length) {
|
|
return '<div class="slide-preview-placeholder">RSS item</div>';
|
|
}
|
|
return summary.join('');
|
|
}
|
|
|
|
function getApiSourceById(sourceId) {
|
|
var normalizedId = Number(sourceId || 0);
|
|
return apiSources.find(function (source) {
|
|
return Number(source.id) === normalizedId;
|
|
}) || null;
|
|
}
|
|
|
|
function getApiSourceItems(sourceId) {
|
|
var source = getApiSourceById(sourceId);
|
|
var responseJson = source && source.responseJson && typeof source.responseJson === 'object' ? source.responseJson : null;
|
|
if (Array.isArray(responseJson)) {
|
|
return responseJson;
|
|
}
|
|
if (responseJson && Array.isArray(responseJson.items)) {
|
|
return responseJson.items;
|
|
}
|
|
if (responseJson && Array.isArray(responseJson.results)) {
|
|
return responseJson.results;
|
|
}
|
|
if (responseJson && Array.isArray(responseJson.data)) {
|
|
return responseJson.data;
|
|
}
|
|
return responseJson ? [responseJson] : [];
|
|
}
|
|
|
|
function getApiFieldList(sourceId) {
|
|
var sampleItem = getApiSourceItems(sourceId)[0] || null;
|
|
|
|
function walkFields(value, prefix, output) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
return;
|
|
}
|
|
|
|
Object.keys(value).forEach(function (key) {
|
|
var nextPath = prefix ? prefix + '.' + key : key;
|
|
var nextValue = value[key];
|
|
if (nextValue && typeof nextValue === 'object' && !Array.isArray(nextValue)) {
|
|
walkFields(nextValue, nextPath, output);
|
|
return;
|
|
}
|
|
|
|
if (output.indexOf(nextPath) === -1) {
|
|
output.push(nextPath);
|
|
}
|
|
});
|
|
}
|
|
|
|
var fields = [];
|
|
walkFields(sampleItem && typeof sampleItem === 'object' ? sampleItem : null, '', fields);
|
|
return fields;
|
|
}
|
|
|
|
function updateApiPlaceholderChips(regionId, sourceId) {
|
|
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
|
|
updatePlaceholderChipList(card, getApiFieldList(sourceId), 'No JSON fields available.');
|
|
}
|
|
|
|
function getCurrentApiConfig(region) {
|
|
var current = existingContent[region.region_key] || {};
|
|
var parsedItemNumber = Math.max(1, Number(current.item_number || 1));
|
|
return {
|
|
source_id: current.source_id === undefined || current.source_id === null || current.source_id === '' ? '' : Number(current.source_id),
|
|
item_number: Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber : 1,
|
|
value: current.value !== undefined ? current.value : ''
|
|
};
|
|
}
|
|
|
|
function resolveApiPath(value, path) {
|
|
var current = value;
|
|
if (!path) {
|
|
return current;
|
|
}
|
|
String(path).split('.').forEach(function (segment) {
|
|
if (current === undefined || current === null) {
|
|
current = '';
|
|
return;
|
|
}
|
|
current = current[segment];
|
|
});
|
|
return current === undefined || current === null ? '' : current;
|
|
}
|
|
|
|
function substituteApiVariables(html, item) {
|
|
var source = String(html || '');
|
|
return source.replace(/\{\{\s*([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_.]+))?\s*\}\}/g, function (_match, tokenName, tokenPath) {
|
|
if (!item || typeof item !== 'object') {
|
|
return '';
|
|
}
|
|
var key = tokenName === 'item' && tokenPath ? tokenPath : tokenName;
|
|
return escapeHtml(resolveApiPath(item, key || ''));
|
|
});
|
|
}
|
|
|
|
function getApiItem(sourceId, itemNumber) {
|
|
var items = getApiSourceItems(sourceId);
|
|
var parsedItemNumber = Math.max(1, Number(itemNumber || 1));
|
|
var index = Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber - 1 : 0;
|
|
return items[index] || null;
|
|
}
|
|
|
|
function getApiPreviewFallback(item) {
|
|
if (!item || typeof item !== 'object') {
|
|
return '<div class="slide-preview-placeholder">API item</div>';
|
|
}
|
|
|
|
var title = String(item.title || item.name || '').trim();
|
|
var description = String(item.description || item.summary || item.text || '').trim();
|
|
var summary = [];
|
|
if (title) {
|
|
summary.push('<h3>' + escapeHtml(title) + '</h3>');
|
|
}
|
|
if (description) {
|
|
summary.push('<p>' + sanitizePreviewHtml(description) + '</p>');
|
|
}
|
|
if (!summary.length) {
|
|
return '<div class="slide-preview-placeholder">API item</div>';
|
|
}
|
|
return summary.join('');
|
|
}
|
|
|
|
function getTextStyleFromCard(_card, region) {
|
|
return getCurrentTextStyle(region);
|
|
}
|
|
|
|
function getSelectedTemplate() {
|
|
return templates.find(function (item) { return Number(item.id) === Number(templateSelect.value); }) || null;
|
|
}
|
|
|
|
function clearImagePreviewUrls() {
|
|
templateFields.querySelectorAll('input[type="file"][name^="region_image_"]').forEach(function (input) {
|
|
if (input.dataset.previewUrl) {
|
|
URL.revokeObjectURL(input.dataset.previewUrl);
|
|
delete input.dataset.previewUrl;
|
|
}
|
|
});
|
|
}
|
|
|
|
function getRegionBox(region, canvasWidth, canvasHeight) {
|
|
return {
|
|
left: Math.max(0, Math.round(Number(region.x || 0))),
|
|
top: Math.max(0, Math.round(Number(region.y || 0))),
|
|
width: Math.max(1, Math.round(Number(region.width || 0) || 1)),
|
|
height: Math.max(1, Math.round(Number(region.height || 0) || 1))
|
|
};
|
|
}
|
|
|
|
function getPreviewCanvasDimensions(template) {
|
|
return {
|
|
width: Math.max(1, Number(template && template.canvas_size_width ? template.canvas_size_width : 1920)),
|
|
height: Math.max(1, Number(template && template.canvas_size_height ? template.canvas_size_height : 1080))
|
|
};
|
|
}
|
|
|
|
function buildPreviewPopupHtml() {
|
|
return '<!doctype html>' +
|
|
'<html>' +
|
|
'<head>' +
|
|
'<meta charset="utf-8" />' +
|
|
'<meta name="viewport" content="width=device-width, initial-scale=1" />' +
|
|
'<title>Slide preview</title>' +
|
|
'<style>' +
|
|
'html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#111;color:#fff;font-family:Arial,sans-serif;}' +
|
|
'#popup-preview-stage{position:relative;width:100%;height:100%;overflow:hidden;background:#111;}' +
|
|
'#popup-preview-canvas{position:absolute;top:0;left:0;transform-origin:top left;will-change:transform;}' +
|
|
'#popup-preview-canvas,#popup-preview-stage{box-sizing:border-box;}' +
|
|
'.slide-preview-background,.slide-preview-overlay,.slide-preview-empty{position:absolute;inset:0;}' +
|
|
'.slide-preview-background{width:100%;height:100%;object-fit:contain;display:block;z-index:1;}' +
|
|
'.slide-preview-overlay{z-index:2;}' +
|
|
'.slide-preview-empty{display:flex;align-items:center;justify-content:center;padding:1rem;text-align:center;color:rgba(255,255,255,0.75);z-index:0;}' +
|
|
'.slide-preview-region{position:absolute;overflow:hidden;box-sizing:border-box;}' +
|
|
'.slide-preview-image{width:100%;height:100%;object-fit:contain;display:block;}' +
|
|
'.slide-preview-placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,0.65);background:rgba(255,255,255,0.06);font-size:0.9rem;}' +
|
|
'</style>' +
|
|
'</head>' +
|
|
'<body>' +
|
|
'<div id="popup-preview-stage">' +
|
|
'<div id="popup-preview-canvas"></div>' +
|
|
'</div>' +
|
|
'</body>' +
|
|
'</html>';
|
|
}
|
|
|
|
function syncPopupPreview() {
|
|
if (!previewPopupWindow || previewPopupWindow.closed) {
|
|
previewPopupWindow = null;
|
|
return;
|
|
}
|
|
|
|
var popupDocument = previewPopupWindow.document;
|
|
if (!popupDocument) {
|
|
return;
|
|
}
|
|
|
|
var popupStage = popupDocument.getElementById('popup-preview-stage');
|
|
var popupCanvas = popupDocument.getElementById('popup-preview-canvas');
|
|
var template = getSelectedTemplate();
|
|
|
|
if (!popupStage || !popupCanvas) {
|
|
if (previewPopupRenderFrame) {
|
|
cancelAnimationFrame(previewPopupRenderFrame);
|
|
}
|
|
previewPopupRenderFrame = requestAnimationFrame(syncPopupPreview);
|
|
return;
|
|
}
|
|
|
|
if (!template) {
|
|
popupStage.style.backgroundColor = '#111111';
|
|
popupCanvas.style.width = '';
|
|
popupCanvas.style.height = '';
|
|
popupCanvas.style.transform = '';
|
|
popupCanvas.innerHTML = slidePreviewCanvas.innerHTML;
|
|
var popupEmpty = popupCanvas.querySelector('#slide-preview-empty');
|
|
var popupOverlay = popupCanvas.querySelector('#slide-preview-overlay');
|
|
var popupBackground = popupCanvas.querySelector('#slide-preview-background');
|
|
if (popupBackground) {
|
|
popupBackground.style.display = 'none';
|
|
}
|
|
if (popupEmpty) {
|
|
popupEmpty.style.display = 'flex';
|
|
popupEmpty.textContent = 'Select a template to preview the slide.';
|
|
}
|
|
if (popupOverlay) {
|
|
popupOverlay.innerHTML = '';
|
|
}
|
|
return;
|
|
}
|
|
|
|
var canvasWidth = currentPreviewCanvasWidth || Math.max(1, Number(template.canvas_size_width || 1920));
|
|
var canvasHeight = currentPreviewCanvasHeight || Math.max(1, Number(template.canvas_size_height || 1080));
|
|
popupStage.style.backgroundColor = template.background_color || '#111111';
|
|
popupCanvas.style.width = canvasWidth + 'px';
|
|
popupCanvas.style.height = canvasHeight + 'px';
|
|
popupCanvas.innerHTML = slidePreviewCanvas.innerHTML;
|
|
var popupBackground = popupCanvas.querySelector('#slide-preview-background');
|
|
var popupEmpty = popupCanvas.querySelector('#slide-preview-empty');
|
|
var popupOverlay = popupCanvas.querySelector('#slide-preview-overlay');
|
|
|
|
if (popupBackground) {
|
|
popupBackground.style.display = template.background_image_path ? 'block' : 'none';
|
|
popupBackground.src = template.background_image_path || '';
|
|
}
|
|
if (popupEmpty) {
|
|
popupEmpty.style.display = 'none';
|
|
}
|
|
|
|
var stageRect = popupStage.getBoundingClientRect();
|
|
if (!stageRect.width || !stageRect.height) {
|
|
if (previewPopupRenderFrame) {
|
|
cancelAnimationFrame(previewPopupRenderFrame);
|
|
}
|
|
previewPopupRenderFrame = requestAnimationFrame(syncPopupPreview);
|
|
return;
|
|
}
|
|
|
|
var scale = Math.min(stageRect.width / canvasWidth, stageRect.height / canvasHeight) || 1;
|
|
popupCanvas.style.transform = 'translate3d(0, 0, 0) scale(' + scale + ')';
|
|
}
|
|
|
|
function openPreviewPopup() {
|
|
if (previewPopupWindow && !previewPopupWindow.closed) {
|
|
previewPopupWindow.focus();
|
|
syncPopupPreview();
|
|
return;
|
|
}
|
|
|
|
var popupWidth = 1280;
|
|
var popupHeight = 900;
|
|
var left = Math.max(0, Math.round((window.screen.width - popupWidth) / 2));
|
|
var top = Math.max(0, Math.round((window.screen.height - popupHeight) / 2));
|
|
var popupFeatures = [
|
|
'popup=yes',
|
|
'toolbar=no',
|
|
'location=no',
|
|
'menubar=no',
|
|
'directories=no',
|
|
'status=no',
|
|
'scrollbars=yes',
|
|
'resizable=yes',
|
|
'width=' + popupWidth,
|
|
'height=' + popupHeight,
|
|
'left=' + left,
|
|
'top=' + top
|
|
].join(',');
|
|
|
|
previewPopupWindow = window.open('', 'slide-preview-popup', popupFeatures);
|
|
if (!previewPopupWindow) {
|
|
return;
|
|
}
|
|
|
|
previewPopupWindow.document.open();
|
|
previewPopupWindow.document.write(buildPreviewPopupHtml());
|
|
previewPopupWindow.document.close();
|
|
previewPopupWindow.addEventListener('resize', syncPopupPreview);
|
|
previewPopupWindow.focus();
|
|
syncPopupPreview();
|
|
}
|
|
|
|
function syncSlideEditorSidebar() {
|
|
sidebarSyncFrame = 0;
|
|
|
|
if (!slideEditorShell || !slideEditorSidebar) {
|
|
return;
|
|
}
|
|
|
|
if (window.matchMedia && window.matchMedia('(max-width: 1199.98px)').matches) {
|
|
slideEditorSidebar.style.transform = '';
|
|
return;
|
|
}
|
|
|
|
var shellRect = slideEditorShell.getBoundingClientRect();
|
|
var sidebarHeight = slideEditorSidebar.offsetHeight;
|
|
var shellTop = window.scrollY + shellRect.top;
|
|
var shellBottom = shellTop + slideEditorShell.offsetHeight;
|
|
var maxTranslate = Math.max(0, shellBottom - shellTop - sidebarHeight - sidebarTopOffset);
|
|
var desiredTranslate = Math.max(0, Math.min(window.scrollY + sidebarTopOffset - shellTop, maxTranslate));
|
|
|
|
slideEditorSidebar.style.transform = desiredTranslate ? 'translate3d(0, ' + desiredTranslate + 'px, 0)' : '';
|
|
}
|
|
|
|
function requestSidebarSync() {
|
|
if (sidebarSyncFrame) {
|
|
return;
|
|
}
|
|
|
|
sidebarSyncFrame = window.requestAnimationFrame(syncSlideEditorSidebar);
|
|
}
|
|
|
|
function renderPreview() {
|
|
var template = getSelectedTemplate();
|
|
if (!template) {
|
|
slidePreviewMeta.textContent = '--';
|
|
slidePreviewBackground.style.display = 'none';
|
|
slidePreviewEmpty.style.display = 'flex';
|
|
slidePreviewEmpty.textContent = 'Select a template to preview the slide.';
|
|
slidePreviewCanvas.style.width = '';
|
|
slidePreviewCanvas.style.height = '';
|
|
slidePreviewOverlay.innerHTML = '';
|
|
slidePreviewOverlay.style.width = '';
|
|
slidePreviewOverlay.style.height = '';
|
|
slidePreviewOverlay.style.transform = '';
|
|
slidePreviewStage.style.aspectRatio = '16 / 9';
|
|
slidePreviewStage.style.backgroundColor = '#111111';
|
|
return;
|
|
}
|
|
|
|
var canvasSize = getPreviewCanvasDimensions(template);
|
|
var canvasWidth = canvasSize.width;
|
|
var canvasHeight = canvasSize.height;
|
|
currentPreviewCanvasWidth = canvasWidth;
|
|
currentPreviewCanvasHeight = canvasHeight;
|
|
slidePreviewStage.style.aspectRatio = canvasWidth + ' / ' + canvasHeight;
|
|
slidePreviewStage.style.backgroundColor = template.background_color || '#111111';
|
|
slidePreviewMeta.textContent = canvasWidth + 'x' + canvasHeight;
|
|
if (!(template.regions || []).length) {
|
|
slidePreviewEmpty.style.display = 'flex';
|
|
slidePreviewEmpty.textContent = 'This template has no regions.';
|
|
slidePreviewBackground.style.display = template.background_image_path ? 'block' : 'none';
|
|
slidePreviewBackground.src = template.background_image_path || '';
|
|
slidePreviewCanvas.style.width = canvasWidth + 'px';
|
|
slidePreviewCanvas.style.height = canvasHeight + 'px';
|
|
slidePreviewCanvas.style.transform = '';
|
|
slidePreviewOverlay.innerHTML = '';
|
|
slidePreviewOverlay.style.width = '';
|
|
slidePreviewOverlay.style.height = '';
|
|
slidePreviewOverlay.style.transform = '';
|
|
return;
|
|
}
|
|
|
|
slidePreviewEmpty.style.display = 'none';
|
|
slidePreviewBackground.style.display = template.background_image_path ? 'block' : 'none';
|
|
slidePreviewBackground.src = template.background_image_path || '';
|
|
|
|
var stageRect = slidePreviewStage.getBoundingClientRect();
|
|
if (!stageRect.width || !stageRect.height) {
|
|
if (previewRenderFrame) {
|
|
cancelAnimationFrame(previewRenderFrame);
|
|
}
|
|
previewRenderFrame = requestAnimationFrame(function () {
|
|
previewRenderFrame = 0;
|
|
renderPreview();
|
|
});
|
|
return;
|
|
}
|
|
var scale = Math.min(stageRect.width / canvasWidth, stageRect.height / canvasHeight) || 1;
|
|
slidePreviewCanvas.style.width = canvasWidth + 'px';
|
|
slidePreviewCanvas.style.height = canvasHeight + 'px';
|
|
slidePreviewCanvas.style.transform = 'translate3d(0, 0, 0) scale(' + scale + ')';
|
|
slidePreviewCanvas.style.transformOrigin = 'top left';
|
|
var regions = template.regions || [];
|
|
slidePreviewOverlay.innerHTML = regions.map(function (region) {
|
|
var box = getRegionBox(region, canvasWidth, canvasHeight);
|
|
var card = templateFields.querySelector('[data-region-id="' + region.id + '"]');
|
|
var hiddenInput = card && card.querySelector('input[type="hidden"][name="region_text_' + region.id + '"]');
|
|
var htmlInput = card && card.querySelector('textarea[name="region_html_' + region.id + '"]');
|
|
var imageHidden = card && card.querySelector('input[type="hidden"][name="existing_region_image_' + region.id + '"]');
|
|
var imageFileInput = card && card.querySelector('input[type="file"][name="region_image_' + region.id + '"]');
|
|
var webpageInput = card && card.querySelector('input[type="url"][name="region_webpage_' + region.id + '"]');
|
|
var rtmpInput = card && card.querySelector('input[type="url"][name="region_rtmp_' + region.id + '"]');
|
|
var disableAudioInput = card && card.querySelector('input[type="checkbox"][name="region_disable_audio_' + region.id + '"]');
|
|
var rssFeedInput = card && card.querySelector('select[name="region_rss_feed_id_' + region.id + '"]');
|
|
var rssItemInput = card && card.querySelector('input[name="region_rss_item_number_' + region.id + '"]');
|
|
var apiSourceInput = card && card.querySelector('select[name="region_api_source_id_' + region.id + '"]');
|
|
var apiItemInput = card && card.querySelector('input[name="region_api_item_number_' + region.id + '"]');
|
|
var existingRtmp = existingContent[region.region_key] || {};
|
|
var existingRss = existingContent[region.region_key] || {};
|
|
var existingApi = existingContent[region.region_key] || {};
|
|
var value = region.region_type === 'image'
|
|
? ((imageFileInput && imageFileInput.dataset.previewUrl) ? imageFileInput.dataset.previewUrl : (imageHidden ? imageHidden.value : ''))
|
|
: region.region_type === 'webpage'
|
|
? (webpageInput ? webpageInput.value : '')
|
|
: region.region_type === 'rtmp'
|
|
? (rtmpInput ? rtmpInput.value : '')
|
|
: region.region_type === 'rss'
|
|
? (hiddenInput ? hiddenInput.value : '')
|
|
: region.region_type === 'api'
|
|
? (hiddenInput ? hiddenInput.value : '')
|
|
: region.region_type === 'html'
|
|
? (htmlInput ? htmlInput.value : '')
|
|
: (hiddenInput ? hiddenInput.value : '');
|
|
var style = region.region_type === 'image'
|
|
? null
|
|
: region.region_type === 'webpage'
|
|
? null
|
|
: region.region_type === 'rtmp'
|
|
? null
|
|
: region.region_type === 'html'
|
|
? null
|
|
: getTextStyleFromCard(card, region);
|
|
var content = region.region_type === 'image'
|
|
? (value ? '<img class="slide-preview-image" src="' + escapeHtml(value) + '" alt="" />' : '<div class="slide-preview-placeholder">Image</div>')
|
|
: region.region_type === 'webpage'
|
|
? renderPreviewWebpageRegion(value)
|
|
: region.region_type === 'rtmp'
|
|
? renderPreviewRtmpRegion(value, disableAudioInput ? disableAudioInput.checked : (existingRtmp.disable_audio === undefined ? true : Boolean(existingRtmp.disable_audio)))
|
|
: region.region_type === 'rss'
|
|
? (function () {
|
|
var item = getRssItem(region, {
|
|
feed_id: rssFeedInput ? rssFeedInput.value : existingRss.feed_id,
|
|
item_number: rssItemInput ? rssItemInput.value : existingRss.item_number
|
|
});
|
|
var rendered = substituteRssVariables(value, item);
|
|
return renderPreviewTextRegion(region, rendered ? rendered : getRssPreviewFallback(item), style, scale);
|
|
}())
|
|
: region.region_type === 'api'
|
|
? (function () {
|
|
var item = getApiItem(apiSourceInput ? apiSourceInput.value : existingApi.source_id, apiItemInput ? apiItemInput.value : existingApi.item_number);
|
|
var rendered = substituteApiVariables(value, item);
|
|
return renderPreviewTextRegion(region, rendered ? rendered : getApiPreviewFallback(item), style, scale);
|
|
}())
|
|
: region.region_type === 'html'
|
|
? renderPreviewHtmlRegion(value)
|
|
: renderPreviewTextRegion(region, value, style, scale);
|
|
var selected = region.region_type === 'image'
|
|
? ' slide-preview-image-region'
|
|
: region.region_type === 'webpage'
|
|
? ' slide-preview-webpage-region'
|
|
: region.region_type === '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('');
|
|
|
|
syncPopupPreview();
|
|
}
|
|
|
|
function renderTextRegion(region) {
|
|
var current = getCurrentRegionValue(region);
|
|
return '' +
|
|
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '">' +
|
|
'<div class="card-header template-field-head">' +
|
|
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
|
'<div class="template-field-actions">' +
|
|
'<button type="button" class="btn btn-sm btn-outline-secondary template-editor-toggle" data-toggle-editor-height="' + region.id + '">Expand</button>' +
|
|
'<span class="chip">Text</span>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="card-body p-3 d-grid gap-3">' +
|
|
'<div class="ckeditor-holder" data-region-id="' + region.id + '">' +
|
|
'<textarea class="ckeditor-source" rows="10">' + escapeHtml(current) + '</textarea>' +
|
|
'</div>' +
|
|
'<input type="hidden" name="region_font_size_' + region.id + '" value="' + escapeHtml(getCurrentTextStyle(region).font_size) + '" />' +
|
|
'<input type="hidden" name="region_text_' + region.id + '" value="' + escapeHtml(current) + '" />' +
|
|
'</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
function renderRssRegion(region) {
|
|
var current = existingContent[region.region_key] || {};
|
|
var config = getCurrentRssConfig(region);
|
|
var fieldList = getRssFieldList(config.feed_id);
|
|
var placeholderChips = fieldList.map(function (field) {
|
|
return '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
|
|
}).join('');
|
|
var feedOptions = rssFeeds.map(function (feed) {
|
|
var selected = Number(feed.id) === Number(config.feed_id) ? ' selected' : '';
|
|
return '<option value="' + escapeHtml(feed.id) + '"' + selected + '>' + escapeHtml(feed.name || ('Feed ' + feed.id)) + '</option>';
|
|
}).join('');
|
|
return '' +
|
|
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '">' +
|
|
'<div class="card-header template-field-head">' +
|
|
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
|
'<div class="template-field-actions">' +
|
|
'<button type="button" class="btn btn-sm btn-outline-secondary template-editor-toggle" data-toggle-editor-height="' + region.id + '">Expand</button>' +
|
|
'<span class="chip">RSS</span>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="card-body p-3 d-grid gap-3">' +
|
|
'<div class="ckeditor-holder" data-region-id="' + region.id + '">' +
|
|
'<textarea class="ckeditor-source" rows="10">' + escapeHtml(current.value !== undefined ? current.value : '') + '</textarea>' +
|
|
'</div>' +
|
|
'<div class="row g-3 align-items-end">' +
|
|
'<div class="col-12 col-md-8">' +
|
|
'<label class="form-label" for="region_rss_feed_id_' + region.id + '">RSS feed</label>' +
|
|
'<select name="region_rss_feed_id_' + region.id + '" class="form-select">' +
|
|
'<option value="">Select a feed</option>' +
|
|
feedOptions +
|
|
'</select>' +
|
|
'</div>' +
|
|
'<div class="col-6 col-md-4">' +
|
|
'<label class="form-label" for="region_rss_item_number_' + region.id + '">Entry number</label>' +
|
|
'<input type="number" min="1" step="1" name="region_rss_item_number_' + region.id + '" class="form-control" value="' + escapeHtml(config.item_number) + '" />' +
|
|
'</div>' +
|
|
'</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.value !== undefined ? current.value : '') + '" />' +
|
|
'<div class="muted slide-image-file">Use placeholders like {{title}} or {{link}}. Available placeholders:</div>' +
|
|
'<div class="d-flex flex-wrap gap-2" data-placeholder-chips>' + (placeholderChips || '<span class="muted slide-image-file">No RSS fields available.</span>') + '</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
function renderApiRegion(region) {
|
|
var current = existingContent[region.region_key] || {};
|
|
var config = getCurrentApiConfig(region);
|
|
var fieldList = getApiFieldList(config.source_id);
|
|
var placeholderChips = fieldList.map(function (field) {
|
|
return '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
|
|
}).join('');
|
|
var sourceOptions = apiSources.map(function (source) {
|
|
var selected = Number(source.id) === Number(config.source_id) ? ' selected' : '';
|
|
return '<option value="' + escapeHtml(source.id) + '"' + selected + '>' + escapeHtml(source.name || ('Source ' + source.id)) + '</option>';
|
|
}).join('');
|
|
return '' +
|
|
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '">' +
|
|
'<div class="card-header template-field-head">' +
|
|
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
|
'<div class="template-field-actions">' +
|
|
'<button type="button" class="btn btn-sm btn-outline-secondary template-editor-toggle" data-toggle-editor-height="' + region.id + '">Expand</button>' +
|
|
'<span class="chip">API</span>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="card-body p-3 d-grid gap-3">' +
|
|
'<div class="ckeditor-holder" data-region-id="' + region.id + '">' +
|
|
'<textarea class="ckeditor-source" rows="10">' + escapeHtml(current.value !== undefined ? current.value : '') + '</textarea>' +
|
|
'</div>' +
|
|
'<div class="row g-3 align-items-end">' +
|
|
'<div class="col-12 col-md-8">' +
|
|
'<label class="form-label" for="region_api_source_id_' + region.id + '">API source</label>' +
|
|
'<select name="region_api_source_id_' + region.id + '" class="form-select">' +
|
|
'<option value="">Select a source</option>' +
|
|
sourceOptions +
|
|
'</select>' +
|
|
'</div>' +
|
|
'<div class="col-6 col-md-4">' +
|
|
'<label class="form-label" for="region_api_item_number_' + region.id + '">Item number</label>' +
|
|
'<input type="number" min="1" step="1" name="region_api_item_number_' + region.id + '" class="form-control" value="' + escapeHtml(config.item_number) + '" />' +
|
|
'</div>' +
|
|
'</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.value !== undefined ? current.value : '') + '" />' +
|
|
'<div class="muted slide-image-file">Use placeholders like {{title}} or {{description}}. Available placeholders:</div>' +
|
|
'<div class="d-flex flex-wrap gap-2" data-placeholder-chips>' + (placeholderChips || '<span class="muted slide-image-file">No JSON fields available.</span>') + '</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
function renderWebpageRegion(region) {
|
|
var current = getCurrentRegionValue(region);
|
|
return '' +
|
|
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '">' +
|
|
'<div class="card-header template-field-head">' +
|
|
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
|
'<span class="chip">Webpage</span>' +
|
|
'</div>' +
|
|
'<div class="card-body p-3 d-grid gap-3">' +
|
|
'<label style="display:block;">Webpage URL' +
|
|
'<input type="url" name="region_webpage_' + region.id + '" class="form-control" value="' + escapeHtml(current) + '" placeholder="https://example.com" />' +
|
|
'</label>' +
|
|
'<div class="muted slide-image-file">The webpage will be loaded in an iframe.</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
function renderRtmpRegion(region) {
|
|
var current = existingContent[region.region_key] || {};
|
|
var url = String(current.value || '').trim();
|
|
var disableAudio = current.disable_audio === undefined ? true : Boolean(current.disable_audio);
|
|
return '' +
|
|
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '">' +
|
|
'<div class="card-header template-field-head">' +
|
|
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
|
'<span class="chip">RTMP</span>' +
|
|
'</div>' +
|
|
'<div class="card-body p-3 d-grid gap-3">' +
|
|
'<label style="display:block;">RTMP URL' +
|
|
'<input type="url" name="region_rtmp_' + region.id + '" class="form-control" value="' + escapeHtml(url) + '" placeholder="rtmp://example.com/live/stream" />' +
|
|
'</label>' +
|
|
'<label class="form-check m-0">' +
|
|
'<input class="form-check-input" type="checkbox" name="region_disable_audio_' + region.id + '" value="1"' + (disableAudio ? ' checked' : '') + ' />' +
|
|
'<span class="form-check-label">Disable audio</span>' +
|
|
'</label>' +
|
|
'<div class="muted slide-image-file">Player controls stay hidden and the stream is not interactive.</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
function renderHtmlRegion(region) {
|
|
var current = getCurrentRegionValue(region);
|
|
return '' +
|
|
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '">' +
|
|
'<div class="card-header template-field-head">' +
|
|
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
|
'<span class="chip">HTML</span>' +
|
|
'</div>' +
|
|
'<div class="card-body p-3 d-grid gap-3">' +
|
|
'<label style="display:block;">HTML content' +
|
|
'<textarea name="region_html_' + region.id + '" class="form-control" rows="10" placeholder="<div>Hello</div>">' + escapeHtml(current) + '</textarea>' +
|
|
'</label>' +
|
|
'<div class="muted slide-image-file">HTML is rendered in a sandboxed iframe preview.</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
function renderImageRegion(region) {
|
|
var current = getCurrentRegionValue(region);
|
|
return '' +
|
|
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '">' +
|
|
'<div class="card-header template-field-head">' +
|
|
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
|
'<span class="chip">Image</span>' +
|
|
'</div>' +
|
|
'<div class="card-body p-3 d-grid gap-3">' +
|
|
'<label style="display:block;">Image file' +
|
|
'<input type="file" name="region_image_' + region.id + '" class="form-control" accept="image/*" />' +
|
|
'</label>' +
|
|
'<input type="hidden" name="existing_region_image_' + region.id + '" value="' + escapeHtml(current) + '" />' +
|
|
(current
|
|
? '<div class="muted slide-image-file">Current: <a href="' + escapeHtml(current) + '" target="_blank" rel="noreferrer">' + escapeHtml(current) + '</a></div>'
|
|
: '<div class="muted slide-image-file">No image selected.</div>') +
|
|
'</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
function renderTemplate() {
|
|
templateSelectorLock.reset();
|
|
clearImagePreviewUrls();
|
|
destroyEditors();
|
|
var template = getTemplateById(templateSelect.value);
|
|
if (!template) {
|
|
templateFields.innerHTML = '';
|
|
requestPreviewRender();
|
|
return;
|
|
}
|
|
|
|
templateFields.innerHTML = template.regions.length ? template.regions.map(function (region) {
|
|
if (region.region_type === 'image') {
|
|
return renderImageRegion(region);
|
|
}
|
|
if (region.region_type === 'webpage') {
|
|
return renderWebpageRegion(region);
|
|
}
|
|
if (region.region_type === 'rtmp') {
|
|
return renderRtmpRegion(region);
|
|
}
|
|
if (region.region_type === 'rss') {
|
|
return renderRssRegion(region);
|
|
}
|
|
if (region.region_type === 'api') {
|
|
return renderApiRegion(region);
|
|
}
|
|
if (region.region_type === 'html') {
|
|
return renderHtmlRegion(region);
|
|
}
|
|
return renderTextRegion(region);
|
|
}).join('') : '<div class="muted">This template has no regions.</div>';
|
|
|
|
templateFields.querySelectorAll('.ckeditor-holder').forEach(function (holder) {
|
|
var regionId = holder.getAttribute('data-region-id');
|
|
var source = holder.querySelector('.ckeditor-source');
|
|
var hidden = getEditorHiddenInput(regionId);
|
|
var editorConfig = {
|
|
licenseKey: 'GPL',
|
|
placeholder: 'Write content...',
|
|
toolbar: {
|
|
items: [
|
|
'undo',
|
|
'redo',
|
|
'|',
|
|
'fontSizeInput',
|
|
'fontFamily',
|
|
'fontColor',
|
|
'fontBackgroundColor',
|
|
'|',
|
|
'bold',
|
|
'italic',
|
|
'underline',
|
|
'strikethrough',
|
|
'subscript',
|
|
'superscript',
|
|
'|',
|
|
'horizontalLine',
|
|
'blockQuote',
|
|
'|',
|
|
'alignment',
|
|
'|',
|
|
'outdent',
|
|
'indent'
|
|
],
|
|
shouldNotGroupWhenFull: false
|
|
},
|
|
plugins: [
|
|
Alignment,
|
|
BlockQuote,
|
|
Bold,
|
|
Essentials,
|
|
ForcePlainTextPaste,
|
|
FontBackgroundColor,
|
|
FontColor,
|
|
FontFamily,
|
|
FontSize,
|
|
FontSizeInputUI,
|
|
HorizontalLine,
|
|
Indent,
|
|
IndentBlock,
|
|
Italic,
|
|
Paragraph,
|
|
Strikethrough,
|
|
Subscript,
|
|
Superscript,
|
|
Underline
|
|
],
|
|
fontFamily: {
|
|
supportAllValues: true
|
|
},
|
|
fontSize: {
|
|
options: [20, 24, 28, 32, 36, 40, 44],
|
|
supportAllValues: true
|
|
},
|
|
initialData: normalizeEditorData((source && source.value) || (hidden ? hidden.value : ''))
|
|
};
|
|
|
|
ClassicEditor.create(source || holder, editorConfig).then(function (editor) {
|
|
editorInstances.set(regionId, editor);
|
|
var fontSizeCommand = editor.commands.get('fontSize');
|
|
if (hidden) {
|
|
hidden.value = editor.getData();
|
|
}
|
|
syncEditorFontSizeHidden(regionId, editor);
|
|
if (fontSizeCommand) {
|
|
fontSizeCommand.on('change:value', function () {
|
|
syncEditorFontSizeHidden(regionId, editor);
|
|
});
|
|
}
|
|
editor.model.document.on('change:data', function () {
|
|
var instance = editorInstances.get(regionId);
|
|
if (instance !== editor) {
|
|
return;
|
|
}
|
|
if (hidden) {
|
|
hidden.value = editor.getData();
|
|
}
|
|
templateSelectorLock.markEdited();
|
|
requestPreviewRender();
|
|
});
|
|
requestPreviewRender();
|
|
}).catch(function (error) {
|
|
console.error('Failed to initialize CKEditor.', error);
|
|
});
|
|
});
|
|
|
|
templateFields.querySelectorAll('[data-toggle-editor-height]').forEach(function (button) {
|
|
button.addEventListener('click', function () {
|
|
var regionId = button.getAttribute('data-toggle-editor-height');
|
|
var card = templateFields.querySelector('[data-region-id="' + regionId + '"]');
|
|
if (!card) {
|
|
return;
|
|
}
|
|
|
|
var expanded = card.classList.toggle('is-expanded');
|
|
button.textContent = expanded ? 'Collapse' : 'Expand';
|
|
});
|
|
});
|
|
|
|
templateFields.querySelectorAll('input[type="file"][name^="region_image_"]').forEach(function (input) {
|
|
input.addEventListener('change', function () {
|
|
if (input.dataset.previewUrl) {
|
|
URL.revokeObjectURL(input.dataset.previewUrl);
|
|
delete input.dataset.previewUrl;
|
|
}
|
|
if (input.files && input.files.length) {
|
|
input.dataset.previewUrl = URL.createObjectURL(input.files[0]);
|
|
}
|
|
templateSelectorLock.markEdited();
|
|
requestPreviewRender();
|
|
});
|
|
});
|
|
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_"]').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)_/, '');
|
|
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);
|
|
}
|
|
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;
|
|
updateApiPlaceholderChips(regionId, apiSource);
|
|
}
|
|
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)_/, '');
|
|
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);
|
|
}
|
|
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;
|
|
updateApiPlaceholderChips(regionId, apiSource);
|
|
}
|
|
templateSelectorLock.markEdited();
|
|
requestPreviewRender();
|
|
});
|
|
});
|
|
|
|
if (existingTemplateId) {
|
|
templateSelectorLock.arm();
|
|
templateSelectorLock.markEdited();
|
|
requestPreviewRender();
|
|
return;
|
|
}
|
|
|
|
window.requestAnimationFrame(function () {
|
|
templateSelectorLock.arm();
|
|
});
|
|
requestPreviewRender();
|
|
}
|
|
|
|
slideForm.addEventListener('submit', async function (event) {
|
|
if (submitting) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
var slideFormUrl = new URL(slideForm.action, window.location.href);
|
|
var isCreateForm = slideFormUrl.pathname === '/slides';
|
|
var submitterValue = event.submitter && event.submitter.name === 'save_action'
|
|
? String(event.submitter.value || '').trim().toLowerCase()
|
|
: '';
|
|
var saves = Array.prototype.map.call(templateFields.querySelectorAll('.ckeditor-holder'), function (holder) {
|
|
var regionId = holder.getAttribute('data-region-id');
|
|
var editor = editorInstances.get(regionId);
|
|
var hidden = getEditorHiddenInput(regionId);
|
|
if (!editor || !hidden) {
|
|
return Promise.resolve();
|
|
}
|
|
syncEditorFontSizeHidden(regionId, editor);
|
|
hidden.value = editor.getData();
|
|
return Promise.resolve();
|
|
});
|
|
submitting = true;
|
|
try {
|
|
await Promise.all(saves);
|
|
var formData = new FormData(slideForm);
|
|
if (event.submitter && event.submitter.name) {
|
|
formData.set(event.submitter.name, event.submitter.value || '');
|
|
}
|
|
var response = await fetch(slideForm.action, {
|
|
method: (slideForm.method || 'POST').toUpperCase(),
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'text/html, application/json, text/plain, */*'
|
|
},
|
|
body: formData,
|
|
credentials: 'same-origin'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(await response.text() || 'Unable to save slide.');
|
|
}
|
|
|
|
if (submitterValue === 'close' || submitterValue === 'new') {
|
|
var redirectUrl = submitterValue === 'close'
|
|
? String(slideForm.dataset.asyncSaveCloseUrl || response.url || slideForm.action)
|
|
: String(slideForm.dataset.asyncSaveNewUrl || response.url || slideForm.action);
|
|
window.location.replace(redirectUrl);
|
|
return;
|
|
}
|
|
|
|
if (isCreateForm) {
|
|
try {
|
|
if (response.url) {
|
|
window.location.replace(response.url);
|
|
return;
|
|
}
|
|
} catch (_error) {
|
|
// Fall through and show a toast if the redirect URL cannot be resolved.
|
|
}
|
|
slideForm.dataset.dirty = 'false';
|
|
showToast('Saved slide.', 'success');
|
|
return;
|
|
}
|
|
|
|
var responseText = await response.text();
|
|
var savedMessage = '';
|
|
try {
|
|
var doc = new DOMParser().parseFromString(responseText || '', 'text/html');
|
|
var toastBody = doc.querySelector('.toast-body');
|
|
if (toastBody && toastBody.textContent) {
|
|
savedMessage = toastBody.textContent.trim();
|
|
}
|
|
} catch (_error) {
|
|
savedMessage = '';
|
|
}
|
|
|
|
showToast(savedMessage || 'Saved slide.', 'success');
|
|
slideForm.dataset.dirty = 'false';
|
|
} catch (error) {
|
|
if (typeof showToast === 'function') {
|
|
var variant = Number(error && error.status) >= 400 && Number(error && error.status) < 500 ? 'warning' : 'danger';
|
|
showToast(error && error.message ? error.message : 'Unable to save slide.', variant);
|
|
} else {
|
|
window.alert(error && error.message ? error.message : 'Unable to save slide.');
|
|
}
|
|
} finally {
|
|
submitting = false;
|
|
}
|
|
});
|
|
|
|
slideForm.addEventListener('input', function () {
|
|
slideForm.dataset.dirty = 'true';
|
|
}, true);
|
|
|
|
slideForm.addEventListener('change', function () {
|
|
slideForm.dataset.dirty = 'true';
|
|
}, true);
|
|
|
|
if (openPreviewPopupButton) {
|
|
openPreviewPopupButton.addEventListener('click', function () {
|
|
openPreviewPopup();
|
|
});
|
|
}
|
|
|
|
templateSelect.addEventListener('change', renderTemplate);
|
|
templateFields.addEventListener('change', function () {
|
|
templateSelectorLock.markEdited();
|
|
requestPreviewRender();
|
|
});
|
|
templateFields.addEventListener('input', function () {
|
|
templateSelectorLock.markEdited();
|
|
requestPreviewRender();
|
|
});
|
|
if (existingTemplateId) {
|
|
templateSelect.value = String(existingTemplateId);
|
|
}
|
|
renderTemplate();
|
|
if (window.ResizeObserver && slidePreviewStage) {
|
|
var previewResizeObserver = new ResizeObserver(function () {
|
|
requestPreviewRender();
|
|
});
|
|
previewResizeObserver.observe(slidePreviewStage);
|
|
}
|
|
requestSidebarSync();
|
|
window.addEventListener('scroll', requestSidebarSync, { passive: true });
|
|
window.addEventListener('resize', requestPreviewRender);
|
|
window.addEventListener('resize', requestSidebarSync);
|
|
window.addEventListener('beforeunload', function () {
|
|
if (previewPopupWindow && !previewPopupWindow.closed) {
|
|
previewPopupWindow.close();
|
|
}
|
|
});
|
|
})();
|
|
|