// 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, '''); }; 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'); 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) { var fields = Array.isArray(fieldList) ? fieldList : []; var visibleFields = fields.slice(0, 8); var overflowFields = fields.slice(visibleFields.length); if (!fields.length) { return ''; } var visibleMarkup = visibleFields.map(function (field) { return typeof placeholderChips.renderChip === 'function' ? placeholderChips.renderChip(field) : '{{' + escapeHtml(field) + '}}'; }).join(''); var transformHint = '
{{title.upper()}}, {{title.title()}}, {{title.lower()}}, {{publishedAt.format("MMM D, YYYY")}}.' + escapeHtml(truncateJsonPreview(item, 1100)) + ''; } 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) : '{{' + escapeHtml(field) + '}}'; }).join('') : ''; container.innerHTML = chips || ''; } 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 ''; } var title = String(item.title || '').trim(); var description = String(item.description || '').trim(); var summary = []; if (title) { summary.push('
' + sanitizePreviewHtml(description) + '
'); } if (!summary.length) { return ''; } 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.'); } 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, 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 ''; }).join(''), sourceOptions: apiSources.map(function (source) { var selected = Number(source.id) === Number(getCurrentApiConfig(region).source_id) ? ' selected' : ''; return ''; }).join(''), placeholderChips: region.region_type === 'rss' ? getRssFieldList(getCurrentRssConfig(region).feed_id).map(function (field) { return '{{' + escapeHtml(field) + '}}'; }).join('') : buildLimitedPlaceholderChipMarkup(getApiFieldList(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).items_path), 'No JSON fields available.'), 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, 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 ''; }).join(''), sourceOptions: apiSources.map(function (source) { var selected = Number(source.id) === Number(apiConfig.source_id) ? ' selected' : ''; return ''; }).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.'), 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(/