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
@@ -2,6 +2,18 @@
|
|||||||
|
|
||||||
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.16.7 - 2026-10-22
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Slide advanced animation options open over the animation editor in a separate popup matching templates. Both use an "Advanced animations: [region]" title and support outside-click dismissal without applying changes.
|
||||||
|
- Slide animation playback controls remain visible above the right-hand settings while those settings scroll, with clearer spacing below field labels.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Slide animation reset controls become available immediately after unsaved basic or advanced changes and restore template settings without requiring a slide save.
|
||||||
|
- Closing Advanced animations preserves the underlying editor's focus and scroll state, and closing all animation popups restores page scrolling.
|
||||||
|
|
||||||
## 2.16.6 - 2026-10-22
|
## 2.16.6 - 2026-10-22
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage-player",
|
"name": "pulse-signage-player",
|
||||||
"version": "2.16.6",
|
"version": "2.16.7",
|
||||||
"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.16.6",
|
"version": "2.16.7",
|
||||||
"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.16.6",
|
"version": "2.16.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.16.6",
|
"version": "2.16.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"bootstrap-icons": "1.13.1",
|
"bootstrap-icons": "1.13.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.16.6",
|
"version": "2.16.7",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage application with MySQL and media storage",
|
"description": "Pulse Signage application with MySQL and media storage",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -2439,8 +2439,25 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slide-animation-properties {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-animation-advanced-overlay {
|
||||||
|
z-index: 1075;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-animation-advanced-backdrop {
|
||||||
|
z-index: 1070;
|
||||||
|
}
|
||||||
|
|
||||||
.slide-animation-controls {
|
.slide-animation-controls {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
@@ -2449,55 +2466,18 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection {
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-animation-advanced-fields {
|
.slide-animation-controls .region-field-grid--animation label {
|
||||||
min-width: 0;
|
|
||||||
overflow-x: clip;
|
|
||||||
}
|
|
||||||
|
|
||||||
#slide-animation-advanced {
|
|
||||||
gap: 0.5rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-animation-advanced-fields .row {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-animation-advanced-fields .row > * {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-animation-advanced-fields .card,
|
|
||||||
.slide-animation-advanced-fields .card-body,
|
|
||||||
.slide-animation-advanced-fields label {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-animation-advanced-fields input,
|
|
||||||
.slide-animation-advanced-fields select {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#slide-animation-advanced[hidden] {
|
|
||||||
display: none !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-animation-preview-panel {
|
.slide-animation-preview-panel {
|
||||||
container-type: size;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#slide-animation-preview {
|
#slide-animation-preview {
|
||||||
|
container-type: size;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2506,11 +2486,11 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#slide-animation-preview .slide-preview-stage {
|
#slide-animation-preview .slide-preview-stage {
|
||||||
width: min(100%, calc(100cqh * var(--slide-layout-ratio, 1.777778)));
|
width: min(100cqw, calc(100cqh * var(--slide-layout-ratio, 1.777778)));
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-animation-preview-panel [data-slide-animation-preview][aria-pressed="true"] {
|
.slide-animation-properties [data-slide-animation-preview][aria-pressed="true"] {
|
||||||
color: var(--bs-btn-active-color, #fff);
|
color: var(--bs-btn-active-color, #fff);
|
||||||
background-color: var(--bs-info);
|
background-color: var(--bs-info);
|
||||||
border-color: var(--bs-info);
|
border-color: var(--bs-info);
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ export function createSlideAnimationEditor(options) {
|
|||||||
var openButton = document.getElementById('adjust-slide-animations');
|
var openButton = document.getElementById('adjust-slide-animations');
|
||||||
var regionSelect = document.getElementById('slide-animation-region');
|
var regionSelect = document.getElementById('slide-animation-region');
|
||||||
var card = document.getElementById('slide-animation-fields');
|
var card = document.getElementById('slide-animation-fields');
|
||||||
var advanced = document.getElementById('slide-animation-advanced');
|
var advanced = document.getElementById('animation-advanced-modal');
|
||||||
var advancedFields = advanced.querySelector('[data-animation-advanced-fields]');
|
var advancedButton = card.querySelector('[data-animation-advanced-button]');
|
||||||
|
var advancedOpen = false;
|
||||||
|
var advancedScrollBar = null;
|
||||||
var stage = options.stage;
|
var stage = options.stage;
|
||||||
var previewHost = document.getElementById('slide-animation-preview');
|
var previewHost = document.getElementById('slide-animation-preview');
|
||||||
var previewButtons = Array.prototype.slice.call(modal.querySelectorAll('[data-slide-animation-preview]'));
|
var previewButtons = Array.prototype.slice.call(modal.querySelectorAll('[data-slide-animation-preview]'));
|
||||||
@@ -31,28 +33,26 @@ export function createSlideAnimationEditor(options) {
|
|||||||
? draft[regionKey]
|
? draft[regionKey]
|
||||||
: region && region.animation_json || '{}';
|
: region && region.animation_json || '{}';
|
||||||
}
|
}
|
||||||
function getCurrentConfig() {
|
|
||||||
var config = controls.read(card);
|
|
||||||
return advanced.hidden ? config : controls.readAdvanced(advancedFields, config);
|
|
||||||
}
|
|
||||||
function syncDraft() {
|
function syncDraft() {
|
||||||
if (!active || !selectedKey()) return;
|
if (!active || !selectedKey()) return;
|
||||||
stopPreview();
|
stopPreview();
|
||||||
var config = getCurrentConfig();
|
var config = controls.read(card);
|
||||||
controls.write(card, config);
|
controls.write(card, config);
|
||||||
draft[selectedKey()] = JSON.stringify(config);
|
draft[selectedKey()] = JSON.stringify(config);
|
||||||
updatePreviewButtons();
|
updatePreviewButtons();
|
||||||
|
updateResetButtons();
|
||||||
|
}
|
||||||
|
function updateResetButtons() {
|
||||||
|
document.getElementById('slide-animation-reset-region').disabled = !Object.prototype.hasOwnProperty.call(draft, selectedKey());
|
||||||
|
document.getElementById('slide-animation-reset-all').disabled = !Object.keys(draft).length;
|
||||||
}
|
}
|
||||||
function renderRegion() {
|
function renderRegion() {
|
||||||
var key = selectedKey();
|
var key = selectedKey();
|
||||||
var region = regions().find(function (item) { return item.region_key === key; });
|
var region = regions().find(function (item) { return item.region_key === key; });
|
||||||
if (!region) return;
|
if (!region) return;
|
||||||
controls.write(card, getConfig(key));
|
controls.write(card, getConfig(key));
|
||||||
controls.populateAdvanced(advancedFields, getConfig(key));
|
|
||||||
advanced.hidden = true;
|
|
||||||
updatePreviewButtons();
|
updatePreviewButtons();
|
||||||
document.getElementById('slide-animation-reset-region').disabled = !Object.prototype.hasOwnProperty.call(draft, key);
|
updateResetButtons();
|
||||||
document.getElementById('slide-animation-reset-all').disabled = !Object.keys(draft).length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function readResponse(response) {
|
function readResponse(response) {
|
||||||
@@ -115,7 +115,7 @@ export function createSlideAnimationEditor(options) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
modal.addEventListener('hide.bs.modal', function (event) {
|
modal.addEventListener('hide.bs.modal', function (event) {
|
||||||
if (sharedSaving) event.preventDefault();
|
if (sharedSaving || advancedOpen) event.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
function getPreviewElements() {
|
function getPreviewElements() {
|
||||||
@@ -231,11 +231,52 @@ export function createSlideAnimationEditor(options) {
|
|||||||
controls.bind(card, {
|
controls.bind(card, {
|
||||||
onChange: syncDraft,
|
onChange: syncDraft,
|
||||||
onAdvanced: function () {
|
onAdvanced: function () {
|
||||||
advanced.hidden = !advanced.hidden;
|
stopPreview();
|
||||||
if (!advanced.hidden) controls.populateAdvanced(advancedFields, controls.read(card));
|
controls.populateAdvanced(advanced, controls.read(card));
|
||||||
|
var region = regions().find(function (item) { return item.region_key === selectedKey(); });
|
||||||
|
advanced.querySelector('[data-animation-modal-title]').textContent = 'Advanced animations: ' + (region.label || region.region_key);
|
||||||
|
advancedOpen = true;
|
||||||
|
advancedButton.setAttribute('aria-expanded', 'true');
|
||||||
|
advanced.classList.add('slide-animation-advanced-overlay');
|
||||||
|
window.pulseModal.getOrCreate(modal)._focustrap.deactivate();
|
||||||
|
modal.inert = true;
|
||||||
|
modal.setAttribute('aria-hidden', 'true');
|
||||||
|
modal.removeAttribute('aria-modal');
|
||||||
|
var advancedModal = window.pulseModal.getOrCreate(advanced);
|
||||||
|
advancedScrollBar = advancedModal._scrollBar;
|
||||||
|
advancedModal._scrollBar = Object.create(advancedScrollBar);
|
||||||
|
advancedModal._scrollBar.hide = function () {};
|
||||||
|
advancedModal._scrollBar.reset = function () {};
|
||||||
|
window.pulseModal.show(advanced);
|
||||||
|
var backdrops = document.querySelectorAll('.modal-backdrop');
|
||||||
|
if (backdrops.length) backdrops[backdrops.length - 1].classList.add('slide-animation-advanced-backdrop');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
advancedFields.addEventListener('change', syncDraft);
|
advanced.addEventListener('shown.bs.modal', function () {
|
||||||
|
advanced.querySelector('[data-animation-modal-field]').focus();
|
||||||
|
});
|
||||||
|
advanced.addEventListener('hidden.bs.modal', function () {
|
||||||
|
window.pulseModal.getOrCreate(advanced)._scrollBar = advancedScrollBar;
|
||||||
|
advancedScrollBar = null;
|
||||||
|
advancedOpen = false;
|
||||||
|
advancedButton.setAttribute('aria-expanded', 'false');
|
||||||
|
advanced.classList.remove('slide-animation-advanced-overlay');
|
||||||
|
modal.inert = false;
|
||||||
|
modal.removeAttribute('aria-hidden');
|
||||||
|
modal.setAttribute('aria-modal', 'true');
|
||||||
|
document.body.classList.add('modal-open');
|
||||||
|
var parentModal = window.pulseModal.getOrCreate(modal);
|
||||||
|
parentModal._focustrap.activate();
|
||||||
|
advancedButton.focus();
|
||||||
|
});
|
||||||
|
advanced.querySelector('[data-animation-modal-apply]').addEventListener('click', function () {
|
||||||
|
controls.write(card, controls.readAdvanced(advanced, controls.read(card)));
|
||||||
|
syncDraft();
|
||||||
|
window.pulseModal.hide(advanced);
|
||||||
|
});
|
||||||
|
advanced.querySelector('[data-animation-modal-reset]').addEventListener('click', function () {
|
||||||
|
controls.populateAdvanced(advanced, '{}');
|
||||||
|
});
|
||||||
document.getElementById('slide-animation-reset-region').addEventListener('click', function () {
|
document.getElementById('slide-animation-reset-region').addEventListener('click', function () {
|
||||||
stopPreview();
|
stopPreview();
|
||||||
delete draft[selectedKey()];
|
delete draft[selectedKey()];
|
||||||
|
|||||||
@@ -584,7 +584,7 @@
|
|||||||
var config = getAnimationConfig(card);
|
var config = getAnimationConfig(card);
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
title.textContent = (label ? label.textContent : 'Region') + ' animation settings';
|
title.textContent = 'Advanced animations: ' + (label ? label.textContent : 'Region');
|
||||||
}
|
}
|
||||||
|
|
||||||
window.pulseRegionAnimationControls.populateAdvanced(animationAdvancedModal, config);
|
window.pulseRegionAnimationControls.populateAdvanced(animationAdvancedModal, config);
|
||||||
|
|||||||
@@ -123,23 +123,21 @@
|
|||||||
<section class="slide-animation-preview-panel">
|
<section class="slide-animation-preview-panel">
|
||||||
<div id="slide-animation-preview"></div>
|
<div id="slide-animation-preview"></div>
|
||||||
</section>
|
</section>
|
||||||
<div class="slide-animation-controls">
|
<div class="slide-animation-properties">
|
||||||
<div class="btn-group w-100 mb-3" role="group" aria-label="Animation preview">
|
<div class="btn-group w-100" role="group" aria-label="Animation preview">
|
||||||
<button type="button" class="btn btn-outline-secondary" data-slide-animation-preview="intro" aria-pressed="false"><i class="bi bi-play-fill me-1" aria-hidden="true" data-animation-preview-icon></i>Intro</button>
|
<button type="button" class="btn btn-outline-secondary" data-slide-animation-preview="intro" aria-pressed="false"><i class="bi bi-play-fill me-1" aria-hidden="true" data-animation-preview-icon></i>Intro</button>
|
||||||
<button type="button" class="btn btn-outline-secondary" data-slide-animation-preview="outro" aria-pressed="false"><i class="bi bi-play-fill me-1" aria-hidden="true" data-animation-preview-icon></i>Outro</button>
|
<button type="button" class="btn btn-outline-secondary" data-slide-animation-preview="outro" aria-pressed="false"><i class="bi bi-play-fill me-1" aria-hidden="true" data-animation-preview-icon></i>Outro</button>
|
||||||
<button type="button" class="btn btn-outline-secondary" data-slide-animation-preview="loop" aria-pressed="false"><i class="bi bi-play-fill me-1" aria-hidden="true" data-animation-preview-icon></i>Attention</button>
|
<button type="button" class="btn btn-outline-secondary" data-slide-animation-preview="loop" aria-pressed="false"><i class="bi bi-play-fill me-1" aria-hidden="true" data-animation-preview-icon></i>Attention</button>
|
||||||
</div>
|
</div>
|
||||||
<label for="slide-animation-region" class="form-label">Region</label>
|
<div class="slide-animation-controls">
|
||||||
<select id="slide-animation-region" class="form-select mb-3"></select>
|
<label for="slide-animation-region" class="form-label">Region</label>
|
||||||
<div id="slide-animation-fields" class="d-grid gap-3">
|
<select id="slide-animation-region" class="form-select mb-3"></select>
|
||||||
{{> signage/templates/region-animation-controls}}
|
<div id="slide-animation-fields" class="d-grid gap-3">
|
||||||
<div id="slide-animation-advanced" class="d-grid gap-3" hidden>
|
{{> signage/templates/region-animation-controls}}
|
||||||
<h3 class="h6 mb-0">Advanced animation settings</h3>
|
|
||||||
<div class="slide-animation-advanced-fields" data-animation-advanced-fields>{{> signage/templates/animation-advanced-fields}}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" id="slide-animation-reset-region" class="btn btn-outline-secondary w-100 mt-3"><i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Reset region animations to template</button>
|
||||||
|
<button type="button" id="slide-animation-reset-all" class="btn btn-outline-secondary w-100 mt-2"><i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Reset all animations to template</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" id="slide-animation-reset-region" class="btn btn-outline-secondary w-100 mt-3"><i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Reset region animations to template</button>
|
|
||||||
<button type="button" id="slide-animation-reset-all" class="btn btn-outline-secondary w-100 mt-2"><i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Reset all animations to template</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
@@ -155,6 +153,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{{/modal-shell}}
|
{{/modal-shell}}
|
||||||
|
|
||||||
|
{{> signage/templates/animation-advanced-modal}}
|
||||||
|
|
||||||
{{#> modal-shell modalId="slide-image-cropper-modal" modalLabelId="slide-image-cropper-modal-label" modalDialogClass="modal-dialog-centered modal-xl modal-dialog-scrollable" modalBackdropStatic=true modalKeyboardDisabled=true}}
|
{{#> modal-shell modalId="slide-image-cropper-modal" modalLabelId="slide-image-cropper-modal-label" modalDialogClass="modal-dialog-centered modal-xl modal-dialog-scrollable" modalBackdropStatic=true modalKeyboardDisabled=true}}
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
{{#> modal-shell modalId="animation-advanced-modal" modalLabelId="animation-advanced-modal-label" modalDialogClass="modal-dialog-centered modal-xl modal-dialog-scrollable" modalBackdropStatic=true modalKeyboardDisabled=true}}
|
{{#> modal-shell modalId="animation-advanced-modal" modalLabelId="animation-advanced-modal-label" modalDialogClass="modal-dialog-centered modal-xl modal-dialog-scrollable" modalKeyboardDisabled=true}}
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="modal-title fs-5" id="animation-advanced-modal-label" data-animation-modal-title>Advanced animation settings</h2>
|
<h2 class="modal-title fs-5" id="animation-advanced-modal-label" data-animation-modal-title>Advanced animations:</h2>
|
||||||
<p class="text-body-secondary mb-0">Fine-tune the selected region animation JSON without editing raw data.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+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');
|
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];
|
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, /Advanced animations:/);
|
||||||
assert.match(animationModal, /id="slide-animation-advanced"[^>]*hidden/);
|
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 region animations to template/);
|
||||||
assert.match(animationModal, /Reset all 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"'));
|
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*\{[^}]*grid-template-columns:\s*minmax\(0,\s*1fr\)\s+21rem;/);
|
||||||
assert.match(css, /\.slide-animation-body\s*\{[^}]*gap:\s*1\.25rem;/);
|
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, /\.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-panel\s*\{[^}]*min-height:\s*0;/);
|
||||||
assert.match(css, /#slide-animation-preview\s*\{[^}]*height:\s*100%;/);
|
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 \.slide-preview-stage\s*\{[^}]*width:\s*min\(100%,\s*calc\(100cqh\s*\*\s*var\(--slide-layout-ratio,/);
|
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\);/);
|
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');
|
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-controls \.region-field-grid--animation label\s*\{[^}]*display:\s*grid;[^}]*gap:\s*0\.5rem;/);
|
||||||
assert.match(css, /\.slide-animation-advanced-fields \.row\s*\{[^}]*gap:\s*0\.5rem;/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shared animation controls round-trip advanced settings with template normalization', function () {
|
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);
|
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) {
|
function createSharedLayoutRouteHarness(failUpdate, allowSlideUpdate) {
|
||||||
const registerContentRoutes = require('../src/web/routes/admin/content');
|
const registerContentRoutes = require('../src/web/routes/admin/content');
|
||||||
const handlers = {};
|
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');
|
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, /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, /Duration %/);
|
||||||
assert.match(advancedFields, /data-animation-modal-field="duration_ms"/);
|
assert.match(advancedFields, /data-animation-modal-field="duration_ms"/);
|
||||||
assert.match(advancedFields, /max="200"/);
|
assert.match(advancedFields, /max="200"/);
|
||||||
|
|||||||
Reference in new issue
Block a user