Save worktree changes
This commit is contained in:
@@ -25,6 +25,8 @@ import {
|
||||
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;
|
||||
@@ -38,6 +40,8 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
}
|
||||
|
||||
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');
|
||||
@@ -94,7 +98,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
function getCurrentFontSizeValue() {
|
||||
var value = fontSizeCommand.value;
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '';
|
||||
return String(DEFAULT_FONT_SIZE);
|
||||
}
|
||||
|
||||
var parsed = Math.round(Number(String(value).replace(/[^0-9.]/g, '')));
|
||||
@@ -118,6 +122,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
view.element.inputMode = 'numeric';
|
||||
view.element.autocomplete = 'off';
|
||||
view.element.spellcheck = false;
|
||||
view.element.style.width = '3.5em';
|
||||
syncValue();
|
||||
});
|
||||
|
||||
@@ -406,6 +411,15 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
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) {
|
||||
@@ -434,19 +448,286 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
}
|
||||
|
||||
function normalizeFontSizeValue(value) {
|
||||
var size = Math.max(1, Math.round(Number(String(value || '').replace(/[^0-9.]/g, ''))));
|
||||
return size ? String(size) : '';
|
||||
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 || 24)),
|
||||
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);
|
||||
}
|
||||
@@ -724,10 +1005,25 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
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 : '');
|
||||
@@ -735,6 +1031,8 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
? null
|
||||
: region.region_type === 'webpage'
|
||||
? null
|
||||
: region.region_type === 'rtmp'
|
||||
? null
|
||||
: region.region_type === 'html'
|
||||
? null
|
||||
: getTextStyleFromCard(card, region);
|
||||
@@ -742,6 +1040,23 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
? (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);
|
||||
@@ -749,6 +1064,12 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
? ' 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';
|
||||
@@ -779,6 +1100,96 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
'</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 '' +
|
||||
@@ -796,6 +1207,29 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
'</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 '' +
|
||||
@@ -851,6 +1285,15 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
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);
|
||||
}
|
||||
@@ -869,8 +1312,6 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
'undo',
|
||||
'redo',
|
||||
'|',
|
||||
'heading',
|
||||
'|',
|
||||
'fontSizeInput',
|
||||
'fontFamily',
|
||||
'fontColor',
|
||||
@@ -904,7 +1345,6 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
FontFamily,
|
||||
FontSize,
|
||||
FontSizeInputUI,
|
||||
Heading,
|
||||
HorizontalLine,
|
||||
Indent,
|
||||
IndentBlock,
|
||||
@@ -922,29 +1362,19 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
options: [20, 24, 28, 32, 36, 40, 44],
|
||||
supportAllValues: true
|
||||
},
|
||||
heading: {
|
||||
options: [
|
||||
{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
|
||||
{ model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
|
||||
{ model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' },
|
||||
{ model: 'heading3', view: 'h3', title: 'Heading 3', class: 'ck-heading_heading3' },
|
||||
{ model: 'heading4', view: 'h4', title: 'Heading 4', class: 'ck-heading_heading4' }
|
||||
]
|
||||
},
|
||||
initialData: normalizeEditorData((source && source.value) || (hidden ? hidden.value : ''))
|
||||
};
|
||||
|
||||
ClassicEditor.create(source || holder, editorConfig).then(function (editor) {
|
||||
editorInstances.set(regionId, editor);
|
||||
var fontSizeHidden = getEditorFontSizeHiddenInput(regionId);
|
||||
var fontSizeCommand = editor.commands.get('fontSize');
|
||||
if (hidden) {
|
||||
hidden.value = editor.getData();
|
||||
}
|
||||
if (fontSizeHidden && fontSizeCommand) {
|
||||
fontSizeHidden.value = normalizeFontSizeValue(fontSizeCommand.value) || fontSizeHidden.value;
|
||||
syncEditorFontSizeHidden(regionId, editor);
|
||||
if (fontSizeCommand) {
|
||||
fontSizeCommand.on('change:value', function () {
|
||||
fontSizeHidden.value = normalizeFontSizeValue(fontSizeCommand.value) || fontSizeHidden.value;
|
||||
syncEditorFontSizeHidden(regionId, editor);
|
||||
});
|
||||
}
|
||||
editor.model.document.on('change:data', function () {
|
||||
@@ -990,6 +1420,42 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
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();
|
||||
});
|
||||
@@ -1001,6 +1467,8 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
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()
|
||||
: '';
|
||||
@@ -1011,6 +1479,7 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
if (!editor || !hidden) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
syncEditorFontSizeHidden(regionId, editor);
|
||||
hidden.value = editor.getData();
|
||||
return Promise.resolve();
|
||||
});
|
||||
@@ -1043,6 +1512,20 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
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 {
|
||||
@@ -1057,6 +1540,13 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1105,3 +1595,4 @@ import { createTemplateSelectorLockController } from '/assets/js/slides/slide-fo
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user