Release 2.21.2: fix animation timing
This commit is contained in:
1 parent
c923dcc1c5
commit
554678b3c4
13 files changed
+151
-54
No files matched your search
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## 2.21.2 - 2026-10-26
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Animation delays now work in slide and template previews and player playback. Outro timing is configured as time before transition, preview-only region staggering is removed, and slide animation controls enforce the same advanced limits as templates.
|
||||||
|
|
||||||
## 2.21.1 - 2026-10-26
|
## 2.21.1 - 2026-10-26
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage-player",
|
"name": "pulse-signage-player",
|
||||||
"version": "2.21.1",
|
"version": "2.21.2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage player application bundle",
|
"description": "Pulse Signage player application bundle",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage-web",
|
"name": "pulse-signage-web",
|
||||||
"version": "2.21.1",
|
"version": "2.21.2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage web and bridge application bundle",
|
"description": "Pulse Signage web and bridge application bundle",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.21.1",
|
"version": "2.21.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.21.1",
|
"version": "2.21.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@better-auth/passkey": "^1.7.13",
|
"@better-auth/passkey": "^1.7.13",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.21.1",
|
"version": "2.21.2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage application with MySQL and media storage",
|
"description": "Pulse Signage application with MySQL and media storage",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ function clearRegionAnimationClasses(root) {
|
|||||||
element.style.removeProperty('--animate-duration');
|
element.style.removeProperty('--animate-duration');
|
||||||
element.style.removeProperty('--animate-delay');
|
element.style.removeProperty('--animate-delay');
|
||||||
element.style.removeProperty('--animate-repeat');
|
element.style.removeProperty('--animate-repeat');
|
||||||
|
element.style.removeProperty('animation-delay');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,13 +162,16 @@ function applyAnimationStep(element, step, phase) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
element.classList.add('animate__animated', 'animate__' + preset);
|
|
||||||
element.style.setProperty('--animate-duration', String(Math.max(1, Number(step.duration_ms || 0) || 1000)) + 'ms');
|
element.style.setProperty('--animate-duration', String(Math.max(1, Number(step.duration_ms || 0) || 1000)) + 'ms');
|
||||||
if (Number(step.delay_ms || 0) > 0) {
|
var delayMs = phase === 'outro' ? 0 : Math.max(0, Number(step.delay_ms || 0));
|
||||||
element.style.setProperty('--animate-delay', String(Math.max(0, Number(step.delay_ms || 0))) + 'ms');
|
if (delayMs > 0) {
|
||||||
|
element.style.setProperty('--animate-delay', String(delayMs) + 'ms');
|
||||||
|
element.style.setProperty('animation-delay', String(delayMs) + 'ms');
|
||||||
} else {
|
} else {
|
||||||
element.style.removeProperty('--animate-delay');
|
element.style.removeProperty('--animate-delay');
|
||||||
|
element.style.removeProperty('animation-delay');
|
||||||
}
|
}
|
||||||
|
element.classList.add('animate__animated', 'animate__' + preset);
|
||||||
|
|
||||||
if (phase === 'loop') {
|
if (phase === 'loop') {
|
||||||
var repeatCount = Number(step.iterations);
|
var repeatCount = Number(step.iterations);
|
||||||
|
|||||||
@@ -76,8 +76,22 @@
|
|||||||
});
|
});
|
||||||
return normalize(config);
|
return normalize(config);
|
||||||
}
|
}
|
||||||
|
function clampAdvancedField(input) {
|
||||||
|
if (!input || !input.dataset) return;
|
||||||
|
var field = input.dataset.animationModalField;
|
||||||
|
var value = String(input.value || '').trim();
|
||||||
|
if (!value) return;
|
||||||
|
var number = Number(value);
|
||||||
|
if (!Number.isFinite(number)) return;
|
||||||
|
var normalized;
|
||||||
|
if (field === 'duration_ms') normalized = Math.min(200, Math.max(1, Math.round(number)));
|
||||||
|
else if (field === 'iterations' && input.dataset.animationModalStep === 'loop') normalized = Math.min(999, Math.max(1, Math.round(number)));
|
||||||
|
else return;
|
||||||
|
if (String(normalized) !== String(input.value)) input.value = String(normalized);
|
||||||
|
}
|
||||||
window.pulseRegionAnimationControls = {
|
window.pulseRegionAnimationControls = {
|
||||||
read: read, write: write, setPreset: setPreset, bind: bind,
|
read: read, write: write, setPreset: setPreset, bind: bind,
|
||||||
populateAdvanced: populateAdvanced, readAdvanced: readAdvanced
|
populateAdvanced: populateAdvanced, readAdvanced: readAdvanced,
|
||||||
|
clampAdvancedField: clampAdvancedField
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
@@ -141,7 +141,7 @@ export function createSlideAnimationEditor(options) {
|
|||||||
element.className.split(/\s+/).forEach(function (className) {
|
element.className.split(/\s+/).forEach(function (className) {
|
||||||
if (className.indexOf('animate__') === 0) element.classList.remove(className);
|
if (className.indexOf('animate__') === 0) element.classList.remove(className);
|
||||||
});
|
});
|
||||||
['--animate-duration', '--animate-delay', '--animate-repeat'].forEach(function (property) { element.style.removeProperty(property); });
|
['--animate-duration', '--animate-delay', '--animate-repeat', 'animation-delay'].forEach(function (property) { element.style.removeProperty(property); });
|
||||||
});
|
});
|
||||||
updatePreviewButtons();
|
updatePreviewButtons();
|
||||||
}
|
}
|
||||||
@@ -168,16 +168,18 @@ export function createSlideAnimationEditor(options) {
|
|||||||
previewMode = mode;
|
previewMode = mode;
|
||||||
var token = previewToken;
|
var token = previewToken;
|
||||||
var longest = 0;
|
var longest = 0;
|
||||||
getPreviewElements().forEach(function (element, index) {
|
getPreviewElements().forEach(function (element) {
|
||||||
var config = window.templateDesignerUtils.normalizeAnimationConfig(getConfig(element.dataset.regionKey));
|
var config = window.templateDesignerUtils.normalizeAnimationConfig(getConfig(element.dataset.regionKey));
|
||||||
var step = config[mode] || {};
|
var step = config[mode] || {};
|
||||||
var preset = window.templateAnimationPresets.normalize(step.preset);
|
var preset = window.templateAnimationPresets.normalize(step.preset);
|
||||||
if (preset === 'none') return;
|
if (preset === 'none') return;
|
||||||
var delay = Math.max(0, Number(step.delay_ms) || 0) + index * 90;
|
var configuredDelay = mode === 'outro' ? 0 : Math.max(0, Number(step.delay_ms) || 0);
|
||||||
|
var delay = configuredDelay;
|
||||||
var duration = Math.max(1, Number(step.duration_ms) || 1000);
|
var duration = Math.max(1, Number(step.duration_ms) || 1000);
|
||||||
var iterations = mode === 'loop' ? Math.max(1, Number(step.iterations) || 1) : 1;
|
var iterations = mode === 'loop' ? Math.max(1, Number(step.iterations) || 1) : 1;
|
||||||
element.style.setProperty('--animate-duration', duration + 'ms');
|
element.style.setProperty('--animate-duration', duration + 'ms');
|
||||||
element.style.setProperty('--animate-delay', delay + 'ms');
|
element.style.setProperty('--animate-delay', delay + 'ms');
|
||||||
|
element.style.setProperty('animation-delay', delay + 'ms');
|
||||||
element.style.setProperty('--animate-repeat', String(iterations));
|
element.style.setProperty('--animate-repeat', String(iterations));
|
||||||
element.classList.remove('animate__animated', 'animate__' + preset, 'animate__repeat-1');
|
element.classList.remove('animate__animated', 'animate__' + preset, 'animate__repeat-1');
|
||||||
void element.offsetWidth;
|
void element.offsetWidth;
|
||||||
@@ -266,6 +268,9 @@ export function createSlideAnimationEditor(options) {
|
|||||||
advanced.addEventListener('shown.bs.modal', function () {
|
advanced.addEventListener('shown.bs.modal', function () {
|
||||||
advanced.querySelector('[data-animation-modal-field]').focus();
|
advanced.querySelector('[data-animation-modal-field]').focus();
|
||||||
});
|
});
|
||||||
|
advanced.addEventListener('input', function (event) {
|
||||||
|
controls.clampAdvancedField(event.target);
|
||||||
|
});
|
||||||
advanced.addEventListener('hidden.bs.modal', function () {
|
advanced.addEventListener('hidden.bs.modal', function () {
|
||||||
window.pulseModal.getOrCreate(advanced)._scrollBar = advancedScrollBar;
|
window.pulseModal.getOrCreate(advanced)._scrollBar = advancedScrollBar;
|
||||||
advancedScrollBar = null;
|
advancedScrollBar = null;
|
||||||
|
|||||||
@@ -363,6 +363,7 @@
|
|||||||
rect.style.removeProperty('--animate-duration');
|
rect.style.removeProperty('--animate-duration');
|
||||||
rect.style.removeProperty('--animate-delay');
|
rect.style.removeProperty('--animate-delay');
|
||||||
rect.style.removeProperty('--animate-repeat');
|
rect.style.removeProperty('--animate-repeat');
|
||||||
|
rect.style.removeProperty('animation-delay');
|
||||||
rect.style.opacity = '';
|
rect.style.opacity = '';
|
||||||
rect.style.transform = '';
|
rect.style.transform = '';
|
||||||
rect.style.filter = '';
|
rect.style.filter = '';
|
||||||
@@ -442,15 +443,20 @@
|
|||||||
var durationMs = Number(step && step.duration_ms);
|
var durationMs = Number(step && step.duration_ms);
|
||||||
var delayMs = Number(step && step.delay_ms);
|
var delayMs = Number(step && step.delay_ms);
|
||||||
var repeatCount = Number(step && step.iterations);
|
var repeatCount = Number(step && step.iterations);
|
||||||
|
if (mode === 'out') delayMs = 0;
|
||||||
var isAttentionSeeker = preset === 'bounce' || preset === 'flash' || preset === 'pulse' || preset === 'rubberBand' || preset === 'shakeX' || preset === 'shakeY' || preset === 'headShake' || preset === 'swing' || preset === 'tada' || preset === 'wobble' || preset === 'jello' || preset === 'heartBeat';
|
var isAttentionSeeker = preset === 'bounce' || preset === 'flash' || preset === 'pulse' || preset === 'rubberBand' || preset === 'shakeX' || preset === 'shakeY' || preset === 'headShake' || preset === 'swing' || preset === 'tada' || preset === 'wobble' || preset === 'jello' || preset === 'heartBeat';
|
||||||
|
|
||||||
rect.classList.remove('animate__animated', 'animate__infinite');
|
rect.classList.remove('animate__animated', 'animate__infinite');
|
||||||
rect.style.removeProperty('--animate-duration');
|
rect.style.removeProperty('--animate-duration');
|
||||||
rect.style.removeProperty('--animate-delay');
|
rect.style.removeProperty('--animate-delay');
|
||||||
rect.style.removeProperty('--animate-repeat');
|
rect.style.removeProperty('--animate-repeat');
|
||||||
|
rect.style.removeProperty('animation-delay');
|
||||||
rect.style.setProperty('--animate-duration', Math.max(0, Number.isFinite(durationMs) && durationMs > 0 ? durationMs : 1000) + 'ms');
|
rect.style.setProperty('--animate-duration', Math.max(0, Number.isFinite(durationMs) && durationMs > 0 ? durationMs : 1000) + 'ms');
|
||||||
if (Number.isFinite(delayMs) && delayMs > 0) {
|
if (Number.isFinite(delayMs) && delayMs > 0) {
|
||||||
rect.style.setProperty('--animate-delay', Math.max(0, delayMs) + 'ms');
|
rect.style.setProperty('--animate-delay', Math.max(0, delayMs) + 'ms');
|
||||||
|
rect.style.setProperty('animation-delay', Math.max(0, delayMs) + 'ms');
|
||||||
|
} else {
|
||||||
|
rect.style.removeProperty('animation-delay');
|
||||||
}
|
}
|
||||||
rect.classList.add('animate__animated', animateClass);
|
rect.classList.add('animate__animated', animateClass);
|
||||||
if (isAttentionSeeker) {
|
if (isAttentionSeeker) {
|
||||||
@@ -475,6 +481,9 @@
|
|||||||
if (!Number.isFinite(delayMs) || delayMs < 0) {
|
if (!Number.isFinite(delayMs) || delayMs < 0) {
|
||||||
delayMs = 0;
|
delayMs = 0;
|
||||||
}
|
}
|
||||||
|
if (normalizedMode === 'out') {
|
||||||
|
delayMs = 0;
|
||||||
|
}
|
||||||
if (!Number.isFinite(iterations) || iterations < 1) {
|
if (!Number.isFinite(iterations) || iterations < 1) {
|
||||||
iterations = 1;
|
iterations = 1;
|
||||||
}
|
}
|
||||||
@@ -595,39 +604,6 @@
|
|||||||
window.pulseRegionAnimationControls.populateAdvanced(animationAdvancedModal, config);
|
window.pulseRegionAnimationControls.populateAdvanced(animationAdvancedModal, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readNumberField(value) {
|
|
||||||
var raw = String(value || '').trim();
|
|
||||||
if (!raw) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
var next = Number(raw);
|
|
||||||
return Number.isFinite(next) ? Math.round(next) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clampAnimationModalField(field) {
|
|
||||||
if (!field || !field.matches) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var isLoopIterationsField = field.matches('[data-animation-modal-step="loop"][data-animation-modal-field="iterations"]');
|
|
||||||
var isDurationField = field.matches('[data-animation-modal-field="duration_ms"]');
|
|
||||||
if (!isLoopIterationsField && !isDurationField) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fieldValue = readNumberField(field.value);
|
|
||||||
if (fieldValue === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var normalizedValue = isLoopIterationsField
|
|
||||||
? Math.min(MAX_ANIMATION_REPEAT_COUNT, Math.max(1, fieldValue))
|
|
||||||
: Math.min(200, Math.max(1, fieldValue));
|
|
||||||
if (String(normalizedValue) !== String(field.value)) {
|
|
||||||
field.value = String(normalizedValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function readAnimationModalConfig() {
|
function readAnimationModalConfig() {
|
||||||
if (!animationAdvancedModal) {
|
if (!animationAdvancedModal) {
|
||||||
return null;
|
return null;
|
||||||
@@ -732,7 +708,6 @@
|
|||||||
function animatePreviewRects(mode, regions) {
|
function animatePreviewRects(mode, regions) {
|
||||||
var rects = getPreviewRects();
|
var rects = getPreviewRects();
|
||||||
var token = previewState.token;
|
var token = previewState.token;
|
||||||
var stagger = 90;
|
|
||||||
var duration = 900;
|
var duration = 900;
|
||||||
var overlayRect = getOverlayRect();
|
var overlayRect = getOverlayRect();
|
||||||
var canvasMovement = Math.max(12, Math.round(overlayRect.height * 0.02));
|
var canvasMovement = Math.max(12, Math.round(overlayRect.height * 0.02));
|
||||||
@@ -740,11 +715,11 @@
|
|||||||
|
|
||||||
previewState.animations = [];
|
previewState.animations = [];
|
||||||
|
|
||||||
rects.forEach(function (rect, index) {
|
rects.forEach(function (rect) {
|
||||||
var region = regions && regions[index] ? regions[index] : null;
|
var region = regions && regions[index] ? regions[index] : null;
|
||||||
var step = getPreviewRegionAnimationStep(region, mode);
|
var step = getPreviewRegionAnimationStep(region, mode);
|
||||||
var preset = normalizePreviewAnimation(step && step.preset);
|
var preset = normalizePreviewAnimation(step && step.preset);
|
||||||
var delay = index * stagger;
|
var delay = mode === 'out' ? 0 : Math.max(0, Number(step && step.delay_ms) || 0);
|
||||||
var animation = null;
|
var animation = null;
|
||||||
var animateCssMode = isAnimateCssPreset(preset);
|
var animateCssMode = isAnimateCssPreset(preset);
|
||||||
var timingMs = delay + getPreviewAnimationStepTimingMs(step, mode, animateCssMode);
|
var timingMs = delay + getPreviewAnimationStepTimingMs(step, mode, animateCssMode);
|
||||||
@@ -1785,7 +1760,7 @@
|
|||||||
activeAnimationCard = null;
|
activeAnimationCard = null;
|
||||||
});
|
});
|
||||||
animationAdvancedModal.addEventListener('input', function (event) {
|
animationAdvancedModal.addEventListener('input', function (event) {
|
||||||
clampAnimationModalField(event.target);
|
window.pulseRegionAnimationControls.clampAdvancedField(event.target);
|
||||||
markTemplateFormDirty();
|
markTemplateFormDirty();
|
||||||
});
|
});
|
||||||
animationAdvancedModal.addEventListener('change', function () {
|
animationAdvancedModal.addEventListener('change', function () {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<div class="card-body d-grid gap-3 align-content-start">
|
<div class="card-body d-grid gap-3 align-content-start">
|
||||||
<label class="d-grid gap-1">Preset<select class="form-select" data-animation-modal-step="outro" data-animation-modal-field="preset">{{#each animationPresets.advanced.outro}}<option value="{{value}}">{{label}}</option>{{/each}}</select></label>
|
<label class="d-grid gap-1">Preset<select class="form-select" data-animation-modal-step="outro" data-animation-modal-field="preset">{{#each animationPresets.advanced.outro}}<option value="{{value}}">{{label}}</option>{{/each}}</select></label>
|
||||||
<label class="d-grid gap-1">Duration %<input class="form-control" type="number" min="1" max="200" step="1" data-animation-modal-step="outro" data-animation-modal-field="duration_ms" placeholder="100" /></label>
|
<label class="d-grid gap-1">Duration %<input class="form-control" type="number" min="1" max="200" step="1" data-animation-modal-step="outro" data-animation-modal-field="duration_ms" placeholder="100" /></label>
|
||||||
<label class="d-grid gap-1">Delay ms<input class="form-control" type="number" min="0" step="1" data-animation-modal-step="outro" data-animation-modal-field="delay_ms" placeholder="0" /></label>
|
<label class="d-grid gap-1">Finish before transition (ms)<input class="form-control" type="number" min="0" step="1" data-animation-modal-step="outro" data-animation-modal-field="delay_ms" placeholder="0" /></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1181,4 +1181,50 @@ test('playlist removal of the current slide queues without rerendering it immedi
|
|||||||
assert.equal(JSON.stringify(sandbox.slides), JSON.stringify([{ id: 1, duration_seconds: 12 }, { id: 2, duration_seconds: 12 }]));
|
assert.equal(JSON.stringify(sandbox.slides), JSON.stringify([{ id: 1, duration_seconds: 12 }, { id: 2, duration_seconds: 12 }]));
|
||||||
assert.equal(calls.showCurrent, 0);
|
assert.equal(calls.showCurrent, 0);
|
||||||
assert.equal(calls.logDebug.some((entry) => entry.includes('applying on next slide transition')), true);
|
assert.equal(calls.logDebug.some((entry) => entry.includes('applying on next slide transition')), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('player applies animation delays and schedules outro triggers to finish at slide advance', () => {
|
||||||
|
const animationPath = path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-animation.js');
|
||||||
|
const transitionPath = path.join(__dirname, '..', 'src', 'player', 'public', 'js', 'player-page-transition.js');
|
||||||
|
const values = {};
|
||||||
|
const classes = new Set();
|
||||||
|
const config = JSON.stringify({
|
||||||
|
intro: { preset: 'fadeIn', duration_ms: 600, delay_ms: 250 },
|
||||||
|
outro: { preset: 'fadeOut', duration_ms: 700, delay_ms: 400 }
|
||||||
|
});
|
||||||
|
const element = {
|
||||||
|
dataset: {},
|
||||||
|
isConnected: true,
|
||||||
|
style: {
|
||||||
|
setProperty(name, value) { values[name] = value; },
|
||||||
|
removeProperty(name) { delete values[name]; }
|
||||||
|
},
|
||||||
|
classList: {
|
||||||
|
add(...names) { names.forEach(name => classes.add(name)); },
|
||||||
|
remove(...names) { names.forEach(name => classes.delete(name)); },
|
||||||
|
[Symbol.iterator]() { return classes[Symbol.iterator](); }
|
||||||
|
},
|
||||||
|
getAttribute(name) { return name === 'data-animation-json' ? config : null; },
|
||||||
|
addEventListener() {},
|
||||||
|
removeEventListener() {},
|
||||||
|
querySelectorAll() { return []; }
|
||||||
|
};
|
||||||
|
const root = { querySelectorAll() { return [element]; } };
|
||||||
|
const timers = [];
|
||||||
|
const sandbox = {
|
||||||
|
window: null,
|
||||||
|
app: { querySelectorAll() { return [root]; }, firstElementChild: root },
|
||||||
|
setTimeout(callback, delay) { timers.push({ callback, delay }); return timers.length; },
|
||||||
|
clearTimeout() {}
|
||||||
|
};
|
||||||
|
sandbox.window = sandbox;
|
||||||
|
vm.runInNewContext(fs.readFileSync(animationPath, 'utf8') + '\n' + fs.readFileSync(transitionPath, 'utf8'), sandbox);
|
||||||
|
|
||||||
|
sandbox.playRegionAnimation(element, 'intro');
|
||||||
|
assert.equal(values['animation-delay'], '250ms');
|
||||||
|
|
||||||
|
sandbox.scheduleSlideOutro(5000);
|
||||||
|
assert.equal(timers[0].delay, 3900);
|
||||||
|
timers[0].callback();
|
||||||
|
assert.equal(values['animation-delay'], undefined);
|
||||||
});
|
});
|
||||||
@@ -322,6 +322,12 @@ test('shared animation controls round-trip advanced settings with template norma
|
|||||||
assert.equal(clamped.intro.duration_ms, 2000);
|
assert.equal(clamped.intro.duration_ms, 2000);
|
||||||
assert.equal(clamped.intro.delay_ms, 0);
|
assert.equal(clamped.intro.delay_ms, 0);
|
||||||
assert.equal(clamped.loop.iterations, 999);
|
assert.equal(clamped.loop.iterations, 999);
|
||||||
|
fields[1].value = '999';
|
||||||
|
fields[4].value = '2000';
|
||||||
|
controls.clampAdvancedField(fields[1]);
|
||||||
|
controls.clampAdvancedField(fields[4]);
|
||||||
|
assert.equal(fields[1].value, '200');
|
||||||
|
assert.equal(fields[4].value, '999');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('slide advanced popup applies changes, cancels drafts and preserves the parent animation draft', function () {
|
test('slide advanced popup applies changes, cancels drafts and preserves the parent animation draft', function () {
|
||||||
@@ -330,7 +336,8 @@ test('slide advanced popup applies changes, cancels drafts and preserves the par
|
|||||||
addEventListener(type, handler) { this.handlers[type] = handler; },
|
addEventListener(type, handler) { this.handlers[type] = handler; },
|
||||||
querySelectorAll() { return []; }, replaceChildren() {}, appendChild() {}, focus() {},
|
querySelectorAll() { return []; }, replaceChildren() {}, appendChild() {}, focus() {},
|
||||||
setAttribute() {}, removeAttribute() {},
|
setAttribute() {}, removeAttribute() {},
|
||||||
style: { setProperty() {}, removeProperty() {} }, classList: { add() {}, remove() {}, toggle() {} } };
|
style: { values: {}, setProperty(name, value) { this.values[name] = value; }, removeProperty(name) { delete this.values[name]; } },
|
||||||
|
classList: { add() {}, remove() {}, toggle() {} } };
|
||||||
}
|
}
|
||||||
const ids = Object.fromEntries([
|
const ids = Object.fromEntries([
|
||||||
'slide-animation-modal', 'adjust-slide-animations', 'slide-animation-region',
|
'slide-animation-modal', 'adjust-slide-animations', 'slide-animation-region',
|
||||||
@@ -352,9 +359,12 @@ test('slide advanced popup applies changes, cancels drafts and preserves the par
|
|||||||
add(...classNames) { readonlyPreviewRegion.className += ' ' + classNames.join(' '); },
|
add(...classNames) { readonlyPreviewRegion.className += ' ' + classNames.join(' '); },
|
||||||
remove(...classNames) { readonlyPreviewRegion.className = readonlyPreviewRegion.className.split(/\s+/).filter(name => !classNames.includes(name)).join(' '); }
|
remove(...classNames) { readonlyPreviewRegion.className = readonlyPreviewRegion.className.split(/\s+/).filter(name => !classNames.includes(name)).join(' '); }
|
||||||
};
|
};
|
||||||
|
const secondPreviewRegion = element();
|
||||||
|
secondPreviewRegion.className = 'slide-preview-region';
|
||||||
|
secondPreviewRegion.dataset.regionKey = 'fixed_brand_second';
|
||||||
advanced.querySelectorAll = () => fields;
|
advanced.querySelectorAll = () => fields;
|
||||||
ids['slide-animation-modal'].querySelectorAll = selector => selector === '[data-slide-animation-preview]' ? animationPreviewButtons : [];
|
ids['slide-animation-modal'].querySelectorAll = selector => selector === '[data-slide-animation-preview]' ? animationPreviewButtons : [];
|
||||||
ids['slide-animation-preview'].querySelectorAll = () => [readonlyPreviewRegion];
|
ids['slide-animation-preview'].querySelectorAll = () => [readonlyPreviewRegion, secondPreviewRegion];
|
||||||
advanced.querySelector = selector => selector === '[data-animation-modal-apply]' ? advancedApply
|
advanced.querySelector = selector => selector === '[data-animation-modal-apply]' ? advancedApply
|
||||||
: selector === '[data-animation-modal-reset]' ? advancedReset : element();
|
: selector === '[data-animation-modal-reset]' ? advancedReset : element();
|
||||||
const hidden = { value: '{}' };
|
const hidden = { value: '{}' };
|
||||||
@@ -399,17 +409,29 @@ test('slide advanced popup applies changes, cancels drafts and preserves the par
|
|||||||
context.createSlideAnimationEditor({ stage: { parentNode: { insertBefore() {} }, nextSibling: null },
|
context.createSlideAnimationEditor({ stage: { parentNode: { insertBefore() {} }, nextSibling: null },
|
||||||
getTemplate() { return { regions: [
|
getTemplate() { return { regions: [
|
||||||
{ region_key: 'hero', animation_json: { intro: { preset: 'bounceIn' }, outro: { preset: 'fadeOut' } } },
|
{ region_key: 'hero', animation_json: { intro: { preset: 'bounceIn' }, outro: { preset: 'fadeOut' } } },
|
||||||
{ region_key: 'fixed_brand', read_only: true, animation_json: { loop: { preset: 'pulse', duration_ms: 200, iterations: 1 } } }
|
{ region_key: 'fixed_brand', read_only: true, animation_json: {
|
||||||
|
outro: { preset: 'fadeOut', 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 } } }
|
||||||
] }; },
|
] }; },
|
||||||
getAnimationOverrides() { return {}; }, requestPreviewRender() {}, markEdited() {},
|
getAnimationOverrides() { return {}; }, requestPreviewRender() {}, markEdited() {},
|
||||||
setAnimationOverrides(value) { applied = value; }
|
setAnimationOverrides(value) { applied = value; }
|
||||||
});
|
});
|
||||||
ids['slide-animation-region'].value = 'hero';
|
ids['slide-animation-region'].value = 'hero';
|
||||||
ids['adjust-slide-animations'].handlers.click();
|
ids['adjust-slide-animations'].handlers.click();
|
||||||
|
fields[1].value = '999';
|
||||||
|
ids['animation-advanced-modal'].handlers.input({ target: fields[1] });
|
||||||
|
assert.equal(fields[1].value, '200');
|
||||||
assert.equal(animationPreviewButtons[2].disabled, false);
|
assert.equal(animationPreviewButtons[2].disabled, false);
|
||||||
animationPreviewButtons[2].handlers.click();
|
animationPreviewButtons[2].handlers.click();
|
||||||
assert.match(readonlyPreviewRegion.className, /animate__pulse/);
|
assert.match(readonlyPreviewRegion.className, /animate__pulse/);
|
||||||
|
assert.equal(readonlyPreviewRegion.style.values['animation-delay'], '240ms');
|
||||||
|
assert.equal(secondPreviewRegion.style.values['animation-delay'], '0ms');
|
||||||
animationPreviewButtons[2].handlers.click();
|
animationPreviewButtons[2].handlers.click();
|
||||||
|
animationPreviewButtons[1].handlers.click();
|
||||||
|
assert.equal(readonlyPreviewRegion.style.values['animation-delay'], '0ms');
|
||||||
|
animationPreviewButtons[1].handlers.click();
|
||||||
const resetRegion = ids['slide-animation-reset-region'];
|
const resetRegion = ids['slide-animation-reset-region'];
|
||||||
const resetAll = ids['slide-animation-reset-all'];
|
const resetAll = ids['slide-animation-reset-all'];
|
||||||
assert.equal(resetRegion.disabled, true);
|
assert.equal(resetRegion.disabled, true);
|
||||||
|
|||||||
+26
-1
@@ -338,7 +338,7 @@ test('template animation controls mark the form dirty', () => {
|
|||||||
|
|
||||||
assert.match(script, /animationPresetInputs[\s\S]*markTemplateFormDirty\(\);/);
|
assert.match(script, /animationPresetInputs[\s\S]*markTemplateFormDirty\(\);/);
|
||||||
assert.ok(script.includes("animationAdvancedModal.addEventListener('input'"));
|
assert.ok(script.includes("animationAdvancedModal.addEventListener('input'"));
|
||||||
assert.ok(script.includes('clampAnimationModalField(event.target);'));
|
assert.ok(script.includes('window.pulseRegionAnimationControls.clampAdvancedField(event.target);'));
|
||||||
assert.ok(script.includes("animationAdvancedModal.addEventListener('change'"));
|
assert.ok(script.includes("animationAdvancedModal.addEventListener('change'"));
|
||||||
assert.ok(script.includes("animationModalApplyButton.addEventListener('click', function () {"));
|
assert.ok(script.includes("animationModalApplyButton.addEventListener('click', function () {"));
|
||||||
assert.ok(script.includes("saveAnimationModal();"));
|
assert.ok(script.includes("saveAnimationModal();"));
|
||||||
@@ -355,6 +355,7 @@ test('template animation durations use percent in the advanced modal', () => {
|
|||||||
|
|
||||||
assert.match(modal, /signage\/templates\/animation-advanced-fields/);
|
assert.match(modal, /signage\/templates\/animation-advanced-fields/);
|
||||||
assert.match(modal, /data-animation-modal-title>Advanced animations:/);
|
assert.match(modal, /data-animation-modal-title>Advanced animations:/);
|
||||||
|
assert.match(advancedFields, /Finish before transition \(ms\).*data-animation-modal-step="outro"/);
|
||||||
assert.ok(modal.indexOf('data-animation-modal-apply') < modal.lastIndexOf('>Cancel</button>'));
|
assert.ok(modal.indexOf('data-animation-modal-apply') < modal.lastIndexOf('>Cancel</button>'));
|
||||||
assert.match(modal, /class="btn btn-warning" data-bs-dismiss="modal">Cancel<\/button>/);
|
assert.match(modal, /class="btn btn-warning" data-bs-dismiss="modal">Cancel<\/button>/);
|
||||||
assert.ok(script.includes("title.textContent = 'Advanced animations: ' + (label ? label.textContent : 'Region');"));
|
assert.ok(script.includes("title.textContent = 'Advanced animations: ' + (label ? label.textContent : 'Region');"));
|
||||||
@@ -584,4 +585,28 @@ test('template designer restores region default content and read-only state from
|
|||||||
type: 'html', value: '<div>Template-owned</div>'
|
type: 'html', value: '<div>Template-owned</div>'
|
||||||
});
|
});
|
||||||
assert.equal(fields['[data-region-read-only]'].checked, true);
|
assert.equal(fields['[data-region-read-only]'].checked, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('template animation preview applies configured delays to the CSS animation property', () => {
|
||||||
|
const script = fs.readFileSync(require.resolve('../src/web/public/js/templates/template-designer.js'), 'utf8');
|
||||||
|
assert.doesNotMatch(script, /var stagger = 90/);
|
||||||
|
assert.match(script, /var delay = mode === 'out' \? 0 : Math\.max\(0, Number\(step && step\.delay_ms\) \|\| 0\);/);
|
||||||
|
const match = script.match(/ function applyAnimateCssPreview\([^]*?\n }/);
|
||||||
|
assert.ok(match);
|
||||||
|
const context = vm.createContext({});
|
||||||
|
vm.runInContext(match[0] + '\nthis.applyAnimateCssPreview = applyAnimateCssPreview;', context);
|
||||||
|
const values = {};
|
||||||
|
const rect = {
|
||||||
|
classList: { add() {}, remove() {} },
|
||||||
|
style: {
|
||||||
|
setProperty(name, value) { values[name] = value; },
|
||||||
|
removeProperty(name) { delete values[name]; }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
context.applyAnimateCssPreview(rect, 'fadeIn', { duration_ms: 1000, delay_ms: 425 }, 'intro');
|
||||||
|
|
||||||
|
assert.equal(values['animation-delay'], '425ms');
|
||||||
|
context.applyAnimateCssPreview(rect, 'fadeOut', { duration_ms: 1000, delay_ms: 425 }, 'out');
|
||||||
|
assert.equal(values['animation-delay'], undefined);
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user