diff --git a/CHANGELOG.md b/CHANGELOG.md index d5f0a33..fb8e79d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file. - Media Library newest-first and oldest-first sorting now uses actual creation timestamps. - Browser requests now show the shared formatted error page for plain-text HTTP errors. - Template region position and stacking fields now update the preview immediately. +- Template regions now keep a positive integer stacking order above the background in the designer and player. - Region inputs no longer submit the template form when Enter is pressed. - Region width and height can be cleared and retyped without minimum-size clamping until the field is committed. - Restored remove controls when selecting image or video media from the Media Library. diff --git a/src/data/templates.js b/src/data/templates.js index 197f13a..5a7bfd4 100644 --- a/src/data/templates.js +++ b/src/data/templates.js @@ -6,6 +6,11 @@ const animationPresets = require('#src/web/public/js/templates/animation-presets const TEMPLATE_NAME_MAX_LENGTH = 255; const REGION_NAME_MAX_LENGTH = 255; +function normalizeRegionZIndex(value) { + const parsed = Number(value); + return Number.isFinite(parsed) ? Math.max(1, Math.round(parsed)) : 1; +} + function sanitizeBackgroundColor(value) { const raw = String(value || '').trim(); if (/^#[0-9a-fA-F]{6}$/.test(raw) || /^#[0-9a-fA-F]{3}$/.test(raw)) { @@ -185,7 +190,7 @@ function extractTemplateRegions(body) { y: Number(region.y || 0), width: Number(region.width || 100), height: Number(region.height || 100), - z_index: Number(region.z_index || 0) + z_index: normalizeRegionZIndex(region.z_index) }; }).filter((region) => region.region_key && region.label); } @@ -221,7 +226,7 @@ function extractTemplateRegions(body) { y: Number(ys[i] || 0), width: Number(widths[i] || 100), height: Number(heights[i] || 100), - z_index: Number(zs[i] || 0) + z_index: normalizeRegionZIndex(zs[i]) }); } diff --git a/src/player/public/css/player.css b/src/player/public/css/player.css index 1a7aef1..67e8b3f 100644 --- a/src/player/public/css/player.css +++ b/src/player/public/css/player.css @@ -544,6 +544,7 @@ body.screen-blackout #app { .template-stage { position: relative; + isolation: isolate; width: 100%; height: 100%; } diff --git a/src/web/public/js/templates/template-designer-utils.js b/src/web/public/js/templates/template-designer-utils.js index d9f1ffb..73691fc 100644 --- a/src/web/public/js/templates/template-designer-utils.js +++ b/src/web/public/js/templates/template-designer-utils.js @@ -273,7 +273,7 @@ y: Number(card.querySelector('[name="region_y[]"]').value || 0), width: Number(card.querySelector('[name="region_width[]"]').value || 0), height: Number(card.querySelector('[name="region_height[]"]').value || 0), - z_index: Number(card.querySelector('[name="region_z[]"]').value || 0) + z_index: Math.max(1, Math.round(Number(card.querySelector('[name="region_z[]"]').value) || 1)) }; } @@ -292,7 +292,7 @@ if (values.y !== undefined) { card.querySelector('[name="region_y[]"]').value = Math.round(values.y); } if (values.width !== undefined) { card.querySelector('[name="region_width[]"]').value = Math.round(values.width); } if (values.height !== undefined) { card.querySelector('[name="region_height[]"]').value = Math.round(values.height); } - if (values.z_index !== undefined) { card.querySelector('[name="region_z[]"]').value = Math.round(values.z_index); } + if (values.z_index !== undefined) { card.querySelector('[name="region_z[]"]').value = Math.max(1, Math.round(Number(values.z_index) || 1)); } } function clampRegion(region, size, minSize) { diff --git a/src/web/public/js/templates/template-designer.js b/src/web/public/js/templates/template-designer.js index 03f1067..2100c7c 100644 --- a/src/web/public/js/templates/template-designer.js +++ b/src/web/public/js/templates/template-designer.js @@ -300,7 +300,7 @@ y: Number(card.querySelector('[name="region_y[]"]').value || 0), width: Number(card.querySelector('[name="region_width[]"]').value || 0), height: Number(card.querySelector('[name="region_height[]"]').value || 0), - z_index: Number(card.querySelector('[name="region_z[]"]').value || 0) + z_index: Math.max(1, Math.round(Number(card.querySelector('[name="region_z[]"]').value) || 1)) }; } @@ -956,7 +956,7 @@ if (values.y !== undefined) { card.querySelector('[name="region_y[]"]').value = Math.round(values.y); } if (values.width !== undefined) { card.querySelector('[name="region_width[]"]').value = Math.round(values.width); } if (values.height !== undefined) { card.querySelector('[name="region_height[]"]').value = Math.round(values.height); } - if (values.z_index !== undefined) { card.querySelector('[name="region_z[]"]').value = Math.round(values.z_index); } + if (values.z_index !== undefined) { card.querySelector('[name="region_z[]"]').value = Math.max(1, Math.round(Number(values.z_index) || 1)); } } function getOverlayRect() { @@ -1290,7 +1290,7 @@ writeAnimationConfig(card, region.animation_json); card.querySelector('[name="region_x[]"]').value = valueOrDefault(region.x, 80); card.querySelector('[name="region_y[]"]').value = valueOrDefault(region.y, 80); - card.querySelector('[name="region_z[]"]').value = valueOrDefault(region.z_index, 1); + card.querySelector('[name="region_z[]"]').value = Math.max(1, Math.round(Number(region.z_index) || 1)); card.querySelector('[name="region_width[]"]').value = valueOrDefault(region.width, 300); card.querySelector('[name="region_height[]"]').value = valueOrDefault(region.height, 120); updateRegionLockRatioState(card); @@ -1382,6 +1382,9 @@ syncLockedDimensions(card, changedField, true); }); input.addEventListener('change', function () { + if (field === 'z') { + input.value = Math.max(1, Math.round(Number(input.value) || 1)); + } syncLockedDimensions(card, changedField); }); }); diff --git a/src/web/views/signage/templates/form.hbs b/src/web/views/signage/templates/form.hbs index 53c1589..96829e7 100644 --- a/src/web/views/signage/templates/form.hbs +++ b/src/web/views/signage/templates/form.hbs @@ -171,7 +171,7 @@
- +
diff --git a/test/templates.test.js b/test/templates.test.js index b39d464..f5edcaf 100644 --- a/test/templates.test.js +++ b/test/templates.test.js @@ -61,6 +61,21 @@ test('extractTemplateRegions normalizes JSON regions and filters invalid rows', }]); }); +test('extractTemplateRegions keeps JSON and form z-index values at positive integers', () => { + const values = [-5, 0, '', undefined, 'invalid', 'Infinity', 1, '4', 2.6]; + const expected = [1, 1, 1, 1, 1, 1, 1, 4, 3]; + const jsonRegions = extractTemplateRegions({ + regions_json: JSON.stringify(values.map((value, index) => ({ region_name: `Region ${index}`, z_index: value }))) + }); + const formRegions = extractTemplateRegions({ + 'region_key[]': values.map((value, index) => `Region ${index}`), + 'region_z[]': values + }); + + assert.deepEqual(jsonRegions.map((region) => region.z_index), expected); + assert.deepEqual(formRegions.map((region) => region.z_index), expected); +}); + test('buildTemplatePayload resolves canvas size and rejects duplicate region names', async () => { const pool = { async query(sql, params) { @@ -106,7 +121,7 @@ test('buildTemplatePayload resolves canvas size and rejects duplicate region nam y: 0, width: 100, height: 100, - z_index: 0 + z_index: 1 }] }); @@ -184,6 +199,7 @@ test('template create page renders the background remove control', () => { assert.match(html, /data-media-picker-file-input="background-image"/); assert.match(html, /id="background-image"/); assert.match(html, /placeholder="type_1"/); + assert.match(html, /name="region_z\[\]" min="1" step="1" value="1" required/); assert.match(html, /Save and Close/); assert.match(html, /Save and New/); assert.match(html, /data-confirm-unsaved="You've made changes\. Are you sure you want to leave this page\?"/); @@ -265,6 +281,7 @@ test('template preview and live output keep matching visual CSS contracts', () = const layoutProperties = ['position', 'overflow', 'box-sizing']; const mediaProperties = ['width', 'height', 'object-fit', 'display']; + assert.equal(readCssDeclarations(playerCss, '.template-stage').isolation, 'isolate'); assert.deepEqual( Object.fromEntries(layoutProperties.map((property) => [property, readCssDeclarations(previewCss, '.slide-preview-region')[property]])), Object.fromEntries(layoutProperties.map((property) => [property, readCssDeclarations(playerCss, '.template-region')[property]])) @@ -405,6 +422,19 @@ test('region coordinate and stacking fields immediately refresh the preview and assert.equal(harness.dirtyCount, 3); }); +test('region stacking stays above the background while typing and clamps on commit', () => { + const harness = createRegionCardHarness(); + for (const value of ['-5', '0', '']) { + harness.edit('z', value); + assert.equal(harness.previews.at(-1).z_index, 1); + harness.edit('z', value, 'change'); + assert.equal(harness.value('z'), 1); + } + harness.edit('z', '4', 'change'); + assert.equal(harness.value('z'), 4); + assert.equal(harness.previews.at(-1).z_index, 4); +}); + test('region dimensions allow clearing and typing below the minimum until committed', () => { for (const field of ['width', 'height']) { const harness = createRegionCardHarness();