Release 2.8.3
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m14s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 32s

This commit is contained in:
2026-08-17 00:08:05 +01:00
parent ea72747822
commit 7bb34f40fe
22 changed files with 291 additions and 82 deletions
+12
View File
@@ -2,6 +2,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## 2.8.3 - 2026-08-17
### Added
- Added API, RSS, Timetable, and Time / Date placeholder help panels with shared transform and date-token documentation.
- Added render-time API and RSS image placeholders with proportional sizing and preview-only bounding boxes.
### Changed
- API and RSS regions now preserve authored content when no data source is selected and remain blank when a selected source has no authored content.
- Normal WYSIWYG image insertion remains upload-backed and separate from image placeholder transforms.
## 2.8.2 - 2026-08-16 ## 2.8.2 - 2026-08-16
### Changed ### Changed
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pulse-signage-player", "name": "pulse-signage-player",
"version": "2.8.2", "version": "2.8.3",
"private": false, "private": false,
"description": "Pulse Signage player application bundle", "description": "Pulse Signage player application bundle",
"engines": { "engines": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pulse-signage-web", "name": "pulse-signage-web",
"version": "2.8.2", "version": "2.8.3",
"private": false, "private": false,
"description": "Pulse Signage web and bridge application bundle", "description": "Pulse Signage web and bridge application bundle",
"engines": { "engines": {
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "pulse-signage", "name": "pulse-signage",
"version": "2.8.2", "version": "2.8.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pulse-signage", "name": "pulse-signage",
"version": "2.8.2", "version": "2.8.3",
"dependencies": { "dependencies": {
"@sparticuz/chromium": "^149.0.0", "@sparticuz/chromium": "^149.0.0",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pulse-signage", "name": "pulse-signage",
"version": "2.8.2", "version": "2.8.3",
"private": false, "private": false,
"description": "Pulse Signage application with MySQL and media storage", "description": "Pulse Signage application with MySQL and media storage",
"engines": { "engines": {
+2 -2
View File
@@ -418,7 +418,7 @@ async function buildTemplateContent(pool, template, body, filesByField, existing
content[region.region_key] = { content[region.region_key] = {
type: 'rss', type: 'rss',
value: stripEditorOnlyMarkup(normalizeEditorMarkup(submitted === undefined ? String(current.value || '') : String(submitted || ''))), value: stripEditorOnlyMarkup(normalizeEditorMarkup(submitted === undefined ? String(current.value || '') : String(submitted || ''))),
feed_id: feedId === undefined || feedId === null || feedId === '' ? (current.feed_id || null) : Number(feedId), feed_id: feedId === undefined || feedId === null ? (current.feed_id || null) : (feedId === '' ? null : Number(feedId)),
item_number: Math.min(itemCount, Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber : 1), item_number: Math.min(itemCount, Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber : 1),
variable_name: 'item', variable_name: 'item',
font_family: style.font_family, font_family: style.font_family,
@@ -436,7 +436,7 @@ async function buildTemplateContent(pool, template, body, filesByField, existing
content[region.region_key] = { content[region.region_key] = {
type: 'api', type: 'api',
value: stripEditorOnlyMarkup(normalizeEditorMarkup(submitted === undefined ? String(current.value || '') : String(submitted || ''))), value: stripEditorOnlyMarkup(normalizeEditorMarkup(submitted === undefined ? String(current.value || '') : String(submitted || ''))),
source_id: sourceId === undefined || sourceId === null || sourceId === '' ? (current.source_id || null) : Number(sourceId), source_id: sourceId === undefined || sourceId === null ? (current.source_id || null) : (sourceId === '' ? null : Number(sourceId)),
item_number: Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber : 1, item_number: Number.isFinite(parsedItemNumber) && parsedItemNumber > 0 ? parsedItemNumber : 1,
items_path: itemsPath === undefined || itemsPath === null ? (current.items_path === undefined || current.items_path === null ? '' : String(current.items_path)) : String(itemsPath || '').trim(), items_path: itemsPath === undefined || itemsPath === null ? (current.items_path === undefined || current.items_path === null ? '' : String(current.items_path)) : String(itemsPath || '').trim(),
variable_name: 'item', variable_name: 'item',
+19 -5
View File
@@ -67,7 +67,23 @@ function substituteApiVariables(html, item) {
return ''; return '';
} }
return escapeHtml(placeholderUtils.formatPlaceholderValue(placeholderUtils.resolvePlaceholderExpression(item, expression))); var resolved = placeholderUtils.resolvePlaceholderExpression(item, expression);
if (typeof placeholderUtils.isImagePlaceholderExpression === 'function' && placeholderUtils.isImagePlaceholderExpression(expression)) {
var imageSource = placeholderUtils.formatPlaceholderValue(resolved);
if (!/^(?:https?:\/\/|\/media\/|\/assets\/|\/[^/])/i.test(imageSource)) {
return '';
}
var imageConfig = typeof placeholderUtils.getImagePlaceholderConfig === 'function' ? placeholderUtils.getImagePlaceholderConfig(expression) : {};
var hasImageBounds = imageConfig.width && imageConfig.height;
var imageStyle = hasImageBounds
? 'display:block;width:100%;height:100%;object-fit:contain;'
: 'display:block;width:auto;height:auto;' + (imageConfig.width ? 'max-width:' + imageConfig.width + 'px;' : '') + (imageConfig.height ? 'max-height:' + imageConfig.height + 'px;' : '');
var image = '<img src="' + escapeHtml(imageSource) + '" alt="" style="' + imageStyle + '" />';
return hasImageBounds
? '<span style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:' + imageConfig.width + 'px;height:' + imageConfig.height + 'px;">' + image + '</span>'
: image;
}
return escapeHtml(placeholderUtils.formatPlaceholderValue(resolved));
}); });
} }
@@ -120,10 +136,8 @@ function renderApiRegion(region, regionContent) {
var fontSize = sanitizeFontSize(regionContent.font_size || region.fontSize); var fontSize = sanitizeFontSize(regionContent.font_size || region.fontSize);
var fontColor = sanitizeTextColor(regionContent.font_color || region.fontColor); var fontColor = sanitizeTextColor(regionContent.font_color || region.fontColor);
var item = getApiItem(sourceId, itemNumber, itemsPath); var item = getApiItem(sourceId, itemNumber, itemsPath);
var body = item ? substituteApiVariables(content, item) : ''; var hasSource = String(sourceId === undefined || sourceId === null ? '' : sourceId).trim() !== '';
if (!body && item) { var body = hasSource ? (item ? substituteApiVariables(content, item) : '') : content;
body = getApiPreviewFallback(item);
}
var contentStyle = 'width:' + region.pixelWidth + 'px;height:' + region.pixelHeight + 'px;transform:scale(' + region.canvasScale + ');transform-origin:top left;' + (fontFamily ? 'font-family:' + escapeHtml(fontFamily) + ';' : '') + 'font-size:' + fontSize + 'px;color:' + escapeHtml(fontColor) + ';'; var contentStyle = 'width:' + region.pixelWidth + 'px;height:' + region.pixelHeight + 'px;transform:scale(' + region.canvasScale + ');transform-origin:top left;' + (fontFamily ? 'font-family:' + escapeHtml(fontFamily) + ';' : '') + 'font-size:' + fontSize + 'px;color:' + escapeHtml(fontColor) + ';';
if (!body) { if (!body) {
return ''; return '';
+24 -14
View File
@@ -1,6 +1,7 @@
// RSS region helpers for resolving feeds, items, and nested values. // RSS region helpers for resolving feeds, items, and nested values.
var registry = window.pulsePlayerRegionTypes; var registry = window.pulsePlayerRegionTypes;
var placeholderUtils = window.placeholderUtils || {};
function getDefaultStyle() { function getDefaultStyle() {
return { return {
@@ -65,12 +66,30 @@ function resolveRssPath(value, path) {
function substituteRssVariables(html, item) { function substituteRssVariables(html, item) {
var source = String(html || ''); var source = String(html || '');
return source.replace(/\{\{\s*([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_.]+))?\s*\}\}/g, function (_match, tokenName, tokenPath) { return source.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, function (_match, expression) {
if (!item || typeof item !== 'object') { if (!item || typeof item !== 'object') {
return ''; return '';
} }
var key = tokenPath ? tokenName + '.' + tokenPath : tokenName; if (typeof placeholderUtils.resolvePlaceholderExpression !== 'function' || typeof placeholderUtils.formatPlaceholderValue !== 'function') {
return escapeHtml(resolveRssPath(item, key || '')); return '';
}
var resolved = placeholderUtils.resolvePlaceholderExpression(item, expression);
if (typeof placeholderUtils.isImagePlaceholderExpression === 'function' && placeholderUtils.isImagePlaceholderExpression(expression)) {
var imageSource = placeholderUtils.formatPlaceholderValue(resolved);
if (!/^(?:https?:\/\/|\/media\/|\/assets\/|\/[^/])/i.test(imageSource)) {
return '';
}
var imageConfig = typeof placeholderUtils.getImagePlaceholderConfig === 'function' ? placeholderUtils.getImagePlaceholderConfig(expression) : {};
var hasImageBounds = imageConfig.width && imageConfig.height;
var imageStyle = hasImageBounds
? 'display:block;width:100%;height:100%;object-fit:contain;'
: 'display:block;width:auto;height:auto;' + (imageConfig.width ? 'max-width:' + imageConfig.width + 'px;' : '') + (imageConfig.height ? 'max-height:' + imageConfig.height + 'px;' : '');
var image = '<img src="' + escapeHtml(imageSource) + '" alt="" style="' + imageStyle + '" />';
return hasImageBounds
? '<span style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:' + imageConfig.width + 'px;height:' + imageConfig.height + 'px;">' + image + '</span>'
: image;
}
return escapeHtml(placeholderUtils.formatPlaceholderValue(resolved));
}); });
} }
@@ -83,17 +102,8 @@ function renderRssRegion(region, regionContent) {
var fontSize = sanitizeFontSize(regionContent.font_size || region.fontSize || defaultStyle.font_size); var fontSize = sanitizeFontSize(regionContent.font_size || region.fontSize || defaultStyle.font_size);
var fontColor = sanitizeTextColor(regionContent.font_color || region.fontColor || defaultStyle.font_color); var fontColor = sanitizeTextColor(regionContent.font_color || region.fontColor || defaultStyle.font_color);
var item = getRssFeedItem(feedId, itemNumber); var item = getRssFeedItem(feedId, itemNumber);
var body = item ? substituteRssVariables(content, item) : ''; var hasFeed = String(feedId === undefined || feedId === null ? '' : feedId).trim() !== '';
if (!body && item) { var body = hasFeed ? (item ? substituteRssVariables(content, item) : '') : content;
var summaryParts = [];
if (item.title) {
summaryParts.push('<h3>' + escapeHtml(item.title) + '</h3>');
}
if (item.description) {
summaryParts.push('<div>' + sanitizeRichText(item.description) + '</div>');
}
body = summaryParts.join('');
}
var contentStyle = 'width:' + region.pixelWidth + 'px;height:' + region.pixelHeight + 'px;transform:scale(' + region.canvasScale + ');transform-origin:top left;' + (fontFamily ? 'font-family:' + escapeHtml(fontFamily) + ';' : '') + 'font-size:' + fontSize + 'px;color:' + escapeHtml(fontColor) + ';'; var contentStyle = 'width:' + region.pixelWidth + 'px;height:' + region.pixelHeight + 'px;transform:scale(' + region.canvasScale + ');transform-origin:top left;' + (fontFamily ? 'font-family:' + escapeHtml(fontFamily) + ';' : '') + 'font-size:' + fontSize + 'px;color:' + escapeHtml(fontColor) + ';';
if (!body) { if (!body) {
return ''; return '';
+1 -1
View File
@@ -4,7 +4,7 @@ const path = require('path');
const PUBLIC_JS_ROOT = path.join(__dirname, '..', 'public', 'js'); const PUBLIC_JS_ROOT = path.join(__dirname, '..', 'public', 'js');
const REGION_ROOT_DIR = path.join(PUBLIC_JS_ROOT, 'regions'); const REGION_ROOT_DIR = path.join(PUBLIC_JS_ROOT, 'regions');
const REGION_TYPE_DIR = path.join(REGION_ROOT_DIR, 'type'); const REGION_TYPE_DIR = path.join(REGION_ROOT_DIR, 'type');
const REGION_CORE_SCRIPTS = ['js/shared/placeholder-utils.js', 'js/shared/placeholder-chips.js', 'js/shared/time-date-placeholders.js', 'js/regions/region-utils.js', 'js/regions/region-types.js']; const REGION_CORE_SCRIPTS = ['js/shared/placeholder-utils.js', 'js/shared/placeholder-chips.js', 'js/shared/placeholder-info.js', 'js/shared/time-date-placeholders.js', 'js/regions/region-utils.js', 'js/regions/region-types.js'];
function withAssetVersion(scriptPath, assetVersion) { function withAssetVersion(scriptPath, assetVersion) {
if (!assetVersion) { if (!assetVersion) {
+7
View File
@@ -2533,6 +2533,13 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection {
text-transform: uppercase; text-transform: uppercase;
} }
.api-region-placeholder-title-help {
font-size: 0.9rem;
font-weight: 800;
letter-spacing: normal;
text-transform: none;
}
.api-region-sample-accordion { .api-region-sample-accordion {
padding: 0.85rem 1rem 1rem; padding: 0.85rem 1rem 1rem;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
+40 -9
View File
@@ -94,7 +94,7 @@
}; };
} }
function substituteVariables(html, item) { function substituteVariables(html, item, options) {
var source = String(html || ''); var source = String(html || '');
return source.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, function (_match, expression) { return source.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, function (_match, expression) {
if (!item || typeof item !== 'object') { if (!item || typeof item !== 'object') {
@@ -105,7 +105,23 @@
return ''; return '';
} }
return escapeHtml(placeholderUtils.formatPlaceholderValue(placeholderUtils.resolvePlaceholderExpression(item, expression))); var resolved = placeholderUtils.resolvePlaceholderExpression(item, expression);
if (typeof placeholderUtils.isImagePlaceholderExpression === 'function' && placeholderUtils.isImagePlaceholderExpression(expression)) {
var imageSource = placeholderUtils.formatPlaceholderValue(resolved);
if (!/^(?:https?:\/\/|\/media\/|\/assets\/|\/[^/])/i.test(imageSource)) {
return '';
}
var imageConfig = typeof placeholderUtils.getImagePlaceholderConfig === 'function' ? placeholderUtils.getImagePlaceholderConfig(expression) : {};
var imageStyle = options && options.preview && imageConfig.width && imageConfig.height
? 'display:block;width:100%;height:100%;object-fit:contain;'
: 'display:block;width:auto;height:auto;' + (imageConfig.width ? 'max-width:' + imageConfig.width + 'px;' : '') + (imageConfig.height ? 'max-height:' + imageConfig.height + 'px;' : '');
var image = '<img src="' + escapeHtml(imageSource) + '" alt="" style="' + imageStyle + '" />';
if (options && options.preview && imageConfig.width && imageConfig.height) {
return '<span style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:' + imageConfig.width + 'px;height:' + imageConfig.height + 'px;border:1px dashed rgba(120,120,120,.75);">' + image + '</span>';
}
return image;
}
return escapeHtml(placeholderUtils.formatPlaceholderValue(resolved));
}); });
} }
@@ -136,6 +152,24 @@
return summary.join(''); return summary.join('');
} }
function renderTransformInfo(regionId) {
var offcanvasId = 'api-placeholder-info-' + regionId;
return '' +
'<button type="button" class="btn btn-link btn-sm p-0 text-decoration-none" data-bs-toggle="offcanvas" data-bs-target="#' + offcanvasId + '" aria-controls="' + offcanvasId + '">More info</button>' +
'<div class="offcanvas offcanvas-end fw-normal" tabindex="-1" id="' + offcanvasId + '" aria-labelledby="' + offcanvasId + '-label">' +
'<div class="offcanvas-header">' +
'<h2 class="offcanvas-title fs-5" id="' + offcanvasId + '-label">API placeholder transforms</h2>' +
'<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>' +
'</div>' +
'<div class="offcanvas-body">' +
'<p class="small text-body-secondary">Placeholders read values from the selected API item. Nested fields use dots.</p>' +
(window.placeholderInfo ? window.placeholderInfo.renderTextTransforms() : '') +
(window.placeholderInfo ? window.placeholderInfo.renderDateFormatTokens() : '') +
(window.placeholderInfo ? window.placeholderInfo.renderImageTransform() : '') +
'</div>' +
'</div>';
}
function normalizeRenderableValue(value) { function normalizeRenderableValue(value) {
if (value && typeof value === 'object') { if (value && typeof value === 'object') {
if (value.value !== undefined) { if (value.value !== undefined) {
@@ -170,11 +204,8 @@
var fontSize = sanitizeFontSize(regionContent.font_size || region.fontSize); var fontSize = sanitizeFontSize(regionContent.font_size || region.fontSize);
var fontColor = sanitizeTextColor(regionContent.font_color || region.fontColor); var fontColor = sanitizeTextColor(regionContent.font_color || region.fontColor);
var item = getItem(sourceId, itemNumber, sources, itemsPath); var item = getItem(sourceId, itemNumber, sources, itemsPath);
var body = item ? substituteVariables(content, item) : ''; var hasSource = String(sourceId === undefined || sourceId === null ? '' : sourceId).trim() !== '';
var body = hasSource ? (item ? substituteVariables(content, item, { preview: true }) : '') : content;
if (!body && item) {
body = getPreviewFallback(item);
}
body = String(body || '') body = String(body || '')
.replace(/<pre[^>]*class="[^"]*api-region-sample-preview[^"]*"[^>]*>[\s\S]*?<\/pre>/gi, '') .replace(/<pre[^>]*class="[^"]*api-region-sample-preview[^"]*"[^>]*>[\s\S]*?<\/pre>/gi, '')
@@ -239,7 +270,7 @@
'<input type="hidden" name="region_font_size_' + region.id + '" value="' + escapeHtml(context.fontSize || '') + '" />' + '<input type="hidden" name="region_font_size_' + region.id + '" value="' + escapeHtml(context.fontSize || '') + '" />' +
'<input type="hidden" name="region_text_' + region.id + '" value="' + escapeHtml(current.value !== undefined ? current.value : '') + '" />' + '<input type="hidden" name="region_text_' + region.id + '" value="' + escapeHtml(current.value !== undefined ? current.value : '') + '" />' +
'<div class="api-region-placeholder-section">' + '<div class="api-region-placeholder-section">' +
'<div class="api-region-placeholder-title">Available placeholders</div>' + '<div class="api-region-placeholder-title api-region-placeholder-title-help d-flex align-items-center gap-2">Available placeholders ' + renderTransformInfo(region.id) + '</div>' +
'<div class="d-flex flex-wrap gap-2" data-placeholder-chips>' + ((placeholderFields.length && window.placeholderChips && typeof window.placeholderChips.renderChips === 'function') ? window.placeholderChips.renderChips(placeholderFields) : placeholderChips || '<span class="muted slide-image-file">No JSON fields available.</span>') + '</div>' + '<div class="d-flex flex-wrap gap-2" data-placeholder-chips>' + ((placeholderFields.length && window.placeholderChips && typeof window.placeholderChips.renderChips === 'function') ? window.placeholderChips.renderChips(placeholderFields) : placeholderChips || '<span class="muted slide-image-file">No JSON fields available.</span>') + '</div>' +
'</div>' + '</div>' +
'<details class="api-region-sample-accordion" data-api-sample-data-accordion>' + '<details class="api-region-sample-accordion" data-api-sample-data-accordion>' +
@@ -290,7 +321,7 @@
return fallbackValue; return fallbackValue;
} }
}())), }())),
source_id: (card.querySelector('select[name="region_api_source_id_' + region.id + '"]') || {}).value || current.source_id, source_id: card.querySelector('select[name="region_api_source_id_' + region.id + '"]') ? card.querySelector('select[name="region_api_source_id_' + region.id + '"]').value : current.source_id,
item_number: (card.querySelector('input[name="region_api_item_number_' + region.id + '"]') || {}).value || current.item_number, item_number: (card.querySelector('input[name="region_api_item_number_' + region.id + '"]') || {}).value || current.item_number,
items_path: card.querySelector('input[name="region_api_items_path_' + region.id + '"]') ? (card.querySelector('input[name="region_api_items_path_' + region.id + '"]') || {}).value : current.items_path items_path: card.querySelector('input[name="region_api_items_path_' + region.id + '"]') ? (card.querySelector('input[name="region_api_items_path_' + region.id + '"]') || {}).value : current.items_path
} : current; } : current;
+43 -20
View File
@@ -3,6 +3,7 @@
(function () { (function () {
var registry = window.pulseRegionTypes; var registry = window.pulseRegionTypes;
var utils = window.pulseRegionUtils || {}; var utils = window.pulseRegionUtils || {};
var placeholderUtils = window.placeholderUtils || {};
var DEFAULT_STYLE = { var DEFAULT_STYLE = {
font_family: 'Arial', font_family: 'Arial',
font_size: 32, font_size: 32,
@@ -114,14 +115,32 @@
return current === undefined || current === null ? '' : current; return current === undefined || current === null ? '' : current;
} }
function substituteVariables(html, item) { function substituteVariables(html, item, options) {
var source = String(html || ''); var source = String(html || '');
return source.replace(/\{\{\s*([a-zA-Z0-9_]+)(?:\.([a-zA-Z0-9_.]+))?\s*\}\}/g, function (_match, tokenName, tokenPath) { return source.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, function (_match, expression) {
if (!item || typeof item !== 'object') { if (!item || typeof item !== 'object') {
return ''; return '';
} }
var key = tokenPath ? tokenName + '.' + tokenPath : tokenName; if (typeof placeholderUtils.resolvePlaceholderExpression !== 'function' || typeof placeholderUtils.formatPlaceholderValue !== 'function') {
return escapeHtml(resolvePath(item, key || '')); return '';
}
var resolved = placeholderUtils.resolvePlaceholderExpression(item, expression);
if (typeof placeholderUtils.isImagePlaceholderExpression === 'function' && placeholderUtils.isImagePlaceholderExpression(expression)) {
var imageSource = placeholderUtils.formatPlaceholderValue(resolved);
if (!/^(?:https?:\/\/|\/media\/|\/assets\/|\/[^/])/i.test(imageSource)) {
return '';
}
var imageConfig = typeof placeholderUtils.getImagePlaceholderConfig === 'function' ? placeholderUtils.getImagePlaceholderConfig(expression) : {};
var imageStyle = options && options.preview && imageConfig.width && imageConfig.height
? 'display:block;width:100%;height:100%;object-fit:contain;'
: 'display:block;width:auto;height:auto;' + (imageConfig.width ? 'max-width:' + imageConfig.width + 'px;' : '') + (imageConfig.height ? 'max-height:' + imageConfig.height + 'px;' : '');
var image = '<img src="' + escapeHtml(imageSource) + '" alt="" style="' + imageStyle + '" />';
if (options && options.preview && imageConfig.width && imageConfig.height) {
return '<span style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:' + imageConfig.width + 'px;height:' + imageConfig.height + 'px;border:1px dashed rgba(120,120,120,.75);">' + image + '</span>';
}
return image;
}
return escapeHtml(placeholderUtils.formatPlaceholderValue(resolved));
}); });
} }
@@ -186,18 +205,8 @@
var fontSize = sanitizeFontSize(regionContent.font_size || region.fontSize || defaultStyle.font_size); var fontSize = sanitizeFontSize(regionContent.font_size || region.fontSize || defaultStyle.font_size);
var fontColor = sanitizeTextColor(regionContent.font_color || region.fontColor || defaultStyle.font_color); var fontColor = sanitizeTextColor(regionContent.font_color || region.fontColor || defaultStyle.font_color);
var item = getItem(feedId, itemNumber, feeds); var item = getItem(feedId, itemNumber, feeds);
var body = item ? substituteVariables(content, item) : ''; var hasFeed = String(feedId === undefined || feedId === null ? '' : feedId).trim() !== '';
var body = hasFeed ? (item ? substituteVariables(content, item, { preview: true }) : '') : content;
if (!body && item) {
var summaryParts = [];
if (item.title) {
summaryParts.push('<h3>' + escapeHtml(item.title) + '</h3>');
}
if (item.description) {
summaryParts.push('<div>' + sanitizePreviewHtml(item.description) + '</div>');
}
body = summaryParts.join('');
}
if (!body) { if (!body) {
return ''; return '';
@@ -206,6 +215,21 @@
return renderedBody ? '<div class="template-region rss" style="width:100%;height:100%;overflow:hidden;font-family:' + escapeHtml(fontFamily) + ';font-size:' + fontSize + 'px;color:' + escapeHtml(fontColor) + ';"><div class="template-region-text-scale" style="width:100%;height:100%;overflow:hidden;">' + renderedBody + '</div></div>' : ''; return renderedBody ? '<div class="template-region rss" style="width:100%;height:100%;overflow:hidden;font-family:' + escapeHtml(fontFamily) + ';font-size:' + fontSize + 'px;color:' + escapeHtml(fontColor) + ';"><div class="template-region-text-scale" style="width:100%;height:100%;overflow:hidden;">' + renderedBody + '</div></div>' : '';
} }
function renderPlaceholderInfo(regionId) {
if (!window.placeholderInfo) {
return '';
}
return window.placeholderInfo.render(regionId, 'RSS placeholder transforms',
'<p class="small text-body-secondary">Placeholders read values from the selected RSS entry. Use the available field chips as the starting point for your message.</p>' +
'<h3 class="fs-6 mt-4 fw-normal">Placeholder paths</h3>' +
'<p class="small">Nested values use dots. Missing values render as empty text.</p>' +
(window.placeholderInfo ? window.placeholderInfo.renderTextTransforms() : '') +
'<h3 class="fs-6 mt-4 fw-normal">Date formatting</h3>' +
'<p class="small">Use <code>format("MMM D, YYYY")</code> with date fields. Use <code>tz()</code> for a short timezone name and <code>tz_long()</code> for the full timezone name.</p>' +
(window.placeholderInfo ? window.placeholderInfo.renderDateFormatTokens() : '') +
(window.placeholderInfo ? window.placeholderInfo.renderImageTransform() : ''));
}
function renderEditorCard(context) { function renderEditorCard(context) {
var region = context.region; var region = context.region;
var current = context.current || {}; var current = context.current || {};
@@ -221,7 +245,7 @@
'<span class="chip">RSS</span>' + '<span class="chip">RSS</span>' +
'</div>' + '</div>' +
'</div>' + '</div>' +
'<div class="card-body p-3 d-grid gap-3 pb-0">' + '<div class="card-body p-3 d-grid gap-3">' +
'<div class="d-flex justify-content-end">' + '<div class="d-flex justify-content-end">' +
'<div class="btn-group btn-group-sm template-editor-size-controls" role="group" aria-label="Editor size controls">' + '<div class="btn-group btn-group-sm template-editor-size-controls" role="group" aria-label="Editor size controls">' +
'<button type="button" class="btn btn-outline-secondary" data-editor-size-action="decrease" aria-label="Decrease editor size">-</button>' + '<button type="button" class="btn btn-outline-secondary" data-editor-size-action="decrease" aria-label="Decrease editor size">-</button>' +
@@ -247,9 +271,8 @@
'<input type="hidden" name="region_font_size_' + region.id + '" value="' + escapeHtml(context.fontSize || '') + '" />' + '<input type="hidden" name="region_font_size_' + region.id + '" value="' + escapeHtml(context.fontSize || '') + '" />' +
'<input type="hidden" name="region_text_' + region.id + '" value="' + escapeHtml(current.value !== undefined ? current.value : '') + '" />' + '<input type="hidden" name="region_text_' + region.id + '" value="' + escapeHtml(current.value !== undefined ? current.value : '') + '" />' +
'<div class="api-region-placeholder-section">' + '<div class="api-region-placeholder-section">' +
'<div class="api-region-placeholder-title">Available placeholders</div>' + '<div class="api-region-placeholder-title api-region-placeholder-title-help d-flex align-items-center gap-2">Available placeholders ' + renderPlaceholderInfo(region.id) + '</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 class="d-flex flex-wrap gap-2" data-placeholder-chips>' + (placeholderChips || '<span class="muted slide-image-file">No RSS fields available.</span>') + '</div>' +
'<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>, or <code>{{publishedAt.format("MMM D, YYYY")}}</code>.</div>' +
'</div>' + '</div>' +
'</div>' + '</div>' +
'</div>'; '</div>';
@@ -291,7 +314,7 @@
return fallbackValue; return fallbackValue;
} }
}())), }())),
feed_id: (card.querySelector('select[name="region_rss_feed_id_' + region.id + '"]') || {}).value || current.feed_id, feed_id: card.querySelector('select[name="region_rss_feed_id_' + region.id + '"]') ? card.querySelector('select[name="region_rss_feed_id_' + region.id + '"]').value : current.feed_id,
item_number: (card.querySelector('input[name="region_rss_item_number_' + region.id + '"]') || {}).value || current.item_number item_number: (card.querySelector('input[name="region_rss_item_number_' + region.id + '"]') || {}).value || current.item_number
} : current; } : current;
+3 -3
View File
@@ -33,9 +33,9 @@
} }
var style = styleOrContext && styleOrContext.style ? styleOrContext.style : styleOrContext || {}; var style = styleOrContext && styleOrContext.style ? styleOrContext.style : styleOrContext || {};
var fontFamily = style && style.font_family ? 'font-family:' + escapeHtml(style.font_family) + ';' : ''; var fontFamily = 'font-family:' + escapeHtml(style && style.font_family || DEFAULT_STYLE.font_family) + ';';
var color = style && style.font_color ? 'color:' + escapeHtml(style.font_color) + ';' : ''; var color = 'color:' + escapeHtml(style && style.font_color || DEFAULT_STYLE.font_color) + ';';
var fontSize = style && style.font_size ? 'font-size:' + Math.max(1, Math.round(Number(style.font_size))) + 'px;' : ''; var fontSize = 'font-size:' + Math.max(1, Math.round(Number(style && style.font_size || DEFAULT_STYLE.font_size))) + 'px;';
var width = Math.max(1, Math.round(Number(region && region.width ? region.width : 0) || 1)); var width = Math.max(1, Math.round(Number(region && region.width ? region.width : 0) || 1));
var height = Math.max(1, Math.round(Number(region && region.height ? region.height : 0) || 1)); var height = Math.max(1, Math.round(Number(region && region.height ? region.height : 0) || 1));
var wrapperStyle = 'width:' + width + 'px;height:' + height + 'px;overflow:hidden;line-height:1.5;' + fontFamily + fontSize + color; var wrapperStyle = 'width:' + width + 'px;height:' + height + 'px;overflow:hidden;line-height:1.5;' + fontFamily + fontSize + color;
+14 -2
View File
@@ -286,6 +286,19 @@
return renderTimeDatePlaceholderChips(); return renderTimeDatePlaceholderChips();
} }
function renderPlaceholderInfo(regionId) {
if (!window.placeholderInfo) {
return '';
}
var placeholders = window.timeDatePlaceholders && typeof window.timeDatePlaceholders.getTokens === 'function'
? window.timeDatePlaceholders.getTokens()
: [];
var table = '<div class="table-responsive"><table class="table table-sm align-middle mb-0"><thead><tr><th>Placeholder</th><th>Output</th><th>Description</th></tr></thead><tbody>' + placeholders.map(function (item) {
return '<tr><td><code>{{' + escapeHtml(item.token) + '}}</code></td><td>' + escapeHtml(item.output) + '</td><td>' + escapeHtml(item.description) + '</td></tr>';
}).join('') + '</tbody></table></div>';
return window.placeholderInfo.render(regionId, 'Time and date placeholders', table);
}
function renderEditorCard(context) { function renderEditorCard(context) {
var region = context.region; var region = context.region;
var current = context.current || {}; var current = context.current || {};
@@ -317,9 +330,8 @@
'<datalist id="region_time_date_timezones_' + region.id + '">' + buildTimezoneOptionsMarkup(timeZone) + '</datalist>' + '<datalist id="region_time_date_timezones_' + region.id + '">' + buildTimezoneOptionsMarkup(timeZone) + '</datalist>' +
'</div>' + '</div>' +
'<div>' + '<div>' +
'<div class="api-region-placeholder-title mb-2">Available placeholders</div>' + '<div class="api-region-placeholder-title api-region-placeholder-title-help d-flex align-items-center gap-2 mb-2">Available placeholders ' + renderPlaceholderInfo(region.id) + '</div>' +
'<div class="d-flex flex-wrap gap-2">' + renderPlaceholderChips() + '</div>' + '<div class="d-flex flex-wrap gap-2">' + renderPlaceholderChips() + '</div>' +
'<div class="text-body-secondary small mt-1">Placeholder values support transforms, for example <code>{{title.upper()}}</code>, <code>{{title.title()}}</code>, or <code>{{title.lower()}}</code>.</div>' +
'</div>' + '</div>' +
'</div>' + '</div>' +
'</div>'; '</div>';
+16 -7
View File
@@ -240,6 +240,21 @@
}; };
} }
function renderPlaceholderInfo(regionId) {
if (!window.placeholderInfo) {
return '';
}
var body = '<p class="small text-body-secondary">Placeholders read fields from the selected timetable entries. The selected group and display mode determine which entries and fields are available.</p>' +
'<h3 class="fs-6 mt-4 fw-normal">Placeholder paths</h3>' +
'<p class="small">Use the available field chips. Nested values use dots, and missing values render as empty text.</p>' +
window.placeholderInfo.renderTextTransforms() +
'<h3 class="fs-6 mt-4 fw-normal">Date and time formatting</h3>' +
'<p class="small">Use <code>format("MMM D, YYYY h:mm A")</code> with date fields. Use <code>tz()</code> for a short timezone name and <code>tz_long()</code> for the full timezone name.</p>' +
window.placeholderInfo.renderDateFormatTokens() +
'';
return window.placeholderInfo.render(regionId, 'Timetable placeholder transforms', body);
}
function renderEditorCard(context) { function renderEditorCard(context) {
var region = context.region; var region = context.region;
var current = context.current || {}; var current = context.current || {};
@@ -298,14 +313,8 @@
'</div>' + '</div>' +
'<div class="text-body-secondary small">Use the selected group and display mode to choose which timetable entry fields are available.</div>' + '<div class="text-body-secondary small">Use the selected group and display mode to choose which timetable entry fields are available.</div>' +
'<div class="api-region-placeholder-section schedule-placeholder-section" data-schedule-placeholder-chips>' + '<div class="api-region-placeholder-section schedule-placeholder-section" data-schedule-placeholder-chips>' +
'<div class="api-region-placeholder-title">Available placeholders</div>' + '<div class="api-region-placeholder-title api-region-placeholder-title-help d-flex align-items-center gap-2">Available placeholders ' + renderPlaceholderInfo(region.id) + '</div>' +
'<div class="d-flex flex-wrap gap-2">' + renderSchedulePlaceholderChips(currentGroup, currentEntries) + '</div>' + '<div class="d-flex flex-wrap gap-2">' + renderSchedulePlaceholderChips(currentGroup, currentEntries) + '</div>' +
'<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>{{start.format("MMM D, YYYY h:mm A")}}</code>, <code>{{start.tz()}}</code> for the short zone name, or <code>{{start.tz_long()}}</code> for the full IANA zone name.</div>' +
'<details class="mt-2">' +
'<summary class="small text-body-secondary">Supported date format tokens</summary>' +
'<div class="mt-2">' + renderScheduleFormatTokenTable() + '</div>' +
'<div class="text-body-secondary small mt-1">Use these tokens inside <code>.format(...)</code>; for example <code>{{start.format("MMM D, YYYY h:mm A")}}</code>.</div>' +
'</details>' +
'</div>' + '</div>' +
'</div>' + '</div>' +
'</div>' + '</div>' +
@@ -0,0 +1,67 @@
// Shared offcanvas renderer for region placeholder documentation.
(function () {
var root = window;
function escapeHtml(value) {
return String(value === undefined || value === null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function render(regionId, title, body) {
var offcanvasId = String(regionId || 'region') + '-placeholder-info';
return '' +
'<button type="button" class="btn btn-link btn-sm p-0 text-decoration-none" data-bs-toggle="offcanvas" data-bs-target="#' + offcanvasId + '" aria-controls="' + offcanvasId + '">More info</button>' +
'<div class="offcanvas offcanvas-end fw-normal" tabindex="-1" id="' + offcanvasId + '" aria-labelledby="' + offcanvasId + '-label">' +
'<div class="offcanvas-header">' +
'<h2 class="offcanvas-title fs-5" id="' + offcanvasId + '-label">' + escapeHtml(title) + '</h2>' +
'<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>' +
'</div>' +
'<div class="offcanvas-body">' + body + '</div>' +
'</div>';
}
function renderTextTransforms() {
return '<h3 class="fs-6 mt-4 fw-normal">Text transforms</h3>' +
'<dl class="small fw-normal">' +
'<dt class="fw-normal"><code>upper()</code></dt><dd>Converts text to uppercase.</dd>' +
'<dt class="fw-normal"><code>lower()</code></dt><dd>Converts text to lowercase.</dd>' +
'<dt class="fw-normal"><code>title()</code></dt><dd>Capitalizes each word.</dd>' +
'<dt class="fw-normal"><code>tz()</code></dt><dd>Returns the short timezone name for a date.</dd>' +
'<dt class="fw-normal"><code>tz_long()</code></dt><dd>Returns the full timezone name for a date.</dd>' +
'</dl>';
}
function renderDateFormatTokens() {
return '<h3 class="fs-6 mt-4 fw-normal">Date format tokens</h3>' +
'<p class="small">Use the <code>format("...")</code> transform with these tokens. Text inside square brackets is treated as a literal.</p>' +
'<div class="table-responsive small"><table class="table table-sm align-middle mb-0"><thead><tr><th>Token</th><th>Meaning</th><th>Example</th></tr></thead><tbody>' +
'<tr><td><code>YYYY</code></td><td>Four-digit year</td><td><code>2026</code></td></tr><tr><td><code>YY</code></td><td>Two-digit year</td><td><code>26</code></td></tr>' +
'<tr><td><code>MMMM</code></td><td>Full month name</td><td><code>August</code></td></tr><tr><td><code>MMM</code></td><td>Short month name</td><td><code>Aug</code></td></tr>' +
'<tr><td><code>MM</code></td><td>Two-digit month</td><td><code>08</code></td></tr><tr><td><code>M</code></td><td>Month number</td><td><code>8</code></td></tr>' +
'<tr><td><code>DD</code></td><td>Two-digit day</td><td><code>16</code></td></tr><tr><td><code>D</code></td><td>Day number</td><td><code>16</code></td></tr>' +
'<tr><td><code>dddd</code></td><td>Full weekday name</td><td><code>Sunday</code></td></tr><tr><td><code>ddd</code></td><td>Short weekday name</td><td><code>Sun</code></td></tr>' +
'<tr><td><code>HH</code> / <code>H</code></td><td>24-hour time</td><td><code>19</code> / <code>7</code></td></tr><tr><td><code>hh</code> / <code>h</code></td><td>12-hour time</td><td><code>07</code> / <code>7</code></td></tr>' +
'<tr><td><code>mm</code> / <code>m</code></td><td>Minutes</td><td><code>05</code> / <code>5</code></td></tr><tr><td><code>ss</code> / <code>s</code></td><td>Seconds</td><td><code>09</code> / <code>9</code></td></tr>' +
'<tr><td><code>A</code> / <code>a</code></td><td>AM or PM</td><td><code>PM</code> / <code>pm</code></td></tr>' +
'</tbody></table></div>';
}
function renderImageTransform() {
return '<h3 class="fs-6 mt-4 fw-normal">Image transform</h3>' +
'<p class="small">Use <code>image(width, height)</code> to render an image URL inside a proportional bounding box.</p>' +
'<ul class="small"><li>The preview shows the width and height boundary.</li><li>The player has no boundary.</li><li>The image keeps its original aspect ratio.</li><li>Both dimensions are required for a bounding box.</li></ul>';
}
root.placeholderInfo = {
escapeHtml: escapeHtml,
render: render,
renderTextTransforms: renderTextTransforms,
renderDateFormatTokens: renderDateFormatTokens,
renderImageTransform: renderImageTransform
};
}());
@@ -298,6 +298,27 @@
return resolved; return resolved;
} }
function isImagePlaceholderExpression(expression) {
var parsed = parsePlaceholderExpression(expression);
return parsed.transforms.some(function (transform) {
return transform && transform.name === 'image';
});
}
function getImagePlaceholderConfig(expression) {
var parsed = parsePlaceholderExpression(expression);
var imageTransform = parsed.transforms.find(function (transform) {
return transform && transform.name === 'image';
});
var args = imageTransform && Array.isArray(imageTransform.args) ? imageTransform.args : [];
var width = Number(args[0]);
var height = Number(args[1]);
return {
width: Number.isFinite(width) && width > 0 ? Math.round(width) : 0,
height: Number.isFinite(height) && height > 0 ? Math.round(height) : 0
};
}
function formatPlaceholderValue(value) { function formatPlaceholderValue(value) {
if (value === undefined || value === null) { if (value === undefined || value === null) {
return ''; return '';
@@ -349,6 +370,8 @@
resolvePath: resolvePath, resolvePath: resolvePath,
parsePlaceholderExpression: parsePlaceholderExpression, parsePlaceholderExpression: parsePlaceholderExpression,
resolvePlaceholderExpression: resolvePlaceholderExpression, resolvePlaceholderExpression: resolvePlaceholderExpression,
isImagePlaceholderExpression: isImagePlaceholderExpression,
getImagePlaceholderConfig: getImagePlaceholderConfig,
formatPlaceholderValue: formatPlaceholderValue, formatPlaceholderValue: formatPlaceholderValue,
collectPlaceholderFieldPaths: collectPlaceholderFieldPaths collectPlaceholderFieldPaths: collectPlaceholderFieldPaths
}; };
@@ -44,10 +44,6 @@ export function createSlideFormEditorController(options) {
return String(value || ''); return String(value || '');
} }
function normalizeEditorMarkup(value) {
return String(value === undefined || value === null ? '' : value).trim();
}
function isEmptyRichTextValue(value) { function isEmptyRichTextValue(value) {
var raw = String(value === undefined || value === null ? '' : value).trim(); var raw = String(value === undefined || value === null ? '' : value).trim();
if (!raw) { if (!raw) {
@@ -575,7 +571,7 @@ export function createSlideFormEditorController(options) {
return Promise.resolve(null); return Promise.resolve(null);
} }
source.value = normalizeEditorMarkup(normalizeEditorData(source && source.value !== undefined && source.value !== null ? source.value : (hidden ? hidden.value : ''))); source.value = normalizeEditorData(source && source.value !== undefined && source.value !== null ? source.value : (hidden ? hidden.value : ''));
if (!source.id) { if (!source.id) {
source.id = 'slide-editor-region-' + regionId; source.id = 'slide-editor-region-' + regionId;
@@ -198,9 +198,9 @@ export function createSlideFormPreviewHelpers(options) {
return module.renderPreview(region, value, style, scale); return module.renderPreview(region, value, style, scale);
} }
var fontFamily = style.font_family ? 'font-family:' + escapeHtml(style.font_family) + ';' : ''; var fontFamily = 'font-family:' + escapeHtml(style.font_family || 'Arial') + ';';
var color = style.font_color ? 'color:' + escapeHtml(style.font_color) + ';' : ''; var color = 'color:' + escapeHtml(style.font_color || '#000000') + ';';
var fontSize = style.font_size ? 'font-size:' + Math.max(1, Math.round(Number(style.font_size))) + 'px;' : ''; var fontSize = 'font-size:' + Math.max(1, Math.round(Number(style.font_size || 32))) + 'px;';
var width = Math.max(1, Math.round(Number(region && region.width ? region.width : 0) || 1)); var width = Math.max(1, Math.round(Number(region && region.width ? region.width : 0) || 1));
var height = Math.max(1, Math.round(Number(region && region.height ? region.height : 0) || 1)); var height = Math.max(1, Math.round(Number(region && region.height ? region.height : 0) || 1));
var wrapperStyle = 'width:' + width + 'px;height:' + height + 'px;overflow:hidden;' + fontFamily + fontSize + color; var wrapperStyle = 'width:' + width + 'px;height:' + height + 'px;overflow:hidden;' + fontFamily + fontSize + color;
@@ -123,7 +123,7 @@ export function createSlideFormRegionHelpers(options) {
return fields; return fields;
} }
function buildLimitedPlaceholderChipMarkup(fieldList, emptyLabel) { function buildLimitedPlaceholderChipMarkup(fieldList, emptyLabel, includeTransformHint) {
var fields = Array.isArray(fieldList) ? fieldList : []; var fields = Array.isArray(fieldList) ? fieldList : [];
var visibleFields = fields.slice(0, 8); var visibleFields = fields.slice(0, 8);
var overflowFields = fields.slice(visibleFields.length); var overflowFields = fields.slice(visibleFields.length);
@@ -137,7 +137,7 @@ export function createSlideFormRegionHelpers(options) {
? placeholderChips.renderChip(field) ? placeholderChips.renderChip(field)
: '<span class="chip">{{' + escapeHtml(field) + '}}</span>'; : '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
}).join(''); }).join('');
var transformHint = '<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>'; 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) { if (!overflowFields.length) {
return visibleMarkup + transformHint; return visibleMarkup + transformHint;
@@ -417,7 +417,7 @@ export function createSlideFormRegionHelpers(options) {
return; return;
} }
container.innerHTML = buildLimitedPlaceholderChipMarkup(getApiFieldList(sourceId, itemsPathOverride), 'No JSON fields available.'); container.innerHTML = buildLimitedPlaceholderChipMarkup(getApiFieldList(sourceId, itemsPathOverride), 'No JSON fields available.', false);
} }
function updateApiSampleDataPanel(regionId, sourceId, itemNumber, itemsPathOverride) { function updateApiSampleDataPanel(regionId, sourceId, itemNumber, itemsPathOverride) {
@@ -561,7 +561,7 @@ export function createSlideFormRegionHelpers(options) {
? getRssFieldList(getCurrentRssConfig(region).feed_id).map(function (field) { ? getRssFieldList(getCurrentRssConfig(region).feed_id).map(function (field) {
return '<span class="chip">{{' + escapeHtml(field) + '}}</span>'; return '<span class="chip">{{' + escapeHtml(field) + '}}</span>';
}).join('') }).join('')
: buildLimitedPlaceholderChipMarkup(getApiFieldList(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).items_path), 'No JSON fields available.'), : buildLimitedPlaceholderChipMarkup(getApiFieldList(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).items_path), 'No JSON fields available.', false),
placeholderFields: region.region_type === 'api' placeholderFields: region.region_type === 'api'
? getApiFieldList(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).items_path) ? getApiFieldList(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).items_path)
: region.region_type === 'rss' : region.region_type === 'rss'
@@ -602,7 +602,7 @@ export function createSlideFormRegionHelpers(options) {
}).join(''), }).join(''),
placeholderChips: region.region_type === 'rss' placeholderChips: region.region_type === 'rss'
? buildLimitedPlaceholderChipMarkup(getRssFieldList(rssConfig.feed_id), 'No RSS fields available.') ? buildLimitedPlaceholderChipMarkup(getRssFieldList(rssConfig.feed_id), 'No RSS fields available.')
: buildLimitedPlaceholderChipMarkup(getApiFieldList(apiConfig.source_id, apiItemsPath), 'No JSON fields available.'), : buildLimitedPlaceholderChipMarkup(getApiFieldList(apiConfig.source_id, apiItemsPath), 'No JSON fields available.', false),
placeholderFields: region.region_type === 'api' placeholderFields: region.region_type === 'api'
? getApiFieldList(apiConfig.source_id, apiItemsPath) ? getApiFieldList(apiConfig.source_id, apiItemsPath)
: region.region_type === 'rss' : region.region_type === 'rss'
+1 -1
View File
@@ -654,7 +654,7 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
var content = typeModule && typeof typeModule.renderPreview === 'function' var content = typeModule && typeof typeModule.renderPreview === 'function'
? typeModule.renderPreview(region, previewContext, previewContext) ? typeModule.renderPreview(region, previewContext, previewContext)
: renderPreviewTextRegion(region, previewContext.value, previewContext.style, scale); : renderPreviewTextRegion(region, previewContext.value, previewContext.style, scale);
if (typeModule && typeof typeModule.renderPreview === 'function') { if (typeModule && typeof typeModule.renderPreview === 'function' && region.region_type !== 'api' && region.region_type !== 'rss') {
content = content || renderPreviewTextRegion(region, previewContext.value, previewContext.style, scale); content = content || renderPreviewTextRegion(region, previewContext.value, previewContext.style, scale);
} }
var selected = region.region_type === 'image' var selected = region.region_type === 'image'
+5
View File
@@ -23,6 +23,11 @@ test('slide editor enables server-backed image uploads', () => {
assert.ok(slideFormEditorSource.includes('remove_script_host: false')); assert.ok(slideFormEditorSource.includes('remove_script_host: false'));
}); });
test('slide editor keeps normal TinyMCE image insertion unchanged', () => {
assert.doesNotMatch(slideFormEditorSource, /bindPlaceholderImageSupport|placeholderFallbackSource/);
assert.ok(slideFormEditorSource.includes('images_upload_handler: uploadEditorImage'));
});
test('slide editor inserts tables with zero padding and spacing by default', () => { test('slide editor inserts tables with zero padding and spacing by default', () => {
assert.ok(slideFormEditorSource.includes("table_default_attributes: {")); assert.ok(slideFormEditorSource.includes("table_default_attributes: {"));
assert.ok(slideFormEditorSource.includes("cellpadding: '0'")); assert.ok(slideFormEditorSource.includes("cellpadding: '0'"));