Release v2.16.7: refine animation editor dialogs
This commit is contained in:
1 parent
03a27c8e25
commit
11dd801f6d
12 files changed
+247
-84
No files matched your search
+136
-8
@@ -232,8 +232,11 @@ test('shared template action requires template update permission and loads the m
|
||||
assert.equal(withPermission.split(script).length - 1, 1, script + ' should load once');
|
||||
}
|
||||
const animationModal = withPermission.split('id="slide-animation-modal"')[1].split('id="slide-image-cropper-modal"')[0];
|
||||
assert.match(animationModal, /Advanced animation settings/);
|
||||
assert.match(animationModal, /id="slide-animation-advanced"[^>]*hidden/);
|
||||
assert.match(animationModal, /Advanced animations:/);
|
||||
assert.equal((withPermission.match(/id="animation-advanced-modal"/g) || []).length, 1);
|
||||
assert.doesNotMatch(withPermission, /id="slide-animation-advanced"/);
|
||||
const mainAnimationModal = animationModal.split('id="animation-advanced-modal"')[0];
|
||||
assert.doesNotMatch(mainAnimationModal, /data-animation-modal-field/);
|
||||
assert.match(animationModal, /Reset region animations to template/);
|
||||
assert.match(animationModal, /Reset all animations to template/);
|
||||
assert.ok(animationModal.indexOf('data-slide-animation-preview="intro"') < animationModal.indexOf('for="slide-animation-region"'));
|
||||
@@ -269,16 +272,21 @@ test('layout and animation modals use the same preview and properties column siz
|
||||
assert.match(css, /\.slide-animation-body\s*\{[^}]*grid-template-columns:\s*minmax\(0,\s*1fr\)\s+21rem;/);
|
||||
assert.match(css, /\.slide-animation-body\s*\{[^}]*gap:\s*1\.25rem;/);
|
||||
assert.match(css, /\.region-field-grid--animation\s+\[data-animation-preset\]\[hidden\]\s*\{\s*display:\s*none\s*!important;/);
|
||||
assert.match(css, /\.slide-animation-preview-panel\s*\{[^}]*container-type:\s*size;[^}]*min-height:\s*0;/);
|
||||
assert.match(css, /#slide-animation-preview\s*\{[^}]*height:\s*100%;/);
|
||||
assert.match(css, /#slide-animation-preview \.slide-preview-stage\s*\{[^}]*width:\s*min\(100%,\s*calc\(100cqh\s*\*\s*var\(--slide-layout-ratio,/);
|
||||
assert.match(css, /\.slide-animation-preview-panel\s*\{[^}]*min-height:\s*0;/);
|
||||
assert.match(css, /\.slide-animation-properties\s*\{[^}]*grid-template-rows:\s*auto minmax\(0,\s*1fr\);[^}]*min-height:\s*0;/);
|
||||
assert.match(css, /#slide-animation-preview\s*\{[^}]*container-type:\s*size;[^}]*height:\s*100%;/);
|
||||
assert.match(css, /#slide-animation-preview \.slide-preview-stage\s*\{[^}]*width:\s*min\(100cqw,\s*calc\(100cqh\s*\*\s*var\(--slide-layout-ratio,/);
|
||||
const form = fs.readFileSync(require.resolve('../src/web/views/signage/slides/form.hbs'), 'utf8');
|
||||
const previewPanel = form.split('<section class="slide-animation-preview-panel">')[1].split('</section>')[0];
|
||||
assert.doesNotMatch(previewPanel, /data-slide-animation-preview=/);
|
||||
const toolbar = form.split('<div class="slide-animation-properties">')[1].split('<div class="slide-animation-controls">')[0];
|
||||
assert.equal((toolbar.match(/data-slide-animation-preview=/g) || []).length, 3);
|
||||
assert.match(css, /@media \(max-width: 767\.98px\)[\s\S]*?\.slide-animation-preview-panel\s*\{[^}]*height:\s*min\(55dvh,\s*32rem\);/);
|
||||
});
|
||||
|
||||
test('advanced animation section uses compact heading and card spacing', function () {
|
||||
test('animation settings leave space below labels', function () {
|
||||
const css = fs.readFileSync(require.resolve('../src/web/public/css/theme-custom.css'), 'utf8');
|
||||
assert.match(css, /#slide-animation-advanced\s*\{\s*gap:\s*0\.5rem\s*!important;/);
|
||||
assert.match(css, /\.slide-animation-advanced-fields \.row\s*\{[^}]*gap:\s*0\.5rem;/);
|
||||
assert.match(css, /\.slide-animation-controls \.region-field-grid--animation label\s*\{[^}]*display:\s*grid;[^}]*gap:\s*0\.5rem;/);
|
||||
});
|
||||
|
||||
test('shared animation controls round-trip advanced settings with template normalization', function () {
|
||||
@@ -316,6 +324,126 @@ test('shared animation controls round-trip advanced settings with template norma
|
||||
assert.equal(clamped.loop.iterations, 999);
|
||||
});
|
||||
|
||||
test('slide advanced popup applies changes, cancels drafts and preserves the parent animation draft', function () {
|
||||
function element() {
|
||||
return { hidden: false, value: '', disabled: false, dataset: {}, handlers: {},
|
||||
addEventListener(type, handler) { this.handlers[type] = handler; },
|
||||
querySelectorAll() { return []; }, replaceChildren() {}, appendChild() {}, focus() {},
|
||||
setAttribute() {}, removeAttribute() {},
|
||||
style: { setProperty() {} }, classList: { add() {}, remove() {} } };
|
||||
}
|
||||
const ids = Object.fromEntries([
|
||||
'slide-animation-modal', 'adjust-slide-animations', 'slide-animation-region',
|
||||
'slide-animation-fields', 'animation-advanced-modal', 'slide-animation-preview',
|
||||
'slide-animation-save-status', 'slide-animation-reset-region',
|
||||
'slide-animation-reset-all', 'slide-animation-apply'
|
||||
].map(id => [id, element()]));
|
||||
const fields = ['intro', 'outro', 'loop'].flatMap(step => ['preset', 'duration_ms', 'delay_ms'].map(field => ({
|
||||
dataset: { animationModalStep: step, animationModalField: field }, value: ''
|
||||
})));
|
||||
const advanced = ids['animation-advanced-modal'];
|
||||
const advancedApply = element();
|
||||
const advancedReset = element();
|
||||
advanced.querySelectorAll = () => fields;
|
||||
advanced.querySelector = selector => selector === '[data-animation-modal-apply]' ? advancedApply
|
||||
: selector === '[data-animation-modal-reset]' ? advancedReset : element();
|
||||
const hidden = { value: '{}' };
|
||||
const presets = ['intro', 'outro', 'loop'].map(step => ({
|
||||
...element(), dataset: { animationPreset: step }, selectedIndex: 0,
|
||||
options: [{ value: 'none' }, { value: 'fadeIn' }]
|
||||
}));
|
||||
const resets = ['intro', 'outro', 'loop'].map(step => ({ ...element(), dataset: { animationResetButton: step } }));
|
||||
const advancedButton = element();
|
||||
const card = ids['slide-animation-fields'];
|
||||
card.querySelectorAll = selector => selector === '[data-animation-preset]' ? presets : resets;
|
||||
card.querySelector = selector => {
|
||||
if (selector === '[name="region_animation_json[]"]') return hidden;
|
||||
if (selector === '[data-animation-advanced-button]') return advancedButton;
|
||||
return resets.find(button => selector.includes('"' + button.dataset.animationResetButton + '"'));
|
||||
};
|
||||
const modalInstances = new Map();
|
||||
const scrollBarCalls = [];
|
||||
const context = { window: { innerWidth: 1200, pulseModal: {
|
||||
show(modal) { this.getOrCreate(modal)._scrollBar.hide(); modal.shown = true; },
|
||||
hide(modal) { this.getOrCreate(modal)._scrollBar.reset(); modal.shown = false; modal.handlers['hidden.bs.modal'](); },
|
||||
getOrCreate(modal) {
|
||||
if (!modalInstances.has(modal)) modalInstances.set(modal, {
|
||||
_scrollBar: {
|
||||
hide() { scrollBarCalls.push(['hide', modal]); },
|
||||
reset() { scrollBarCalls.push(['reset', modal]); }
|
||||
},
|
||||
_focustrap: { activate() {}, deactivate() {} }
|
||||
});
|
||||
return modalInstances.get(modal);
|
||||
}
|
||||
},
|
||||
pulseRegionLayout: { getTemplateAnimationSnapshot() { return []; } } },
|
||||
document: { body: element(), getElementById(id) { return ids[id] || null; }, querySelector() { return null; }, querySelectorAll() { return []; }, createElement: element }
|
||||
};
|
||||
for (const file of ['templates/animation-presets.js', 'templates/template-designer-utils.js', 'shared/region-animation-controls.js']) {
|
||||
vm.runInNewContext(fs.readFileSync(require.resolve('../src/web/public/js/' + file), 'utf8'), context);
|
||||
}
|
||||
const source = fs.readFileSync(require.resolve('../src/web/public/js/slides/slide-animation-editor.js'), 'utf8');
|
||||
vm.runInNewContext(source.replace('export function', 'function'), context);
|
||||
let applied;
|
||||
context.createSlideAnimationEditor({ stage: { parentNode: { insertBefore() {} }, nextSibling: null },
|
||||
getTemplate() { return { regions: [{ region_key: 'hero', animation_json: { intro: { preset: 'bounceIn' }, outro: { preset: 'fadeOut' } } }] }; },
|
||||
getAnimationOverrides() { return {}; }, requestPreviewRender() {}, markEdited() {},
|
||||
setAnimationOverrides(value) { applied = value; }
|
||||
});
|
||||
ids['slide-animation-region'].value = 'hero';
|
||||
ids['adjust-slide-animations'].handlers.click();
|
||||
const resetRegion = ids['slide-animation-reset-region'];
|
||||
const resetAll = ids['slide-animation-reset-all'];
|
||||
assert.equal(resetRegion.disabled, true);
|
||||
assert.equal(resetAll.disabled, true);
|
||||
presets[0].value = 'fadeIn';
|
||||
presets[0].handlers.change();
|
||||
assert.equal(resetRegion.disabled, false);
|
||||
assert.equal(resetAll.disabled, false);
|
||||
resetRegion.handlers.click();
|
||||
assert.equal(JSON.parse(hidden.value).intro.preset, 'bounceIn');
|
||||
assert.equal(resetRegion.disabled, true);
|
||||
assert.equal(resetAll.disabled, true);
|
||||
presets[0].value = 'fadeIn';
|
||||
presets[0].handlers.change();
|
||||
assert.equal(resetAll.disabled, false);
|
||||
resetAll.handlers.click();
|
||||
assert.equal(JSON.parse(hidden.value).intro.preset, 'bounceIn');
|
||||
assert.equal(resetRegion.disabled, true);
|
||||
assert.equal(resetAll.disabled, true);
|
||||
assert.equal(applied, undefined);
|
||||
advancedButton.handlers.click();
|
||||
assert.equal(advanced.shown, true);
|
||||
assert.equal(ids['slide-animation-modal'].shown, true);
|
||||
assert.equal(ids['slide-animation-modal'].inert, true);
|
||||
assert.equal(fields[0].value, 'bounceIn');
|
||||
fields[4].value = '150';
|
||||
advancedApply.handlers.click();
|
||||
assert.equal(ids['slide-animation-modal'].shown, true);
|
||||
assert.equal(ids['slide-animation-modal'].inert, false);
|
||||
assert.equal(resetRegion.disabled, false);
|
||||
assert.equal(resetAll.disabled, false);
|
||||
resets[0].handlers.click({ preventDefault() {} });
|
||||
advancedButton.handlers.click();
|
||||
assert.equal(fields[0].value, 'none');
|
||||
fields[0].value = 'bounceIn';
|
||||
context.window.pulseModal.hide(advanced);
|
||||
advancedButton.handlers.click();
|
||||
assert.equal(fields[0].value, 'none');
|
||||
advancedReset.handlers.click();
|
||||
assert.equal(fields[3].value, 'none');
|
||||
context.window.pulseModal.hide(advanced);
|
||||
ids['slide-animation-apply'].handlers.click();
|
||||
assert.equal(JSON.parse(applied.hero).intro.preset, 'none');
|
||||
assert.equal(JSON.parse(applied.hero).outro.preset, 'fadeOut');
|
||||
assert.equal(JSON.parse(applied.hero).outro.duration_ms, 1500);
|
||||
assert.deepEqual(scrollBarCalls, [
|
||||
['hide', ids['slide-animation-modal']],
|
||||
['reset', ids['slide-animation-modal']]
|
||||
]);
|
||||
});
|
||||
|
||||
function createSharedLayoutRouteHarness(failUpdate, allowSlideUpdate) {
|
||||
const registerContentRoutes = require('../src/web/routes/admin/content');
|
||||
const handlers = {};
|
||||
|
||||
@@ -329,6 +329,9 @@ test('template animation durations use percent in the advanced modal', () => {
|
||||
const sharedControls = fs.readFileSync(require.resolve('../src/web/public/js/shared/region-animation-controls.js'), 'utf8');
|
||||
|
||||
assert.match(modal, /signage\/templates\/animation-advanced-fields/);
|
||||
assert.match(modal, /data-animation-modal-title>Advanced animations:/);
|
||||
assert.ok(script.includes("title.textContent = 'Advanced animations: ' + (label ? label.textContent : 'Region');"));
|
||||
assert.doesNotMatch(modal, /modalBackdropStatic=true/);
|
||||
assert.match(advancedFields, /Duration %/);
|
||||
assert.match(advancedFields, /data-animation-modal-field="duration_ms"/);
|
||||
assert.match(advancedFields, /max="200"/);
|
||||
|
||||
Reference in new issue
Block a user