869 lines
37 KiB
JavaScript
869 lines
37 KiB
JavaScript
// Region-specific helpers for slide forms.
|
|
|
|
import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-preview.js';
|
|
|
|
export function createSlideFormRegionHelpers(options) {
|
|
var settings = options || {};
|
|
var templateFields = settings.templateFields || null;
|
|
var existingContent = settings.existingContent || {};
|
|
var rssFeeds = Array.isArray(settings.rssFeeds) ? settings.rssFeeds : [];
|
|
var apiSources = Array.isArray(settings.apiSources) ? settings.apiSources : [];
|
|
var timetableGroups = Array.isArray(settings.timetableGroups) ? settings.timetableGroups : [];
|
|
var getRegionTypeModule = typeof settings.getRegionTypeModule === 'function' ? settings.getRegionTypeModule : function () { return null; };
|
|
var requestPreviewRender = typeof settings.requestPreviewRender === 'function' ? settings.requestPreviewRender : function () {};
|
|
var placeholderUtils = window.placeholderUtils || {};
|
|
var placeholderChips = window.placeholderChips || {};
|
|
var escapeHtml = typeof settings.escapeHtml === 'function'
|
|
? settings.escapeHtml
|
|
: function (value) {
|
|
return String(value === undefined || value === null ? '' : value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
};
|
|
var previewHelpers = createSlideFormPreviewHelpers({
|
|
getRegionTypeModule: getRegionTypeModule,
|
|
escapeHtml: escapeHtml
|
|
});
|
|
var defaultFontSize = Math.max(1, Number(settings.defaultFontSize || 32));
|
|
var uploadMaxLabel = String(settings.uploadMaxLabel || '100 MB');
|
|
var uploadVideoMaxLabel = String(settings.uploadVideoMaxLabel || '1 GB');
|
|
var uploadMaxBytes = Number(settings.uploadMaxBytes || 100 * 1024 * 1024);
|
|
var uploadVideoMaxBytes = Number(settings.uploadVideoMaxBytes || 1024 * 1024 * 1024);
|
|
var uploadMimeTypes = Array.isArray(settings.uploadMimeTypes) ? settings.uploadMimeTypes : [];
|
|
|
|
function sanitizeFontSize(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 String(defaultFontSize);
|
|
}
|
|
|
|
var sanitizePreviewHtml = previewHelpers.sanitizePreviewHtml;
|
|
var sanitizeTagAttributes = previewHelpers.sanitizeTagAttributes;
|
|
var renderPreviewListItem = previewHelpers.renderPreviewListItem;
|
|
var renderPreviewTable = previewHelpers.renderPreviewTable;
|
|
var renderPreviewBlock = previewHelpers.renderPreviewBlock;
|
|
var renderPreviewText = previewHelpers.renderPreviewText;
|
|
var renderPreviewTextRegion = previewHelpers.renderPreviewTextRegion;
|
|
var startPreviewVideoPlayback = previewHelpers.startPreviewVideoPlayback;
|
|
|
|
function getCurrentTextStyle(region) {
|
|
var current = existingContent[region.region_key] || {};
|
|
var textRegion = getRegionTypeModule('text');
|
|
var defaultStyle = window.pulseRegionTypes && typeof window.pulseRegionTypes.getDefaultRegionStyle === 'function'
|
|
? window.pulseRegionTypes.getDefaultRegionStyle('text')
|
|
: textRegion && typeof textRegion.getDefaultStyle === 'function'
|
|
? textRegion.getDefaultStyle()
|
|
: {
|
|
font_family: 'Arial',
|
|
font_size: defaultFontSize,
|
|
font_color: '#000000'
|
|
};
|
|
return {
|
|
font_family: String(current.font_family || region.font_family || defaultStyle.font_family || 'Arial').trim() || 'Arial',
|
|
font_size: Math.max(8, Number(current.font_size || defaultStyle.font_size || defaultFontSize)),
|
|
font_color: String(current.font_color || region.font_color || defaultStyle.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 getRssFeedItemCount(feedId) {
|
|
var feed = getRssFeedById(feedId);
|
|
return feed && Array.isArray(feed.items) ? feed.items.length : 0;
|
|
}
|
|
|
|
function getRssFieldList(feedId) {
|
|
var module = getRegionTypeModule('rss');
|
|
if (module && typeof module.getFieldList === 'function') {
|
|
return module.getFieldList(feedId, rssFeeds);
|
|
}
|
|
|
|
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 buildLimitedPlaceholderChipMarkup(fieldList, emptyLabel, includeTransformHint) {
|
|
var fields = Array.isArray(fieldList) ? fieldList : [];
|
|
var visibleFields = fields.slice(0, 8);
|
|
var overflowFields = fields.slice(visibleFields.length);
|
|
|
|
if (!fields.length) {
|
|
return '<span class="muted slide-image-file">' + escapeHtml(emptyLabel) + '</span>';
|
|
}
|
|
|
|
var visibleMarkup = visibleFields.map(function (field) {
|
|
return typeof placeholderChips.renderChip === 'function'
|
|
? placeholderChips.renderChip(field)
|
|
: '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
|
|
}).join('');
|
|
var transformHint = includeTransformHint === false ? '' : '<div class="text-body-secondary small mt-1">Placeholder values support transforms, for example <code>{{title.upper()}}</code>, <code>{{title.title()}}</code>, <code>{{title.lower()}}</code>, <code>{{publishedAt.format("MMM D, YYYY")}}</code>.</div>';
|
|
|
|
if (!overflowFields.length) {
|
|
return visibleMarkup + transformHint;
|
|
}
|
|
|
|
return '' +
|
|
visibleMarkup +
|
|
transformHint +
|
|
'<details class="w-100 mt-1" data-placeholder-chips-more>' +
|
|
'<summary class="small text-body-secondary">Show ' + escapeHtml(overflowFields.length) + ' more fields</summary>' +
|
|
'<div class="d-flex flex-wrap gap-2 mt-1">' + overflowFields.map(function (field) {
|
|
return typeof placeholderChips.renderChip === 'function'
|
|
? placeholderChips.renderChip(field)
|
|
: '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
|
|
}).join('') + '</div>' +
|
|
'</details>';
|
|
}
|
|
|
|
function truncateJsonPreview(value, maxLength) {
|
|
var text = '';
|
|
try {
|
|
text = JSON.stringify(value, null, 2);
|
|
} catch (_error) {
|
|
text = String(value === undefined || value === null ? '' : value);
|
|
}
|
|
|
|
if (!text) {
|
|
return '';
|
|
}
|
|
|
|
var limit = Math.max(200, Number(maxLength || 1100));
|
|
if (text.length <= limit) {
|
|
return text;
|
|
}
|
|
|
|
return text.slice(0, limit).replace(/\s+$/, '') + '\n...';
|
|
}
|
|
|
|
function buildApiSampleDataMarkup(sourceId, itemNumber, itemsPathOverride) {
|
|
var source = getApiSourceById(sourceId);
|
|
if (!source) {
|
|
return '<div class="api-region-sample-meta">Select an API source to preview sample data.</div>';
|
|
}
|
|
|
|
var item = getApiSourceItems(sourceId, itemsPathOverride)[Math.max(0, Math.max(1, Number(itemNumber || 1)) - 1)] || null;
|
|
if (!item) {
|
|
return '<div class="api-region-sample-meta">No sample item is available for the current selection.</div>';
|
|
}
|
|
|
|
return '<pre class="api-region-sample-preview" data-api-sample-data-output>' + escapeHtml(truncateJsonPreview(item, 1100)) + '</pre>';
|
|
}
|
|
|
|
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 typeof placeholderChips.renderChip === 'function'
|
|
? placeholderChips.renderChip(field)
|
|
: '<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 ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null;
|
|
if (!card) {
|
|
return;
|
|
}
|
|
|
|
var container = card.querySelector('[data-placeholder-chips]');
|
|
if (!container) {
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = buildLimitedPlaceholderChipMarkup(getRssFieldList(feedId), 'No RSS fields available.');
|
|
}
|
|
|
|
function updateRssItemNumberLimit(regionId, feedId) {
|
|
var card = templateFields ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null;
|
|
if (!card) {
|
|
return;
|
|
}
|
|
|
|
var input = card.querySelector('input[name="region_rss_item_number_' + regionId + '"]');
|
|
if (!input) {
|
|
return;
|
|
}
|
|
|
|
var itemCount = Math.max(1, Number(getRssFeedItemCount(feedId) || 1));
|
|
input.setAttribute('max', String(itemCount));
|
|
if (Number(input.value || 0) > itemCount) {
|
|
input.value = String(itemCount);
|
|
}
|
|
}
|
|
|
|
function clampRssItemNumber(regionId, feedId, itemNumber) {
|
|
var card = templateFields ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null;
|
|
if (!card) {
|
|
return '';
|
|
}
|
|
|
|
var input = card.querySelector('input[name="region_rss_item_number_' + regionId + '"]');
|
|
if (!input) {
|
|
return '';
|
|
}
|
|
|
|
var itemCount = Math.max(1, Number(getRssFeedItemCount(feedId) || 1));
|
|
var parsedValue = Math.max(1, Number(itemNumber || 1));
|
|
var clampedValue = Math.min(itemCount, Number.isFinite(parsedValue) && parsedValue > 0 ? parsedValue : 1);
|
|
input.setAttribute('max', String(itemCount));
|
|
input.value = String(clampedValue);
|
|
return String(clampedValue);
|
|
}
|
|
|
|
function getCurrentRssConfig(region) {
|
|
var module = getRegionTypeModule('rss');
|
|
if (module && typeof module.getCurrentConfig === 'function') {
|
|
return module.getCurrentConfig(region, existingContent);
|
|
}
|
|
|
|
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 resolvePath(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 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(resolvePath(item, key || ''));
|
|
});
|
|
}
|
|
|
|
function getRssItem(feedId, itemNumber) {
|
|
var module = getRegionTypeModule('rss');
|
|
if (module && typeof module.getItem === 'function') {
|
|
return module.getItem(feedId, itemNumber, rssFeeds);
|
|
}
|
|
|
|
var feed = getRssFeedById(feedId);
|
|
var items = feed && Array.isArray(feed.items) ? feed.items : [];
|
|
var parsedItemNumber = Math.max(1, Number(itemNumber || 1));
|
|
var index = Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber - 1 : 0;
|
|
return items[index] || null;
|
|
}
|
|
|
|
function getRssPreviewFallback(item) {
|
|
var module = getRegionTypeModule('rss');
|
|
if (module && typeof module.getPreviewFallback === 'function') {
|
|
return module.getPreviewFallback(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 getApiSourceItemsPath(source, overridePath) {
|
|
if (overridePath === undefined || overridePath === null) {
|
|
return String(source && (source.items_path || source.itemsPath) || '').trim();
|
|
}
|
|
|
|
return String(overridePath || '').trim();
|
|
}
|
|
|
|
function getApiSourceItems(sourceId, itemsPathOverride) {
|
|
var source = getApiSourceById(sourceId);
|
|
var module = getRegionTypeModule('api');
|
|
if (module && typeof module.getItems === 'function') {
|
|
return module.getItems(sourceId, apiSources, itemsPathOverride);
|
|
}
|
|
|
|
var responseJson = source && source.responseJson && typeof source.responseJson === 'object' ? source.responseJson : null;
|
|
var itemsPath = getApiSourceItemsPath(source, itemsPathOverride);
|
|
if (itemsPath !== undefined && itemsPath !== null && itemsPath !== '') {
|
|
var current = responseJson;
|
|
String(itemsPath).split('.').forEach(function (segment) {
|
|
if (current === undefined || current === null) {
|
|
current = '';
|
|
return;
|
|
}
|
|
current = current[segment];
|
|
});
|
|
return Array.isArray(current) ? current : [];
|
|
}
|
|
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 getApiItemCount(sourceId, itemsPathOverride) {
|
|
var module = getRegionTypeModule('api');
|
|
if (module && typeof module.getItemCount === 'function') {
|
|
return module.getItemCount(sourceId, apiSources, itemsPathOverride);
|
|
}
|
|
|
|
return getApiSourceItems(sourceId, itemsPathOverride).length;
|
|
}
|
|
|
|
function getApiFieldList(sourceId, itemsPathOverride) {
|
|
var sampleItem = getApiSourceItems(sourceId, itemsPathOverride)[0] || null;
|
|
if (typeof placeholderUtils.collectPlaceholderFieldPaths === 'function') {
|
|
return placeholderUtils.collectPlaceholderFieldPaths(sampleItem && typeof sampleItem === 'object' ? sampleItem : null);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function updateApiPlaceholderChips(regionId, sourceId, itemsPathOverride) {
|
|
var card = templateFields ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null;
|
|
if (!card) {
|
|
return;
|
|
}
|
|
|
|
var container = card.querySelector('[data-placeholder-chips]');
|
|
if (!container) {
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = buildLimitedPlaceholderChipMarkup(getApiFieldList(sourceId, itemsPathOverride), 'No JSON fields available.', false);
|
|
}
|
|
|
|
function updateApiSampleDataPanel(regionId, sourceId, itemNumber, itemsPathOverride) {
|
|
var card = templateFields ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null;
|
|
if (!card) {
|
|
return;
|
|
}
|
|
|
|
var container = card.querySelector('[data-api-sample-data-panel]');
|
|
if (!container) {
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = buildApiSampleDataMarkup(sourceId, itemNumber, itemsPathOverride);
|
|
}
|
|
|
|
function updateApiItemNumberLimit(regionId, sourceId, itemsPathOverride) {
|
|
var card = templateFields ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null;
|
|
if (!card) {
|
|
return;
|
|
}
|
|
|
|
var input = card.querySelector('input[name="region_api_item_number_' + regionId + '"]');
|
|
if (!input) {
|
|
return;
|
|
}
|
|
|
|
var itemCount = Math.max(1, Number(getApiItemCount(sourceId, itemsPathOverride) || 1));
|
|
input.setAttribute('max', String(itemCount));
|
|
if (Number(input.value || 0) > itemCount) {
|
|
input.value = String(itemCount);
|
|
}
|
|
}
|
|
|
|
function clampApiItemNumber(regionId, sourceId, itemNumber, itemsPathOverride) {
|
|
var card = templateFields ? templateFields.querySelector('[data-region-id="' + regionId + '"]') : null;
|
|
if (!card) {
|
|
return '';
|
|
}
|
|
|
|
var input = card.querySelector('input[name="region_api_item_number_' + regionId + '"]');
|
|
if (!input) {
|
|
return '';
|
|
}
|
|
|
|
var itemCount = Math.max(1, Number(getApiItemCount(sourceId, itemsPathOverride) || 1));
|
|
var parsedValue = Math.max(1, Number(itemNumber || 1));
|
|
var clampedValue = Math.min(itemCount, Number.isFinite(parsedValue) && parsedValue > 0 ? parsedValue : 1);
|
|
input.setAttribute('max', String(itemCount));
|
|
input.value = String(clampedValue);
|
|
return String(clampedValue);
|
|
}
|
|
|
|
function getCurrentApiConfig(region) {
|
|
var module = getRegionTypeModule('api');
|
|
if (module && typeof module.getCurrentConfig === 'function') {
|
|
return module.getCurrentConfig(region, existingContent, apiSources);
|
|
}
|
|
|
|
var current = existingContent[region.region_key] || {};
|
|
var source = getApiSourceById(current.source_id);
|
|
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,
|
|
items_path: Object.prototype.hasOwnProperty.call(current, 'items_path') ? String(current.items_path === undefined || current.items_path === null ? '' : current.items_path) : undefined,
|
|
default_items_path: String(source && (source.items_path || source.itemsPath) || '').trim(),
|
|
value: current.value !== undefined ? current.value : ''
|
|
};
|
|
}
|
|
|
|
function reduceAspectRatio(width, height) {
|
|
var a = Math.max(1, Math.round(Number(width || 0) || 1));
|
|
var b = Math.max(1, Math.round(Number(height || 0) || 1));
|
|
var x = a;
|
|
var y = b;
|
|
|
|
while (y !== 0) {
|
|
var remainder = x % y;
|
|
x = y;
|
|
y = remainder;
|
|
}
|
|
|
|
return Math.max(1, Math.round(a / x)) + ':' + Math.max(1, Math.round(b / x));
|
|
}
|
|
|
|
function getCurrentRegionValue(region) {
|
|
var current = existingContent[region.region_key];
|
|
return current && current.value !== undefined ? current.value : '';
|
|
}
|
|
|
|
function getCurrentTimeDateConfig(region) {
|
|
var current = existingContent[region.region_key] || {};
|
|
return {
|
|
value: current.value !== undefined ? current.value : '',
|
|
timezone: current.timezone !== undefined ? current.timezone : (current.time_zone !== undefined ? current.time_zone : '')
|
|
};
|
|
}
|
|
|
|
function getCurrentTimetableConfig(region) {
|
|
var current = existingContent[region.region_key] || {};
|
|
return {
|
|
timetable_group_id: current.timetable_group_id === undefined || current.timetable_group_id === null || current.timetable_group_id === '' ? '' : Number(current.timetable_group_id),
|
|
display_mode: String(current.display_mode || 'upcoming').trim().toLowerCase() || 'upcoming',
|
|
max_items: Math.max(1, Number(current.max_items || 5))
|
|
};
|
|
}
|
|
|
|
function getCurrentRegionVideoDuration(region) {
|
|
var current = existingContent[region.region_key] || {};
|
|
var duration = Math.round(Number(current.duration_seconds || 0) * 1000) / 1000;
|
|
return Number.isFinite(duration) && duration > 0 ? String(duration) : '';
|
|
}
|
|
|
|
function buildRegionEditorCardContext(region) {
|
|
var module = getRegionTypeModule(region.region_type);
|
|
if (module && typeof module.buildEditorCardContext === 'function') {
|
|
return module.buildEditorCardContext({
|
|
region: region,
|
|
current: region.region_type === 'rss' || region.region_type === 'api' || region.region_type === 'time-date' || region.region_type === 'timetable' ? (existingContent[region.region_key] || {}) : getCurrentRegionValue(region),
|
|
currentDuration: getCurrentRegionVideoDuration(region),
|
|
regionRatio: reduceAspectRatio(region.width, region.height),
|
|
uploadMaxLabel: uploadMaxLabel,
|
|
uploadVideoMaxLabel: uploadVideoMaxLabel,
|
|
uploadMaxBytes: uploadMaxBytes,
|
|
uploadVideoMaxBytes: uploadVideoMaxBytes,
|
|
uploadMimeTypes: uploadMimeTypes,
|
|
disableAudio: (existingContent[region.region_key] || {}).disable_audio,
|
|
config: region.region_type === 'api' ? getCurrentApiConfig(region) : region.region_type === 'timetable' ? getCurrentTimetableConfig(region) : getCurrentRssConfig(region),
|
|
style: getCurrentTextStyle(region),
|
|
fontSize: getCurrentTextStyle(region).font_size,
|
|
feedOptions: rssFeeds.map(function (feed) {
|
|
var selected = Number(feed.id) === Number(getCurrentRssConfig(region).feed_id) ? ' selected' : '';
|
|
return '<option value="' + escapeHtml(feed.id) + '"' + selected + '>' + escapeHtml(feed.name || ('Feed ' + feed.id)) + '</option>';
|
|
}).join(''),
|
|
sourceOptions: apiSources.map(function (source) {
|
|
var selected = Number(source.id) === Number(getCurrentApiConfig(region).source_id) ? ' selected' : '';
|
|
return '<option value="' + escapeHtml(source.id) + '"' + selected + '>' + escapeHtml(source.name || ('Source ' + source.id)) + '</option>';
|
|
}).join(''),
|
|
placeholderChips: region.region_type === 'rss'
|
|
? getRssFieldList(getCurrentRssConfig(region).feed_id).map(function (field) {
|
|
return '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
|
|
}).join('')
|
|
: buildLimitedPlaceholderChipMarkup(getApiFieldList(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).items_path), 'No JSON fields available.', false),
|
|
placeholderFields: region.region_type === 'api'
|
|
? getApiFieldList(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).items_path)
|
|
: region.region_type === 'rss'
|
|
? getRssFieldList(getCurrentRssConfig(region).feed_id)
|
|
: [],
|
|
sampleDataPreview: buildApiSampleDataMarkup(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).item_number, getCurrentApiConfig(region).items_path),
|
|
timetableGroups: timetableGroups
|
|
}, existingContent, region.region_type === 'rss' ? rssFeeds : region.region_type === 'timetable' ? timetableGroups : apiSources);
|
|
}
|
|
|
|
var textStyle = getCurrentTextStyle(region);
|
|
var rssConfig = getCurrentRssConfig(region);
|
|
var apiConfig = getCurrentApiConfig(region);
|
|
var currentContent = existingContent[region.region_key] || {};
|
|
var apiItemsPath = apiConfig.items_path !== undefined ? apiConfig.items_path : apiConfig.default_items_path;
|
|
|
|
return {
|
|
region: region,
|
|
current: region.region_type === 'rss' || region.region_type === 'api' ? currentContent : getCurrentRegionValue(region),
|
|
currentDuration: getCurrentRegionVideoDuration(region),
|
|
regionRatio: reduceAspectRatio(region.width, region.height),
|
|
uploadMaxLabel: uploadMaxLabel,
|
|
uploadVideoMaxLabel: uploadVideoMaxLabel,
|
|
uploadMaxBytes: uploadMaxBytes,
|
|
uploadVideoMaxBytes: uploadVideoMaxBytes,
|
|
uploadMimeTypes: uploadMimeTypes,
|
|
disableAudio: currentContent.disable_audio,
|
|
config: region.region_type === 'api' ? apiConfig : region.region_type === 'timetable' ? getCurrentTimetableConfig(region) : rssConfig,
|
|
style: textStyle,
|
|
fontSize: textStyle.font_size,
|
|
feedOptions: rssFeeds.map(function (feed) {
|
|
var selected = Number(feed.id) === Number(rssConfig.feed_id) ? ' selected' : '';
|
|
return '<option value="' + escapeHtml(feed.id) + '"' + selected + '>' + escapeHtml(feed.name || ('Feed ' + feed.id)) + '</option>';
|
|
}).join(''),
|
|
sourceOptions: apiSources.map(function (source) {
|
|
var selected = Number(source.id) === Number(apiConfig.source_id) ? ' selected' : '';
|
|
return '<option value="' + escapeHtml(source.id) + '"' + selected + '>' + escapeHtml(source.name || ('Source ' + source.id)) + '</option>';
|
|
}).join(''),
|
|
placeholderChips: region.region_type === 'rss'
|
|
? buildLimitedPlaceholderChipMarkup(getRssFieldList(rssConfig.feed_id), 'No RSS fields available.')
|
|
: buildLimitedPlaceholderChipMarkup(getApiFieldList(apiConfig.source_id, apiItemsPath), 'No JSON fields available.', false),
|
|
placeholderFields: region.region_type === 'api'
|
|
? getApiFieldList(apiConfig.source_id, apiItemsPath)
|
|
: region.region_type === 'rss'
|
|
? getRssFieldList(rssConfig.feed_id)
|
|
: [],
|
|
sampleDataPreview: buildApiSampleDataMarkup(apiConfig.source_id, apiConfig.item_number, apiItemsPath),
|
|
timetableGroups: timetableGroups
|
|
};
|
|
}
|
|
|
|
function getRegionEditorCardHtml(region) {
|
|
var module = getRegionTypeModule(region.region_type);
|
|
if (!module || typeof module.renderEditorCard !== 'function') {
|
|
return '';
|
|
}
|
|
|
|
return makeRegionCardCollapsible(module.renderEditorCard(buildRegionEditorCardContext(region)), region);
|
|
}
|
|
|
|
function bindRegionCards() {
|
|
if (!templateFields) {
|
|
return;
|
|
}
|
|
|
|
Array.prototype.forEach.call(templateFields.querySelectorAll('[data-region-id]'), function (card) {
|
|
var module = null;
|
|
|
|
if (!card || !card.querySelector || !card.querySelector('input[type="url"][name^="region_qr_code_"]')) {
|
|
return;
|
|
}
|
|
|
|
module = getRegionTypeModule('qr-code');
|
|
|
|
if (module && typeof module.bindEditorCard === 'function') {
|
|
module.bindEditorCard({
|
|
card: card,
|
|
region: { id: card.getAttribute('data-region-id') },
|
|
existingContent: existingContent,
|
|
requestPreviewRender: requestPreviewRender
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
function syncRegionCards() {
|
|
if (!templateFields) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
var syncs = [];
|
|
Array.prototype.forEach.call(templateFields.querySelectorAll('[data-region-id]'), function (card) {
|
|
if (!card || !card.querySelector || !card.querySelector('input[type="url"][name^="region_qr_code_"]')) {
|
|
return;
|
|
}
|
|
|
|
if (typeof card.__pulseQrSync === 'function') {
|
|
syncs.push(Promise.resolve(card.__pulseQrSync()));
|
|
}
|
|
});
|
|
|
|
return Promise.all(syncs);
|
|
}
|
|
|
|
function makeRegionCardCollapsible(html, region) {
|
|
if (typeof html !== 'string' || !html) {
|
|
return html;
|
|
}
|
|
|
|
if (html.indexOf('data-lte-toggle="card-collapse"') !== -1) {
|
|
return html;
|
|
}
|
|
|
|
var cardHtml = html.replace(
|
|
'class="card card-outline card-secondary admin-form-card template-field-card mb-3"',
|
|
'class="card card-widget card-outline card-secondary admin-form-card template-field-card mb-3"'
|
|
);
|
|
|
|
if (cardHtml === html) {
|
|
return html;
|
|
}
|
|
|
|
var bodyOpenMatch = cardHtml.match(/<div class="card-body([^"]*)">/);
|
|
if (bodyOpenMatch) {
|
|
var bodyClassTokens = String(bodyOpenMatch[1] || '')
|
|
.trim()
|
|
.split(/\s+/)
|
|
.filter(Boolean);
|
|
var wrapperGapTokens = bodyClassTokens.filter(function (token) {
|
|
return /^gap-\d+$/i.test(token);
|
|
});
|
|
var wrapperPaddingTokens = bodyClassTokens.filter(function (token) {
|
|
return /^(?:p|pt|pb|ps|pe|px|py)-\d+$/i.test(token);
|
|
});
|
|
var bodyClassTokensWithoutGrid = bodyClassTokens.filter(function (token) {
|
|
return token !== 'd-grid' && !/^gap-\d+$/i.test(token) && !/^(?:p|pt|pb|ps|pe|px|py)-\d+$/i.test(token);
|
|
});
|
|
var bodyClassAttribute = ' class="card-body p-0' + (bodyClassTokensWithoutGrid.length ? ' ' + bodyClassTokensWithoutGrid.join(' ') : '') + '"';
|
|
var wrapperClassTokens = ['template-field-body', 'd-grid']
|
|
.concat(wrapperPaddingTokens)
|
|
.concat(wrapperGapTokens);
|
|
var wrapperClassAttribute = ' class="' + wrapperClassTokens.filter(Boolean).join(' ') + '"';
|
|
|
|
cardHtml = cardHtml.replace(bodyOpenMatch[0], '<div' + bodyClassAttribute + '><div' + wrapperClassAttribute + '>');
|
|
|
|
var lastCloseIndex = cardHtml.lastIndexOf('</div>');
|
|
if (lastCloseIndex !== -1) {
|
|
var bodyCloseIndex = cardHtml.lastIndexOf('</div>', lastCloseIndex - 1);
|
|
if (bodyCloseIndex !== -1) {
|
|
cardHtml = cardHtml.slice(0, bodyCloseIndex) + '</div>' + cardHtml.slice(bodyCloseIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
var collapseLabel = region && region.label ? String(region.label) : 'region';
|
|
var collapseButton = '' +
|
|
'<button type="button" class="btn btn-tool p-0 ms-auto text-body-secondary template-field-collapse" data-lte-toggle="card-collapse" aria-label="Collapse ' + escapeHtml(collapseLabel) + '" title="Collapse ' + escapeHtml(collapseLabel) + '">' +
|
|
'<i class="bi bi-dash-lg" aria-hidden="true" data-lte-icon="collapse"></i>' +
|
|
'<i class="bi bi-plus-lg" aria-hidden="true" data-lte-icon="expand"></i>' +
|
|
'<span class="visually-hidden">Collapse ' + escapeHtml(collapseLabel) + '</span>' +
|
|
'</button>';
|
|
|
|
var bodyMarkerIndex = cardHtml.indexOf('<div class="card-body');
|
|
if (bodyMarkerIndex === -1) {
|
|
return cardHtml;
|
|
}
|
|
|
|
var actionsMarker = '<div class="template-field-actions">';
|
|
if (cardHtml.indexOf(actionsMarker) !== -1) {
|
|
return cardHtml.replace(/(<div class="template-field-actions">[\s\S]*?)<\/div>/, '$1' + collapseButton + '</div>');
|
|
}
|
|
|
|
var headerCloseIndex = cardHtml.lastIndexOf('</div>', bodyMarkerIndex);
|
|
if (headerCloseIndex === -1) {
|
|
return cardHtml;
|
|
}
|
|
|
|
return cardHtml.slice(0, headerCloseIndex) + collapseButton + cardHtml.slice(headerCloseIndex);
|
|
}
|
|
|
|
function getPreviewRegionContent(card, region) {
|
|
var module = getRegionTypeModule(region.region_type);
|
|
if (module && typeof module.buildPreviewRenderContext === 'function') {
|
|
return module.buildPreviewRenderContext(region, card, existingContent, region.region_type === 'rss' ? rssFeeds : region.region_type === 'timetable' ? timetableGroups : apiSources);
|
|
}
|
|
|
|
var current = existingContent[region.region_key];
|
|
var content = current && typeof current === 'object' ? Object.assign({}, current) : {};
|
|
if (!card) {
|
|
return content;
|
|
}
|
|
|
|
var hiddenInput = card.querySelector('input[type="hidden"][name="region_text_' + region.id + '"]');
|
|
var textAreaInput = card.querySelector('textarea[name="region_text_' + region.id + '"]');
|
|
var htmlInput = card.querySelector('textarea[name="region_html_' + region.id + '"]');
|
|
var timeDateTimezoneInput = card.querySelector('input[name="region_timezone_' + region.id + '"]');
|
|
var imageHidden = card.querySelector('input[type="hidden"][name="existing_region_image_' + region.id + '"]');
|
|
var imageFileInput = card.querySelector('input[type="file"][name="region_image_' + region.id + '"]');
|
|
var videoHidden = card.querySelector('input[type="hidden"][name="existing_region_video_' + region.id + '"]');
|
|
var videoFileInput = card.querySelector('input[type="file"][name="region_video_' + region.id + '"]');
|
|
var webpageInput = card.querySelector('input[type="url"][name="region_webpage_' + region.id + '"]');
|
|
var rtmpInput = card.querySelector('input[type="url"][name="region_rtmp_' + region.id + '"]');
|
|
var rssFeedInput = card.querySelector('select[name="region_rss_feed_id_' + region.id + '"]');
|
|
var rssItemInput = card.querySelector('input[name="region_rss_item_number_' + region.id + '"]');
|
|
var apiSourceInput = card.querySelector('select[name="region_api_source_id_' + region.id + '"]');
|
|
var apiItemInput = card.querySelector('input[name="region_api_item_number_' + region.id + '"]');
|
|
var apiItemsPathInput = card.querySelector('input[name="region_api_items_path_' + region.id + '"]');
|
|
|
|
if (region.region_type === 'image') {
|
|
content.value = (imageFileInput && imageFileInput.dataset.previewUrl) ? imageFileInput.dataset.previewUrl : (imageHidden ? imageHidden.value : content.value || '');
|
|
} else if (region.region_type === 'video') {
|
|
content.value = (videoFileInput && videoFileInput.dataset.previewUrl) ? videoFileInput.dataset.previewUrl : (videoHidden ? videoHidden.value : content.value || '');
|
|
} else if (region.region_type === 'webpage') {
|
|
content.value = webpageInput ? webpageInput.value : (content.value || '');
|
|
} else if (region.region_type === 'rtmp') {
|
|
content.value = rtmpInput ? rtmpInput.value : (content.value || '');
|
|
content.disable_audio = false;
|
|
} else if (region.region_type === 'html') {
|
|
content.value = htmlInput ? htmlInput.value : (content.value || '');
|
|
} else if (region.region_type === 'time-date') {
|
|
var timeDateCurrent = getCurrentTimeDateConfig(region);
|
|
content.value = textAreaInput ? textAreaInput.value : (timeDateCurrent.value || '');
|
|
content.timezone = timeDateTimezoneInput ? timeDateTimezoneInput.value : (timeDateCurrent.timezone || '');
|
|
} else if (region.region_type === 'rss') {
|
|
content.value = hiddenInput ? hiddenInput.value : (content.value || '');
|
|
content.feed_id = rssFeedInput ? rssFeedInput.value : content.feed_id;
|
|
content.item_number = rssItemInput ? clampRssItemNumber(region.id, content.feed_id, rssItemInput.value) : content.item_number;
|
|
} else if (region.region_type === 'api') {
|
|
content.value = hiddenInput ? hiddenInput.value : (content.value || '');
|
|
content.source_id = apiSourceInput ? apiSourceInput.value : content.source_id;
|
|
content.item_number = apiItemInput ? apiItemInput.value : content.item_number;
|
|
content.items_path = apiItemsPathInput ? apiItemsPathInput.value : content.items_path;
|
|
} else if (region.region_type === 'timetable') {
|
|
var timetableGroupInput = card.querySelector('select[name="region_timetable_group_id_' + region.id + '"]');
|
|
var timetableDisplayModeInput = card.querySelector('select[name="region_timetable_display_mode_' + region.id + '"]');
|
|
var timetableMaxItemsInput = card.querySelector('input[name="region_timetable_max_items_' + region.id + '"]');
|
|
var currentSchedule = getCurrentTimetableConfig(region);
|
|
content.value = hiddenInput ? hiddenInput.value : (content.value || currentSchedule.value || '');
|
|
content.timetable_group_id = timetableGroupInput ? timetableGroupInput.value : currentSchedule.timetable_group_id;
|
|
content.display_mode = timetableDisplayModeInput ? timetableDisplayModeInput.value : currentSchedule.display_mode;
|
|
content.max_items = timetableMaxItemsInput ? timetableMaxItemsInput.value : currentSchedule.max_items;
|
|
} else {
|
|
content.value = hiddenInput ? hiddenInput.value : (content.value || '');
|
|
}
|
|
|
|
return content;
|
|
}
|
|
|
|
function buildPreviewRenderContext(region, card) {
|
|
var module = getRegionTypeModule(region.region_type);
|
|
if (module && typeof module.buildPreviewRenderContext === 'function') {
|
|
return module.buildPreviewRenderContext(region, card, existingContent, region.region_type === 'rss' ? rssFeeds : region.region_type === 'timetable' ? timetableGroups : apiSources);
|
|
}
|
|
|
|
var textStyle = getCurrentTextStyle(region);
|
|
var previewContent = getPreviewRegionContent(card, region);
|
|
return {
|
|
value: previewContent.value,
|
|
timezone: previewContent.timezone,
|
|
style: textStyle,
|
|
existingContent: existingContent,
|
|
feeds: rssFeeds,
|
|
sources: apiSources,
|
|
disable_audio: previewContent.disable_audio === undefined ? true : Boolean(previewContent.disable_audio),
|
|
feed_id: previewContent.feed_id,
|
|
item_number: previewContent.item_number,
|
|
source_id: previewContent.source_id,
|
|
items_path: previewContent.items_path,
|
|
timetable_group_id: previewContent.timetable_group_id,
|
|
display_mode: previewContent.display_mode,
|
|
max_items: previewContent.max_items,
|
|
timetableGroups: timetableGroups,
|
|
font_size: textStyle.font_size,
|
|
font_color: textStyle.font_color,
|
|
font_family: textStyle.font_family
|
|
};
|
|
}
|
|
|
|
return {
|
|
sanitizePreviewHtml: sanitizePreviewHtml,
|
|
sanitizeTagAttributes: sanitizeTagAttributes,
|
|
getCurrentTextStyle: getCurrentTextStyle,
|
|
getRssFieldList: getRssFieldList,
|
|
updateRssPlaceholderChips: updateRssPlaceholderChips,
|
|
getCurrentRssConfig: getCurrentRssConfig,
|
|
getApiFieldList: getApiFieldList,
|
|
updateApiPlaceholderChips: updateApiPlaceholderChips,
|
|
updateApiSampleDataPanel: updateApiSampleDataPanel,
|
|
updateApiItemNumberLimit: updateApiItemNumberLimit,
|
|
updateRssItemNumberLimit: updateRssItemNumberLimit,
|
|
getCurrentApiConfig: getCurrentApiConfig,
|
|
reduceAspectRatio: reduceAspectRatio,
|
|
renderPreviewTextRegion: renderPreviewTextRegion,
|
|
startPreviewVideoPlayback: startPreviewVideoPlayback,
|
|
getRegionEditorCardHtml: getRegionEditorCardHtml,
|
|
bindRegionCards: bindRegionCards,
|
|
syncRegionCards: syncRegionCards,
|
|
buildRegionEditorCardContext: buildRegionEditorCardContext,
|
|
getPreviewRegionContent: getPreviewRegionContent,
|
|
buildPreviewRenderContext: buildPreviewRenderContext,
|
|
getCurrentRegionValue: getCurrentRegionValue,
|
|
getCurrentRegionVideoDuration: getCurrentRegionVideoDuration
|
|
};
|
|
}
|