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