diff --git a/CHANGELOG.md b/CHANGELOG.md index bbbe6b2..3c5769a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to this project will be documented in this file. ### Fixed -- Animation delays now work in slide and template previews and player playback. Outro timing follows each preset's computed Animate.css duration, is configured as time before transition, and preview-only region staggering is removed; slide animation controls enforce the same advanced limits as templates. +- Animation delays now work in slide and template previews and player playback. Outro timing follows each preset's computed Animate.css duration and is configured as time before transition; preview cleanup waits for each preset's computed runtime, preview-only region staggering is removed, and slide animation controls enforce the same advanced limits as templates. ## 2.21.1 - 2026-10-26 diff --git a/src/web/public/js/slides/slide-animation-editor.js b/src/web/public/js/slides/slide-animation-editor.js index f1dba06..27cb9fe 100644 --- a/src/web/public/js/slides/slide-animation-editor.js +++ b/src/web/public/js/slides/slide-animation-editor.js @@ -132,6 +132,17 @@ export function createSlideAnimationEditor(options) { function getPreviewElements() { return Array.prototype.slice.call(previewHost.querySelectorAll('.slide-preview-region[data-region-key]')); } + function getAnimationDurationMs(element, fallbackDurationMs) { + if (typeof window.getComputedStyle !== 'function') return fallbackDurationMs; + var durations = String(window.getComputedStyle(element).animationDuration || '').split(','); + var longestDuration = durations.reduce(function (longest, value) { + var match = value.trim().match(/^([\d.]+)\s*(ms|s)$/i); + if (!match) return longest; + var duration = Number(match[1]) * (match[2].toLowerCase() === 's' ? 1000 : 1); + return Number.isFinite(duration) ? Math.max(longest, duration) : longest; + }, 0); + return longestDuration || fallbackDurationMs; + } function stopPreview() { if (previewTimer) window.clearTimeout(previewTimer); previewTimer = 0; @@ -185,7 +196,7 @@ export function createSlideAnimationEditor(options) { void element.offsetWidth; element.classList.add('animate__animated', 'animate__' + preset); if (mode === 'loop') element.classList.add('animate__repeat-1'); - longest = Math.max(longest, delay + duration * iterations); + longest = Math.max(longest, delay + getAnimationDurationMs(element, duration) * iterations); }); updatePreviewButtons(); previewTimer = window.setTimeout(function () { diff --git a/test/region-layout.test.js b/test/region-layout.test.js index 7f0a157..faf0292 100644 --- a/test/region-layout.test.js +++ b/test/region-layout.test.js @@ -383,7 +383,13 @@ test('slide advanced popup applies changes, cancels drafts and preserves the par }; const modalInstances = new Map(); const scrollBarCalls = []; - const context = { window: { innerWidth: 1200, setTimeout() { return 1; }, clearTimeout() {}, pulseModal: { + const previewTimers = []; + const context = { window: { innerWidth: 1200, + setTimeout(_callback, delay) { previewTimers.push(delay); return previewTimers.length; }, clearTimeout() {}, + getComputedStyle() { + return { animationDuration: '400ms' }; + }, + 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) { @@ -410,7 +416,7 @@ test('slide advanced popup applies changes, cancels drafts and preserves the par getTemplate() { return { regions: [ { region_key: 'hero', animation_json: { intro: { preset: 'bounceIn' }, outro: { preset: 'fadeOut' } } }, { region_key: 'fixed_brand', read_only: true, animation_json: { - outro: { preset: 'fadeOut', duration_ms: 200, delay_ms: 500 }, + outro: { preset: 'hinge', duration_ms: 200, delay_ms: 500 }, loop: { preset: 'pulse', duration_ms: 200, delay_ms: 240, iterations: 1 } } }, { region_key: 'fixed_brand_second', read_only: true, animation_json: { loop: { preset: 'pulse', duration_ms: 200, iterations: 1 } } } @@ -432,6 +438,7 @@ test('slide advanced popup applies changes, cancels drafts and preserves the par animationPreviewButtons[1].handlers.click(); assert.equal(readonlyPreviewRegion.style.values['animation-delay'], '0ms'); animationPreviewButtons[1].handlers.click(); + assert.equal(previewTimers[previewTimers.length - 1], 580); const resetRegion = ids['slide-animation-reset-region']; const resetAll = ids['slide-animation-reset-all']; assert.equal(resetRegion.disabled, true);