Fix template region stacking order
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m19s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 32s

This commit is contained in:
lzstealth committed 2026-10-17 21:20:04 +01:00
1 parent a5d73e75d3
commit 5c134ef75f
7 files changed
+49 -9

No files matched your search

+1
View File
@@ -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. - 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. - 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 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 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. - 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. - Restored remove controls when selecting image or video media from the Media Library.
+7 -2
View File
@@ -6,6 +6,11 @@ const animationPresets = require('#src/web/public/js/templates/animation-presets
const TEMPLATE_NAME_MAX_LENGTH = 255; const TEMPLATE_NAME_MAX_LENGTH = 255;
const REGION_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) { function sanitizeBackgroundColor(value) {
const raw = String(value || '').trim(); const raw = String(value || '').trim();
if (/^#[0-9a-fA-F]{6}$/.test(raw) || /^#[0-9a-fA-F]{3}$/.test(raw)) { 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), y: Number(region.y || 0),
width: Number(region.width || 100), width: Number(region.width || 100),
height: Number(region.height || 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); }).filter((region) => region.region_key && region.label);
} }
@@ -221,7 +226,7 @@ function extractTemplateRegions(body) {
y: Number(ys[i] || 0), y: Number(ys[i] || 0),
width: Number(widths[i] || 100), width: Number(widths[i] || 100),
height: Number(heights[i] || 100), height: Number(heights[i] || 100),
z_index: Number(zs[i] || 0) z_index: normalizeRegionZIndex(zs[i])
}); });
} }
+1
View File
@@ -544,6 +544,7 @@ body.screen-blackout #app {
.template-stage { .template-stage {
position: relative; position: relative;
isolation: isolate;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
@@ -273,7 +273,7 @@
y: Number(card.querySelector('[name="region_y[]"]').value || 0), y: Number(card.querySelector('[name="region_y[]"]').value || 0),
width: Number(card.querySelector('[name="region_width[]"]').value || 0), width: Number(card.querySelector('[name="region_width[]"]').value || 0),
height: Number(card.querySelector('[name="region_height[]"]').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.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.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.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) { function clampRegion(region, size, minSize) {
@@ -300,7 +300,7 @@
y: Number(card.querySelector('[name="region_y[]"]').value || 0), y: Number(card.querySelector('[name="region_y[]"]').value || 0),
width: Number(card.querySelector('[name="region_width[]"]').value || 0), width: Number(card.querySelector('[name="region_width[]"]').value || 0),
height: Number(card.querySelector('[name="region_height[]"]').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.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.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.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() { function getOverlayRect() {
@@ -1290,7 +1290,7 @@
writeAnimationConfig(card, region.animation_json); writeAnimationConfig(card, region.animation_json);
card.querySelector('[name="region_x[]"]').value = valueOrDefault(region.x, 80); card.querySelector('[name="region_x[]"]').value = valueOrDefault(region.x, 80);
card.querySelector('[name="region_y[]"]').value = valueOrDefault(region.y, 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_width[]"]').value = valueOrDefault(region.width, 300);
card.querySelector('[name="region_height[]"]').value = valueOrDefault(region.height, 120); card.querySelector('[name="region_height[]"]').value = valueOrDefault(region.height, 120);
updateRegionLockRatioState(card); updateRegionLockRatioState(card);
@@ -1382,6 +1382,9 @@
syncLockedDimensions(card, changedField, true); syncLockedDimensions(card, changedField, true);
}); });
input.addEventListener('change', function () { input.addEventListener('change', function () {
if (field === 'z') {
input.value = Math.max(1, Math.round(Number(input.value) || 1));
}
syncLockedDimensions(card, changedField); syncLockedDimensions(card, changedField);
}); });
}); });
+1 -1
View File
@@ -171,7 +171,7 @@
<div class="region-field-grid region-field-grid--position"> <div class="region-field-grid region-field-grid--position">
<label>X<input class="form-control" type="number" name="region_x[]" value="80" required /></label> <label>X<input class="form-control" type="number" name="region_x[]" value="80" required /></label>
<label>Y<input class="form-control" type="number" name="region_y[]" value="80" required /></label> <label>Y<input class="form-control" type="number" name="region_y[]" value="80" required /></label>
<label>Z-Index<input class="form-control" type="number" name="region_z[]" value="1" required /></label> <label>Z-Index<input class="form-control" type="number" name="region_z[]" min="1" step="1" value="1" required /></label>
</div> </div>
<div class="region-field-grid region-field-grid--size"> <div class="region-field-grid region-field-grid--size">
<label>Width<input class="form-control" type="number" name="region_width[]" value="300" required /></label> <label>Width<input class="form-control" type="number" name="region_width[]" value="300" required /></label>
+31 -1
View File
@@ -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 () => { test('buildTemplatePayload resolves canvas size and rejects duplicate region names', async () => {
const pool = { const pool = {
async query(sql, params) { async query(sql, params) {
@@ -106,7 +121,7 @@ test('buildTemplatePayload resolves canvas size and rejects duplicate region nam
y: 0, y: 0,
width: 100, width: 100,
height: 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, /data-media-picker-file-input="background-image"/);
assert.match(html, /id="background-image"/); assert.match(html, /id="background-image"/);
assert.match(html, /placeholder="type_1"/); 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 Close/);
assert.match(html, /Save and New/); assert.match(html, /Save and New/);
assert.match(html, /data-confirm-unsaved="You&#x27;ve made changes\. Are you sure you want to leave this page\?"/); assert.match(html, /data-confirm-unsaved="You&#x27;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 layoutProperties = ['position', 'overflow', 'box-sizing'];
const mediaProperties = ['width', 'height', 'object-fit', 'display']; const mediaProperties = ['width', 'height', 'object-fit', 'display'];
assert.equal(readCssDeclarations(playerCss, '.template-stage').isolation, 'isolate');
assert.deepEqual( assert.deepEqual(
Object.fromEntries(layoutProperties.map((property) => [property, readCssDeclarations(previewCss, '.slide-preview-region')[property]])), Object.fromEntries(layoutProperties.map((property) => [property, readCssDeclarations(previewCss, '.slide-preview-region')[property]])),
Object.fromEntries(layoutProperties.map((property) => [property, readCssDeclarations(playerCss, '.template-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); 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', () => { test('region dimensions allow clearing and typing below the minimum until committed', () => {
for (const field of ['width', 'height']) { for (const field of ['width', 'height']) {
const harness = createRegionCardHarness(); const harness = createRegionCardHarness();