Fix template region stacking order
This commit is contained in:
1 parent
a5d73e75d3
commit
5c134ef75f
7 files changed
+49
-9
No files matched your search
@@ -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.
|
||||||
|
|||||||
@@ -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])
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -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've made changes\. Are you sure you want to leave this page\?"/);
|
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 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();
|
||||||
|
|||||||
Reference in new issue
Block a user