556 lines
22 KiB
JavaScript
556 lines
22 KiB
JavaScript
(function () {
|
|
var dataElement = document.getElementById('slide-editor-data');
|
|
if (!dataElement) {
|
|
return;
|
|
}
|
|
|
|
var slideEditorData = {};
|
|
try {
|
|
slideEditorData = JSON.parse(dataElement.getAttribute('data-json') || dataElement.textContent || '{}') || {};
|
|
} catch (_error) {
|
|
slideEditorData = {};
|
|
}
|
|
|
|
var templates = Array.isArray(slideEditorData.templates) ? slideEditorData.templates : [];
|
|
var existingTemplateId = slideEditorData.existingTemplateId !== undefined ? slideEditorData.existingTemplateId : null;
|
|
var existingContent = slideEditorData.existingContent || {};
|
|
var templateSelect = document.getElementById('template-select');
|
|
var templateFields = document.getElementById('template-fields');
|
|
var slideForm = document.getElementById('slide-form');
|
|
var slidePreviewMeta = document.getElementById('slide-preview-meta');
|
|
var slidePreviewStage = document.getElementById('slide-preview-stage');
|
|
var slidePreviewBackground = document.getElementById('slide-preview-background');
|
|
var slidePreviewEmpty = document.getElementById('slide-preview-empty');
|
|
var slidePreviewOverlay = document.getElementById('slide-preview-overlay');
|
|
var editorInstances = new Map();
|
|
var submitting = false;
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
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) {
|
|
var raw = String(value || '');
|
|
if (!raw) {
|
|
return { time: Date.now(), blocks: [] };
|
|
}
|
|
try {
|
|
var parsed = JSON.parse(raw);
|
|
if (parsed && Array.isArray(parsed.blocks)) {
|
|
return parsed;
|
|
}
|
|
} catch (_error) {
|
|
// fall through to legacy HTML/plain text
|
|
}
|
|
return {
|
|
time: Date.now(),
|
|
blocks: [{
|
|
type: 'paragraph',
|
|
data: { text: raw }
|
|
}]
|
|
};
|
|
}
|
|
|
|
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[^>]*)?>$/i);
|
|
if (!match) {
|
|
return '';
|
|
}
|
|
var closing = Boolean(match[1]);
|
|
var name = String(match[2] || '').toLowerCase();
|
|
var allowed = ['b', 'strong', 'i', 'em', 'u', 'br', 'p', 'div', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre', 'code', 'table', 'thead', 'tbody', 'tr', 'th', 'td'];
|
|
if (allowed.indexOf(name) === -1) {
|
|
return '';
|
|
}
|
|
if (name === 'br') {
|
|
return '<br>';
|
|
}
|
|
return closing ? '</' + name + '>' : '<' + name + '>';
|
|
});
|
|
}
|
|
|
|
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>';
|
|
}
|
|
try {
|
|
var parsed = JSON.parse(raw);
|
|
if (parsed && Array.isArray(parsed.blocks)) {
|
|
return parsed.blocks.map(renderPreviewBlock).join('');
|
|
}
|
|
} catch (_error) {
|
|
// fall through to raw text
|
|
}
|
|
return '<p>' + sanitizePreviewHtml(raw) + '</p>';
|
|
}
|
|
|
|
function renderPreviewTextRegion(region, value, style, scale) {
|
|
var fontFamily = style.font_family ? 'font-family:' + escapeHtml(style.font_family) + ';' : '';
|
|
var fontSize = style.font_size ? 'font-size:' + Math.max(1, Math.round(Number(style.font_size) * scale)) + 'px;' : '';
|
|
var color = style.font_color ? 'color:' + escapeHtml(style.font_color) + ';' : '';
|
|
return '<div class="slide-preview-text-content" style="' + fontFamily + fontSize + color + '">' + 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 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 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 || 24)),
|
|
font_color: String(current.font_color || region.font_color || '#000000').trim() || '#000000'
|
|
};
|
|
}
|
|
|
|
function getTextStyleFromCard(card, region) {
|
|
var familyInput = card.querySelector('[name="region_font_family_' + region.id + '"]');
|
|
var sizeInput = card.querySelector('[name="region_font_size_' + region.id + '"]');
|
|
var colorInput = card.querySelector('[name="region_font_color_' + region.id + '"]');
|
|
var current = getCurrentTextStyle(region);
|
|
return {
|
|
font_family: String(familyInput ? familyInput.value : current.font_family).trim() || current.font_family,
|
|
font_size: Math.max(8, Number(sizeInput ? sizeInput.value : current.font_size)),
|
|
font_color: String(colorInput ? colorInput.value : current.font_color).trim() || current.font_color
|
|
};
|
|
}
|
|
|
|
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, stageRect, canvasWidth, canvasHeight) {
|
|
return {
|
|
left: (Number(region.x || 0) / canvasWidth) * stageRect.width,
|
|
top: (Number(region.y || 0) / canvasHeight) * stageRect.height,
|
|
width: (Number(region.width || 0) / canvasWidth) * stageRect.width,
|
|
height: (Number(region.height || 0) / canvasHeight) * stageRect.height
|
|
};
|
|
}
|
|
|
|
function renderPreview() {
|
|
var template = getSelectedTemplate();
|
|
if (!template) {
|
|
slidePreviewMeta.textContent = 'Select a template to preview the slide.';
|
|
slidePreviewBackground.style.display = 'none';
|
|
slidePreviewEmpty.style.display = 'flex';
|
|
slidePreviewEmpty.textContent = 'Select a template to preview the slide.';
|
|
slidePreviewOverlay.innerHTML = '';
|
|
slidePreviewStage.style.aspectRatio = '16 / 9';
|
|
return;
|
|
}
|
|
|
|
var canvasWidth = Math.max(1, Number(template.canvas_size_width || 1920));
|
|
var canvasHeight = Math.max(1, Number(template.canvas_size_height || 1080));
|
|
slidePreviewStage.style.aspectRatio = canvasWidth + ' / ' + canvasHeight;
|
|
slidePreviewMeta.textContent = template.name + ' • ' + 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 || '';
|
|
slidePreviewOverlay.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
slidePreviewEmpty.style.display = 'none';
|
|
slidePreviewBackground.style.display = template.background_image_path ? 'block' : 'none';
|
|
slidePreviewBackground.src = template.background_image_path || '';
|
|
|
|
var stageRect = slidePreviewStage.getBoundingClientRect();
|
|
var scale = Math.min(stageRect.width / canvasWidth, stageRect.height / canvasHeight) || 1;
|
|
var regions = template.regions || [];
|
|
slidePreviewOverlay.innerHTML = regions.map(function (region) {
|
|
var box = getRegionBox(region, stageRect, canvasWidth, canvasHeight);
|
|
var card = templateFields.querySelector('[data-region-id="' + region.id + '"]');
|
|
var hiddenInput = card && card.querySelector('input[type="hidden"][name="region_text_' + region.id + '"]');
|
|
var htmlInput = card && card.querySelector('textarea[name="region_html_' + region.id + '"]');
|
|
var imageHidden = card && card.querySelector('input[type="hidden"][name="existing_region_image_' + region.id + '"]');
|
|
var imageFileInput = card && card.querySelector('input[type="file"][name="region_image_' + region.id + '"]');
|
|
var webpageInput = card && card.querySelector('input[type="url"][name="region_webpage_' + region.id + '"]');
|
|
var value = region.region_type === 'image'
|
|
? ((imageFileInput && imageFileInput.dataset.previewUrl) ? imageFileInput.dataset.previewUrl : (imageHidden ? imageHidden.value : ''))
|
|
: region.region_type === 'webpage'
|
|
? (webpageInput ? webpageInput.value : '')
|
|
: region.region_type === 'html'
|
|
? (htmlInput ? htmlInput.value : '')
|
|
: (hiddenInput ? hiddenInput.value : '');
|
|
var style = region.region_type === 'image'
|
|
? null
|
|
: region.region_type === 'webpage'
|
|
? null
|
|
: region.region_type === 'html'
|
|
? null
|
|
: getTextStyleFromCard(card, region);
|
|
var content = region.region_type === 'image'
|
|
? (value ? '<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 === 'html'
|
|
? renderPreviewHtmlRegion(value)
|
|
: renderPreviewTextRegion(region, value, style, scale);
|
|
var selected = region.region_type === 'image'
|
|
? ' slide-preview-image-region'
|
|
: region.region_type === 'webpage'
|
|
? ' slide-preview-webpage-region'
|
|
: region.region_type === 'html'
|
|
? ' slide-preview-html-region'
|
|
: ' slide-preview-text-region';
|
|
return '<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('');
|
|
}
|
|
|
|
function renderTextRegion(region) {
|
|
var current = getCurrentRegionValue(region);
|
|
var style = getCurrentTextStyle(region);
|
|
var fontChoices = [
|
|
'Arial',
|
|
'Courier New',
|
|
'Garamond',
|
|
'Georgia',
|
|
'Helvetica',
|
|
'Impact',
|
|
'Lucida Console',
|
|
'Palatino',
|
|
'Tahoma',
|
|
'Times New Roman',
|
|
'Trebuchet MS',
|
|
'Verdana'
|
|
];
|
|
return '' +
|
|
'<div class="card template-field-card" data-region-id="' + region.id + '">' +
|
|
'<div class="template-field-head">' +
|
|
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
|
'<span class="chip">Text</span>' +
|
|
'</div>' +
|
|
'<div class="row template-text-style-row">' +
|
|
'<label style="flex: 1;">Font style' +
|
|
'<select name="region_font_family_' + region.id + '">' + fontChoices.map(function (font) {
|
|
return '<option value="' + escapeHtml(font) + '"' + (style.font_family === font ? ' selected' : '') + '>' + escapeHtml(font) + '</option>';
|
|
}).join('') + '</select>' +
|
|
'</label>' +
|
|
'<label style="width: 120px;">Size' +
|
|
'<input type="number" name="region_font_size_' + region.id + '" min="8" max="180" step="1" value="' + escapeHtml(style.font_size) + '" />' +
|
|
'</label>' +
|
|
'<label style="width: 140px;">Color' +
|
|
'<input type="color" name="region_font_color_' + region.id + '" value="' + escapeHtml(style.font_color) + '" />' +
|
|
'</label>' +
|
|
'</div>' +
|
|
'<div class="editorjs-holder" id="editorjs-' + region.id + '" data-region-id="' + region.id + '"></div>' +
|
|
'<input type="hidden" name="region_text_' + region.id + '" value="' + escapeHtml(current) + '" />' +
|
|
'</div>';
|
|
}
|
|
|
|
function renderWebpageRegion(region) {
|
|
var current = getCurrentRegionValue(region);
|
|
return '' +
|
|
'<div class="card template-field-card" data-region-id="' + region.id + '">' +
|
|
'<div class="template-field-head">' +
|
|
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
|
'<span class="chip">Webpage</span>' +
|
|
'</div>' +
|
|
'<label style="display:block; margin-top:12px;">Webpage URL' +
|
|
'<input type="url" name="region_webpage_' + region.id + '" value="' + escapeHtml(current) + '" placeholder="https://example.com" />' +
|
|
'</label>' +
|
|
'<div class="muted slide-image-file">The webpage will be loaded in an iframe.</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
function renderHtmlRegion(region) {
|
|
var current = getCurrentRegionValue(region);
|
|
return '' +
|
|
'<div class="card template-field-card" data-region-id="' + region.id + '">' +
|
|
'<div class="template-field-head">' +
|
|
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
|
'<span class="chip">HTML</span>' +
|
|
'</div>' +
|
|
'<label style="display:block; margin-top:12px;">HTML content' +
|
|
'<textarea name="region_html_' + region.id + '" rows="10" placeholder="<div>Hello</div>">' + escapeHtml(current) + '</textarea>' +
|
|
'</label>' +
|
|
'<div class="muted slide-image-file">HTML is rendered in a sandboxed iframe preview.</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
function renderImageRegion(region) {
|
|
var current = getCurrentRegionValue(region);
|
|
return '' +
|
|
'<div class="card template-field-card" data-region-id="' + region.id + '">' +
|
|
'<div class="template-field-head">' +
|
|
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
|
'<span class="chip">Image</span>' +
|
|
'</div>' +
|
|
'<label style="display:block; margin-top:12px;">Image file' +
|
|
'<input type="file" name="region_image_' + region.id + '" accept="image/*" />' +
|
|
'</label>' +
|
|
'<input type="hidden" name="existing_region_image_' + region.id + '" value="' + escapeHtml(current) + '" />' +
|
|
(current
|
|
? '<div class="muted slide-image-file">Current: <a href="' + escapeHtml(current) + '" target="_blank" rel="noreferrer">' + escapeHtml(current) + '</a></div>'
|
|
: '<div class="muted slide-image-file">No image selected.</div>') +
|
|
'</div>';
|
|
}
|
|
|
|
function renderTemplate() {
|
|
clearImagePreviewUrls();
|
|
destroyEditors();
|
|
var template = getTemplateById(templateSelect.value);
|
|
if (!template) {
|
|
templateFields.innerHTML = '';
|
|
renderPreview();
|
|
return;
|
|
}
|
|
|
|
templateFields.innerHTML = template.regions.length ? template.regions.map(function (region) {
|
|
if (region.region_type === 'image') {
|
|
return renderImageRegion(region);
|
|
}
|
|
if (region.region_type === 'webpage') {
|
|
return renderWebpageRegion(region);
|
|
}
|
|
if (region.region_type === 'html') {
|
|
return renderHtmlRegion(region);
|
|
}
|
|
return renderTextRegion(region);
|
|
}).join('') : '<div class="muted">This template has no regions.</div>';
|
|
|
|
templateFields.querySelectorAll('.editorjs-holder').forEach(function (holder) {
|
|
var regionId = holder.getAttribute('data-region-id');
|
|
var hidden = getEditorHiddenInput(regionId);
|
|
var editor = new EditorJS({
|
|
holder: holder.id,
|
|
minHeight: 120,
|
|
placeholder: 'Write content...',
|
|
inlineToolbar: true,
|
|
tools: {
|
|
header: {
|
|
class: Header,
|
|
config: {
|
|
placeholder: 'Enter a header',
|
|
levels: [1, 2, 3, 4],
|
|
defaultLevel: 3
|
|
}
|
|
},
|
|
list: {
|
|
class: EditorjsList,
|
|
inlineToolbar: false,
|
|
config: {
|
|
defaultStyle: 'unordered'
|
|
}
|
|
},
|
|
table: {
|
|
class: Table,
|
|
inlineToolbar: true,
|
|
config: {
|
|
rows: 2,
|
|
cols: 3,
|
|
maxRows: 5,
|
|
maxCols: 5
|
|
}
|
|
},
|
|
marker: {
|
|
class: Marker
|
|
}
|
|
},
|
|
data: normalizeEditorData(hidden ? hidden.value : ''),
|
|
onChange: function () {
|
|
var instance = editorInstances.get(regionId);
|
|
if (!instance) {
|
|
return;
|
|
}
|
|
instance.save().then(function (output) {
|
|
if (hidden) {
|
|
hidden.value = JSON.stringify(output);
|
|
}
|
|
renderPreview();
|
|
});
|
|
}
|
|
});
|
|
editorInstances.set(regionId, editor);
|
|
});
|
|
|
|
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]);
|
|
}
|
|
renderPreview();
|
|
});
|
|
});
|
|
renderPreview();
|
|
}
|
|
|
|
slideForm.addEventListener('submit', async function (event) {
|
|
if (submitting) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
var saves = Array.prototype.map.call(templateFields.querySelectorAll('.editorjs-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();
|
|
}
|
|
return editor.save().then(function (output) {
|
|
hidden.value = JSON.stringify(output);
|
|
});
|
|
});
|
|
submitting = true;
|
|
try {
|
|
await Promise.all(saves);
|
|
var response = await fetch(slideForm.action, {
|
|
method: (slideForm.method || 'POST').toUpperCase(),
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'text/html, application/json, text/plain, */*'
|
|
},
|
|
body: new FormData(slideForm),
|
|
credentials: 'same-origin'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(await response.text() || 'Unable to save slide.');
|
|
}
|
|
|
|
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.');
|
|
window.location.href = response.url || slideForm.action;
|
|
} finally {
|
|
submitting = false;
|
|
}
|
|
});
|
|
|
|
templateSelect.addEventListener('change', renderTemplate);
|
|
templateFields.addEventListener('change', renderPreview);
|
|
templateFields.addEventListener('input', renderPreview);
|
|
if (existingTemplateId) {
|
|
templateSelect.value = String(existingTemplateId);
|
|
}
|
|
renderTemplate();
|
|
window.addEventListener('resize', renderPreview);
|
|
})();
|