diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cfc51e..3a3936f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. +## 2.7.2 - 2026-08-14 + +### Fixed + +- HTML and webpage region previews now normalize object-shaped content before rendering, so the player, thumbnails, and popup preview show the intended iframe content instead of leaking raw objects. +- HTML and webpage preview iframes now size explicitly to the full region bounds in the player, thumbnails, and popup preview. + ## 2.7.1 - 2026-08-14 ### Changed diff --git a/build/package.player.json b/build/package.player.json index 3d9e2a6..b23fefb 100644 --- a/build/package.player.json +++ b/build/package.player.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-player", - "version": "2.7.1", + "version": "2.7.2", "private": false, "description": "Pulse Signage player application bundle", "main": "src/common.js", diff --git a/build/package.web.json b/build/package.web.json index a1f8706..7499c24 100644 --- a/build/package.web.json +++ b/build/package.web.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-web", - "version": "2.7.1", + "version": "2.7.2", "private": false, "description": "Pulse Signage web and bridge application bundle", "main": "src/common.js", diff --git a/package.json b/package.json index 2d9c865..4f4cc2e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.7.1", + "version": "2.7.2", "private": false, "description": "Pulse Signage application with MySQL and media storage", "repository": { diff --git a/src/player/regions/html.js b/src/player/regions/html.js index 3b29619..c40c4ba 100644 --- a/src/player/regions/html.js +++ b/src/player/regions/html.js @@ -2,16 +2,49 @@ var registry = window.pulsePlayerRegionTypes; +function normalizeRenderableValue(value) { + if (value && typeof value === 'object') { + if (value.value !== undefined) { + return normalizeRenderableValue(value.value); + } + if (value.text !== undefined) { + return normalizeRenderableValue(value.text); + } + if (value.html !== undefined) { + return normalizeRenderableValue(value.html); + } + if (value.content !== undefined) { + return normalizeRenderableValue(value.content); + } + return ''; + } + + return String(value === undefined || value === null ? '' : value); +} + +function buildHtmlDocument(html) { + var raw = String(html || '').trim(); + if (!raw) { + return ''; + } + + if (/^' + raw + ''; +} + function renderHtmlRegionContent(value) { - var html = String(value || '').trim(); + var html = normalizeRenderableValue(value).trim(); if (!html) { return ''; } - return ''; + return ''; } function renderHtmlRegion(region, regionContent) { - return '
' + renderHtmlRegionContent(regionContent.value || '') + '
'; + return '
' + renderHtmlRegionContent(regionContent && regionContent.value !== undefined ? regionContent.value : '') + '
'; } registry.register('html', { diff --git a/src/player/regions/webpage.js b/src/player/regions/webpage.js index 99e4b4b..dbbccad 100644 --- a/src/player/regions/webpage.js +++ b/src/player/regions/webpage.js @@ -2,12 +2,38 @@ var registry = window.pulsePlayerRegionTypes; +function normalizeRenderableValue(value) { + if (value && typeof value === 'object') { + if (value.value !== undefined) { + return normalizeRenderableValue(value.value); + } + if (value.text !== undefined) { + return normalizeRenderableValue(value.text); + } + if (value.url !== undefined) { + return normalizeRenderableValue(value.url); + } + if (value.href !== undefined) { + return normalizeRenderableValue(value.href); + } + if (value.src !== undefined) { + return normalizeRenderableValue(value.src); + } + if (value.content !== undefined) { + return normalizeRenderableValue(value.content); + } + return ''; + } + + return String(value === undefined || value === null ? '' : value); +} + function renderWebpageRegion(region, regionContent) { - var url = String(regionContent.value || '').trim(); + var url = normalizeRenderableValue(regionContent && regionContent.value !== undefined ? regionContent.value : '').trim(); if (!url) { return ''; } - return '
'; + return '
'; } registry.register('webpage', { diff --git a/src/player/render-helpers.js b/src/player/render-helpers.js index d8d504d..4eb6dde 100644 --- a/src/player/render-helpers.js +++ b/src/player/render-helpers.js @@ -279,12 +279,45 @@ function renderEditorJsContent(value) { return wrapRichTextParagraph(sanitizeRichText(raw)); } +function buildHtmlDocument(html) { + const raw = String(html || '').trim(); + if (!raw) { + return ''; + } + + if (/^' + raw + ''; +} + function renderHtmlRegionContent(value) { - const html = String(value || '').trim(); + const html = normalizeRenderableValue(value).trim(); if (!html) { return '
HTML
'; } - return ''; + return ''; +} + +function normalizeRenderableValue(value) { + if (value && typeof value === 'object') { + if (value.value !== undefined) { + return normalizeRenderableValue(value.value); + } + if (value.text !== undefined) { + return normalizeRenderableValue(value.text); + } + if (value.html !== undefined) { + return normalizeRenderableValue(value.html); + } + if (value.content !== undefined) { + return normalizeRenderableValue(value.content); + } + return ''; + } + + return String(value === undefined || value === null ? '' : value); } function fitCanvasSize(canvasWidth, canvasHeight, maxWidth, maxHeight) { diff --git a/src/web/lib/media/slide-thumbnail-preview.js b/src/web/lib/media/slide-thumbnail-preview.js index 1062cdb..9a103d4 100644 --- a/src/web/lib/media/slide-thumbnail-preview.js +++ b/src/web/lib/media/slide-thumbnail-preview.js @@ -30,6 +30,35 @@ function resolveAssetUrl(baseUrl, value) { return normalizedBaseUrl + '/' + raw.replace(/^\/+/, ''); } +function normalizeRenderableValue(value) { + if (value && typeof value === 'object') { + if (value.value !== undefined) { + return normalizeRenderableValue(value.value); + } + if (value.text !== undefined) { + return normalizeRenderableValue(value.text); + } + if (value.html !== undefined) { + return normalizeRenderableValue(value.html); + } + if (value.url !== undefined) { + return normalizeRenderableValue(value.url); + } + if (value.href !== undefined) { + return normalizeRenderableValue(value.href); + } + if (value.src !== undefined) { + return normalizeRenderableValue(value.src); + } + if (value.content !== undefined) { + return normalizeRenderableValue(value.content); + } + return ''; + } + + return String(value === undefined || value === null ? '' : value); +} + function getThumbnailCanvasSize(slide) { const template = slide && slide.template ? slide.template : null; return { @@ -79,7 +108,7 @@ function buildTextRegionMarkup(region, regionContent) { function buildRegionInnerHtml(region, regionContent, baseUrl) { const regionType = String(regionContent.type || region.region_type || 'text').trim().toLowerCase(); - const rawValue = regionContent && regionContent.value !== undefined ? regionContent.value : ''; + const rawValue = normalizeRenderableValue(regionContent && regionContent.value !== undefined ? regionContent.value : ''); if (regionType === 'image') { const src = resolveAssetUrl(baseUrl, rawValue); @@ -98,7 +127,7 @@ function buildRegionInnerHtml(region, regionContent, baseUrl) { if (regionType === 'webpage') { const src = resolveAssetUrl(baseUrl, rawValue); return src - ? '
' + ? '
' : ''; } @@ -114,7 +143,7 @@ function buildRegionInnerHtml(region, regionContent, baseUrl) { if (regionType === 'html') { const html = String(rawValue || '').trim(); return html - ? '
' + ? '
' : ''; } diff --git a/src/web/lib/media/slide-thumbnails.js b/src/web/lib/media/slide-thumbnails.js index a36cb3e..a478e65 100644 --- a/src/web/lib/media/slide-thumbnails.js +++ b/src/web/lib/media/slide-thumbnails.js @@ -52,6 +52,35 @@ function resolveAssetUrl(baseUrl, value) { return normalizedBaseUrl + '/' + raw.replace(/^\/+/, ''); } +function normalizeRenderableValue(value) { + if (value && typeof value === 'object') { + if (value.value !== undefined) { + return normalizeRenderableValue(value.value); + } + if (value.text !== undefined) { + return normalizeRenderableValue(value.text); + } + if (value.html !== undefined) { + return normalizeRenderableValue(value.html); + } + if (value.url !== undefined) { + return normalizeRenderableValue(value.url); + } + if (value.href !== undefined) { + return normalizeRenderableValue(value.href); + } + if (value.src !== undefined) { + return normalizeRenderableValue(value.src); + } + if (value.content !== undefined) { + return normalizeRenderableValue(value.content); + } + return ''; + } + + return String(value === undefined || value === null ? '' : value); +} + function getCanvasSize(slide) { const template = slide && slide.template ? slide.template : null; return { @@ -109,7 +138,7 @@ function buildTextRegionMarkup(region, regionContent) { function buildRegionInnerHtml(region, regionContent, baseUrl) { const regionType = String(regionContent.type || region.region_type || 'text').trim().toLowerCase(); - const rawValue = regionContent && regionContent.value !== undefined ? regionContent.value : ''; + const rawValue = normalizeRenderableValue(regionContent && regionContent.value !== undefined ? regionContent.value : ''); if (regionType === 'image') { const src = resolveAssetUrl(baseUrl, rawValue); @@ -128,7 +157,7 @@ function buildRegionInnerHtml(region, regionContent, baseUrl) { if (regionType === 'webpage') { const src = resolveAssetUrl(baseUrl, rawValue); return src - ? '
' + ? '
' : ''; } @@ -144,7 +173,7 @@ function buildRegionInnerHtml(region, regionContent, baseUrl) { if (regionType === 'html') { const html = String(rawValue || '').trim(); return html - ? '
' + ? '
' : ''; } diff --git a/src/web/public/js/regions/type/html.js b/src/web/public/js/regions/type/html.js index 4361218..1a5c6da 100644 --- a/src/web/public/js/regions/type/html.js +++ b/src/web/public/js/regions/type/html.js @@ -8,13 +8,49 @@ return utils.escapeHtml ? utils.escapeHtml(value) : String(value === undefined || value === null ? '' : value); } - function renderPreview(value) { - var content = value && typeof value === 'object' && value.value !== undefined ? value : null; - var html = String(content ? content.value : value || '').trim(); + function normalizePreviewValue(value) { + if (value && typeof value === 'object') { + if (value.value !== undefined) { + return normalizePreviewValue(value.value); + } + if (value.text !== undefined) { + return normalizePreviewValue(value.text); + } + if (value.html !== undefined) { + return normalizePreviewValue(value.html); + } + if (value.content !== undefined) { + return normalizePreviewValue(value.content); + } + return ''; + } + + return String(value === undefined || value === null ? '' : value); + } + + function buildHtmlDocument(html) { + var raw = String(html || '').trim(); + if (!raw) { + return ''; + } + + if (/^' + raw + ''; + } + + function renderPreview(region, value) { + var html = normalizePreviewValue(value !== undefined ? value : region).trim(); if (!html) { return '
HTML
'; } - return ''; + if (/^'; + } + + return '
' + html + '
'; } function renderEditorCard(context) { diff --git a/src/web/public/js/regions/type/webpage.js b/src/web/public/js/regions/type/webpage.js index 3e4a48f..9bbd1e9 100644 --- a/src/web/public/js/regions/type/webpage.js +++ b/src/web/public/js/regions/type/webpage.js @@ -8,9 +8,34 @@ return utils.escapeHtml ? utils.escapeHtml(value) : String(value === undefined || value === null ? '' : value); } - function renderPreview(value) { - var content = value && typeof value === 'object' && value.value !== undefined ? value : null; - var src = String(content ? content.value : value || '').trim(); + function normalizePreviewValue(value) { + if (value && typeof value === 'object') { + if (value.value !== undefined) { + return normalizePreviewValue(value.value); + } + if (value.text !== undefined) { + return normalizePreviewValue(value.text); + } + if (value.url !== undefined) { + return normalizePreviewValue(value.url); + } + if (value.href !== undefined) { + return normalizePreviewValue(value.href); + } + if (value.src !== undefined) { + return normalizePreviewValue(value.src); + } + if (value.content !== undefined) { + return normalizePreviewValue(value.content); + } + return ''; + } + + return String(value === undefined || value === null ? '' : value); + } + + function renderPreview(region, value) { + var src = normalizePreviewValue(value !== undefined ? value : region).trim(); if (!src) { return '
Webpage
'; } @@ -24,7 +49,10 @@ region: region, headerActions: 'Webpage', bodyHtml: '' + - '' + '
' + + '' + + '' + + '
' }); } diff --git a/src/web/public/js/slides/slide-form.js b/src/web/public/js/slides/slide-form.js index 65927eb..6181b54 100644 --- a/src/web/public/js/slides/slide-form.js +++ b/src/web/public/js/slides/slide-form.js @@ -825,6 +825,16 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev }); }); + templateFields.querySelectorAll('input[type="url"][name^="region_webpage_"]').forEach(function (input) { + input.addEventListener('input', function () { + templateSelectorLock.markEdited(); + }); + + input.addEventListener('change', function () { + templateSelectorLock.markEdited(); + }); + }); + if (existingTemplateId && hasExistingSlideContent()) { templateSelectorLock.arm(); templateSelectorLock.markEdited(); @@ -880,18 +890,42 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev templateSelect.addEventListener('change', renderTemplate); templateFields.addEventListener('change', function () { + var webpageInput = event.target && typeof event.target.matches === 'function' && event.target.matches('input[type="url"][name^="region_webpage_"]'); + if (webpageInput) { + templateSelectorLock.arm(); + templateSelectorLock.markEdited(); + return; + } + templateSelectorLock.arm(); templateSelectorLock.markEdited(); requestPreviewRender(); }); templateFields.addEventListener('input', function () { + var webpageInput = event.target && typeof event.target.matches === 'function' && event.target.matches('input[type="url"][name^="region_webpage_"]'); + if (webpageInput) { + templateSelectorLock.arm(); + templateSelectorLock.markEdited(); + return; + } + templateSelectorLock.arm(); templateSelectorLock.markEdited(); requestPreviewRender(); }); templateFields.addEventListener('click', function (event) { var button = event.target && typeof event.target.closest === 'function' ? event.target.closest('[data-api-items-path-reset]') : null; + var webpageUpdateButton = event.target && typeof event.target.closest === 'function' ? event.target.closest('[data-webpage-preview-update]') : null; if (!button) { + if (!webpageUpdateButton) { + return; + } + } + + if (webpageUpdateButton) { + templateSelectorLock.arm(); + templateSelectorLock.markEdited(); + requestPreviewRender(); return; } diff --git a/src/web/views/signage/slides/popup-preview.hbs b/src/web/views/signage/slides/popup-preview.hbs index 2fa6f3e..0ce7157 100644 --- a/src/web/views/signage/slides/popup-preview.hbs +++ b/src/web/views/signage/slides/popup-preview.hbs @@ -28,6 +28,22 @@ display: block; } + #popup-preview-canvas iframe { + width: 100%; + height: 100%; + border: 0; + display: block; + overflow: hidden; + } + + #popup-preview-canvas .slide-preview-webpage-region iframe { + background: #fff; + } + + #popup-preview-canvas .slide-preview-html-region iframe { + background: transparent; + } + .slide-preview-text-content { line-height: 1.5; } diff --git a/test/player-render-helpers.test.js b/test/player-render-helpers.test.js index 4ae6a3a..c15b11b 100644 --- a/test/player-render-helpers.test.js +++ b/test/player-render-helpers.test.js @@ -10,6 +10,7 @@ const { mediaKind, normalizeSlide, renderEditorJsContent, + renderHtmlRegionContent, sanitizeRichText } = require('../src/player/render-helpers'); @@ -86,4 +87,27 @@ test('renderEditorJsContent sanitizes editor blocks and wraps legacy text', () = test('timetable region registers the timetable type', () => { assert.ok(timetableRegionSource.includes("registry.register('timetable'")); assert.ok(timetableRegionSource.includes("sanitizeRichText(substituteTimetableVariables(value")); +}); + +test('shared iframe renderers size preview content explicitly', () => { + const playerHtmlRegionSource = fs.readFileSync(require.resolve('../src/player/regions/html.js'), 'utf8'); + const playerWebpageRegionSource = fs.readFileSync(require.resolve('../src/player/regions/webpage.js'), 'utf8'); + const playerRenderHelpersSource = fs.readFileSync(require.resolve('../src/player/render-helpers.js'), 'utf8'); + const slideThumbnailPreviewSource = fs.readFileSync(require.resolve('../src/web/lib/media/slide-thumbnail-preview.js'), 'utf8'); + const slideThumbnailsSource = fs.readFileSync(require.resolve('../src/web/lib/media/slide-thumbnails.js'), 'utf8'); + + assert.ok(playerHtmlRegionSource.includes('style="width:100%;height:100%;border:0;display:block;background:transparent;overflow:hidden;"')); + assert.ok(playerWebpageRegionSource.includes('style="width:100%;height:100%;border:0;display:block;background:#fff;overflow:hidden;"')); + assert.ok(playerRenderHelpersSource.includes('style="width:100%;height:100%;border:0;display:block;background:transparent;overflow:hidden;"')); + assert.ok(slideThumbnailPreviewSource.includes('style="width:100%;height:100%;border:0;display:block;background:#fff;overflow:hidden;"')); + assert.ok(slideThumbnailPreviewSource.includes('style="width:100%;height:100%;border:0;display:block;background:transparent;overflow:hidden;"')); + assert.ok(slideThumbnailsSource.includes('style="width:100%;height:100%;border:0;display:block;background:#fff;overflow:hidden;"')); + assert.ok(slideThumbnailsSource.includes('style="width:100%;height:100%;border:0;display:block;background:transparent;overflow:hidden;"')); +}); + +test('html region helpers unwrap object-shaped values before rendering', () => { + const rendered = renderHtmlRegionContent({ html: '
Hi
' }); + + assert.match(rendered, /srcdoc="<!doctype html><html><head><style>html,body\{margin:0;width:100%;height:100%;overflow:hidden;background:transparent;\}<\/style><\/head><body><div>Hi<\/div><\/body><\/html>"/); + assert.doesNotMatch(rendered, /\[object Object\]/); }); \ No newline at end of file diff --git a/test/slide-form-editor.test.js b/test/slide-form-editor.test.js index 72ffcb0..77c376f 100644 --- a/test/slide-form-editor.test.js +++ b/test/slide-form-editor.test.js @@ -6,6 +6,8 @@ const slideFormEditorSource = fs.readFileSync(require.resolve('../src/web/public const slideFormSource = fs.readFileSync(require.resolve('../src/web/public/js/slides/slide-form.js'), 'utf8'); const slideThumbnailPreviewSource = fs.readFileSync(require.resolve('../src/web/lib/media/slide-thumbnail-preview.js'), 'utf8'); const slideThumbnailsSource = fs.readFileSync(require.resolve('../src/web/lib/media/slide-thumbnails.js'), 'utf8'); +const slideHtmlRegionSource = fs.readFileSync(require.resolve('../src/web/public/js/regions/type/html.js'), 'utf8'); +const slideWebpageRegionSource = fs.readFileSync(require.resolve('../src/web/public/js/regions/type/webpage.js'), 'utf8'); test('slide editor disables pasted data images in TinyMCE', () => { assert.ok(slideFormEditorSource.includes('paste_data_images: false')); @@ -56,4 +58,24 @@ test('slide form queues editor image cleanup on save and close', () => { test('slide thumbnail previews treat image-only text as visible content', () => { assert.ok(slideThumbnailPreviewSource.includes('/ { + const popupPreviewSource = fs.readFileSync(require.resolve('../src/web/views/signage/slides/popup-preview.hbs'), 'utf8'); + + assert.ok(popupPreviewSource.includes('#popup-preview-canvas iframe')); + assert.ok(popupPreviewSource.includes('#popup-preview-canvas .slide-preview-webpage-region iframe')); + assert.ok(popupPreviewSource.includes('#popup-preview-canvas .slide-preview-html-region iframe')); +}); + +test('browser preview helpers normalize object-shaped HTML and webpage values', () => { + assert.ok(slideHtmlRegionSource.includes('normalizePreviewValue')); + assert.ok(slideWebpageRegionSource.includes('normalizePreviewValue')); +}); + +test('webpage preview updates only through the explicit button', () => { + assert.ok(slideWebpageRegionSource.includes('input-group flex-nowrap')); + assert.ok(slideWebpageRegionSource.includes('data-webpage-preview-update')); + assert.ok(slideFormSource.includes('input[type="url"][name^="region_webpage_"]')); + assert.ok(slideFormSource.includes('requestPreviewRender();')); }); \ No newline at end of file