Release 2.8.0
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m49s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 32s

This commit is contained in:
2026-08-16 17:15:31 +01:00
parent 1bdc122995
commit 203d0bfc01
105 changed files with 4185 additions and 677 deletions
+15
View File
@@ -441,6 +441,21 @@
});
}
try {
form.dispatchEvent(new CustomEvent('web-async-save:success', {
bubbles: true,
detail: {
form: form,
response: response,
responseText: responseText,
responseDocument: responseDocument,
submitterValue: submitterValue
}
}));
} catch (_error) {
// Ignore event dispatch failures and continue the save flow.
}
clearFormDirty(form);
if (submitterValue === 'close' || submitterValue === 'new') {
@@ -39,6 +39,63 @@ function setAnnouncementScreenSelection(isSelected) {
});
}
function getAnnouncementActionButtonState() {
var actionForm = document.getElementById('announcement-action-form');
var visibleButton = document.querySelector('button[form="announcement-action-form"]');
var actionPath = actionForm ? String(actionForm.getAttribute('action') || '').trim() : '';
var isPlay = /\/play$/.test(actionPath);
var isActive = !isPlay;
var selectedScreenCount = document.querySelectorAll('input[name="screen_ids[]"]:checked').length;
return {
visibleButton: visibleButton,
actionForm: actionForm,
isActive: isActive,
hasTargets: selectedScreenCount > 0,
actionDisabled: !isActive && selectedScreenCount === 0,
actionLabel: isActive ? 'Stop' : 'Play',
actionIcon: isActive ? 'bi-stop-fill' : 'bi-play-fill',
actionClassName: !isActive && selectedScreenCount === 0
? 'btn-outline-info'
: (isActive ? 'btn-outline-warning' : 'btn-info'),
actionDisabledTitle: !isActive && selectedScreenCount === 0
? 'Select at least one screen group to play this announcement.'
: '',
actionConfirmMessage: isActive
? 'Stop this announcement on the selected screens now?'
: 'Send this announcement to the selected screens now?'
};
}
function updateAnnouncementActionButtonState() {
var state = getAnnouncementActionButtonState();
if (!state.visibleButton) {
return;
}
state.visibleButton.className = state.visibleButton.className.replace(/btn-outline-(info|warning|success)|btn-info/g, state.actionClassName);
state.visibleButton.disabled = state.actionDisabled;
state.visibleButton.setAttribute('aria-disabled', state.actionDisabled ? 'true' : 'false');
state.visibleButton.setAttribute('title', state.actionDisabled ? state.actionDisabledTitle : state.actionConfirmMessage);
state.visibleButton.setAttribute('aria-label', state.actionLabel);
var icon = state.visibleButton.querySelector('i.bi');
if (icon) {
icon.className = 'bi ' + state.actionIcon + ' me-1';
}
var label = state.visibleButton.childNodes.length > 1 ? state.visibleButton.childNodes[state.visibleButton.childNodes.length - 1] : null;
if (label && label.nodeType === Node.TEXT_NODE) {
label.textContent = state.actionLabel;
} else {
state.visibleButton.textContent = state.actionLabel;
}
if (state.actionForm) {
state.actionForm.setAttribute('data-confirm-message', state.actionConfirmMessage);
}
}
function positionAnnouncementTypePicker() {
var picker = document.querySelector('[data-announcement-type-picker]');
var toggle = document.querySelector('[data-announcement-type-picker-toggle]');
@@ -141,125 +198,6 @@ function setAnnouncementTypeValue(typeKey) {
updateAnnouncementTypePickerSelection();
}
function updateAnnouncementIconPreview() {
var iconSelect = document.getElementById('announcement-icon');
var preview = document.querySelector('[data-announcement-icon-preview]');
if (!iconSelect || !preview) {
return;
}
var selectedOption = iconSelect.options[iconSelect.selectedIndex] || null;
var iconKey = selectedOption ? String(selectedOption.value || '').trim().toLowerCase() : '';
var label = selectedOption ? String(selectedOption.textContent || selectedOption.label || iconKey).trim() : iconKey;
preview.className = 'announcement-icon-preview';
preview.innerHTML = '<i class="bi bi-' + iconKey + '" aria-hidden="true"></i>';
preview.setAttribute('aria-label', label);
preview.setAttribute('title', label);
}
function positionAnnouncementIconPicker() {
var picker = document.querySelector('[data-announcement-icon-picker]');
var toggle = document.querySelector('[data-announcement-icon-picker-toggle]');
var shell = document.querySelector('[data-announcement-icon-picker-shell]');
if (!picker || picker.hidden || !toggle || !shell) {
return;
}
var padding = 8;
var toggleRect = toggle.getBoundingClientRect();
var menuRect = picker.getBoundingClientRect();
var viewportHeight = window.innerHeight || document.documentElement.clientHeight || toggleRect.bottom;
var placementAbove = false;
var spaceBelow = viewportHeight - toggleRect.bottom - padding;
var spaceAbove = toggleRect.top - padding;
if (menuRect.height > spaceBelow && spaceAbove > spaceBelow) {
placementAbove = true;
}
picker.classList.toggle('is-open-above', placementAbove);
}
function closeAnnouncementIconPicker() {
var picker = document.querySelector('[data-announcement-icon-picker]');
var toggle = document.querySelector('[data-announcement-icon-picker-toggle]');
if (!picker) {
return;
}
picker.hidden = true;
picker.classList.remove('is-open-above');
if (toggle) {
toggle.setAttribute('aria-expanded', 'false');
}
}
function openAnnouncementIconPicker() {
var picker = document.querySelector('[data-announcement-icon-picker]');
var toggle = document.querySelector('[data-announcement-icon-picker-toggle]');
if (!picker) {
return;
}
picker.hidden = false;
if (toggle) {
toggle.setAttribute('aria-expanded', 'true');
}
if (typeof window !== 'undefined' && window.requestAnimationFrame) {
window.requestAnimationFrame(positionAnnouncementIconPicker);
} else {
positionAnnouncementIconPicker();
}
}
function updateAnnouncementIconPickerSelection() {
var iconSelect = document.getElementById('announcement-icon');
var previewButton = document.querySelector('[data-announcement-icon-picker-toggle]');
var picker = document.querySelector('[data-announcement-icon-picker]');
if (!iconSelect || !previewButton || !picker) {
return;
}
var selectedOption = iconSelect.options[iconSelect.selectedIndex] || null;
var selectedValue = selectedOption ? String(selectedOption.value || '').trim().toLowerCase() : '';
var label = selectedOption ? String(selectedOption.textContent || selectedOption.label || selectedValue).trim() : selectedValue;
var icon = previewButton.querySelector('[data-announcement-icon-picker-icon]');
var text = previewButton.querySelector('[data-announcement-icon-picker-label]');
previewButton.setAttribute('aria-label', label);
previewButton.setAttribute('title', label);
if (icon) {
icon.className = 'bi bi-' + selectedValue;
}
if (text) {
text.textContent = label;
}
picker.querySelectorAll('[data-announcement-icon-option]').forEach(function (button) {
var isSelected = String(button.getAttribute('data-icon-key') || '').trim().toLowerCase() === selectedValue;
button.classList.toggle('is-selected', isSelected);
button.setAttribute('aria-pressed', isSelected ? 'true' : 'false');
});
}
function setAnnouncementIconValue(iconKey) {
var iconSelect = document.getElementById('announcement-icon');
if (!iconSelect) {
return;
}
var normalized = String(iconKey || '').trim().toLowerCase();
if (!normalized) {
return;
}
iconSelect.value = normalized;
updateAnnouncementIconPreview();
updateAnnouncementIconPickerSelection();
}
function initAnnouncementForm() {
var typeInput = document.getElementById('announcement-type');
var typePickerToggle = document.querySelector('[data-announcement-type-picker-toggle]');
@@ -270,10 +208,6 @@ function initAnnouncementForm() {
var colorPicker = document.querySelector('[data-announcement-color-picker-shell]');
var screenSelectAll = document.querySelector('[data-announcement-screen-select-all]');
var screenSelectNone = document.querySelector('[data-announcement-screen-select-none]');
var iconSelect = document.getElementById('announcement-icon');
var iconPickerToggle = document.querySelector('[data-announcement-icon-picker-toggle]');
var iconPicker = document.querySelector('[data-announcement-icon-picker]');
var iconPickerClose = document.querySelector('[data-announcement-icon-picker-close]');
if (typeInput) {
updateAnnouncementTypePickerSelection();
@@ -347,44 +281,16 @@ function initAnnouncementForm() {
});
}
if (iconSelect) {
iconSelect.addEventListener('change', updateAnnouncementIconPreview);
updateAnnouncementIconPreview();
}
document.addEventListener('web-async-save:success', function (event) {
var detail = event && event.detail ? event.detail : null;
var form = detail && detail.form ? detail.form : null;
if (!form || form.id !== 'announcement-form') {
return;
}
if (iconPickerToggle) {
iconPickerToggle.addEventListener('click', function (event) {
event.preventDefault();
var isExpanded = iconPicker && !iconPicker.hidden;
if (isExpanded) {
closeAnnouncementIconPicker();
} else {
openAnnouncementIconPicker();
}
});
}
updateAnnouncementActionButtonState();
});
if (iconPickerClose) {
iconPickerClose.addEventListener('click', function (event) {
event.preventDefault();
closeAnnouncementIconPicker();
});
}
if (iconPicker) {
iconPicker.addEventListener('click', function (event) {
var button = event.target && event.target.closest ? event.target.closest('[data-announcement-icon-option]') : null;
if (!button) {
return;
}
event.preventDefault();
setAnnouncementIconValue(button.getAttribute('data-icon-key'));
closeAnnouncementIconPicker();
});
}
window.addEventListener('resize', positionAnnouncementIconPicker);
window.addEventListener('resize', positionAnnouncementTypePicker);
document.addEventListener('click', function (event) {
@@ -396,20 +302,9 @@ function initAnnouncementForm() {
}
}
var picker = document.querySelector('[data-announcement-icon-picker]');
var toggle = document.querySelector('[data-announcement-icon-picker-toggle]');
if (!picker || picker.hidden) {
return;
}
if (picker.contains(event.target) || (toggle && toggle.contains(event.target))) {
return;
}
closeAnnouncementIconPicker();
});
updateAnnouncementIconPickerSelection();
updateAnnouncementActionButtonState();
}
if (typeof document !== 'undefined') {
+111
View File
@@ -0,0 +1,111 @@
(function () {
'use strict';
function initIconPicker(root) {
var select = root.querySelector('select');
var toggle = root.querySelector('[data-icon-picker-toggle]');
var menu = root.querySelector('[data-icon-picker-menu]');
var preview = root.querySelector('[data-icon-picker-preview]');
var label = root.querySelector('[data-icon-picker-label]');
var search = root.querySelector('[data-icon-picker-search]');
var close = root.querySelector('[data-icon-picker-close]');
var grid = root.querySelector('[data-icon-picker-grid]');
var empty = root.querySelector('[data-icon-picker-empty]');
var initialOptions = Array.prototype.slice.call(root.querySelectorAll('[data-icon-picker-option]'));
if (!select || !toggle || !menu || !grid) {
return;
}
function catalogOptions() {
return Array.prototype.slice.call(select.options).map(function (option) {
return { value: option.value, label: option.textContent || option.label || option.value };
});
}
function createOption(option, selectedValue) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'announcement-icon-picker__option';
button.setAttribute('data-icon-picker-option', '');
button.setAttribute('data-icon-key', option.value);
button.setAttribute('data-icon-label', option.label);
button.setAttribute('aria-pressed', option.value === selectedValue ? 'true' : 'false');
button.title = option.label;
button.innerHTML = '<i class="bi bi-' + option.value + '" aria-hidden="true"></i><span class="visually-hidden">' + option.label + '</span>';
return button;
}
function getOptionLimit() {
var firstOption = grid.querySelector('[data-icon-picker-option]');
var gridStyle = window.getComputedStyle(grid);
var gap = parseFloat(gridStyle.columnGap || gridStyle.gap || '0') || 0;
var optionWidth = firstOption ? firstOption.getBoundingClientRect().width : 0;
var columns = optionWidth && grid.clientWidth
? Math.max(1, Math.floor((grid.clientWidth + gap) / (optionWidth + gap)))
: 4;
return columns * 6;
}
function updatePreview() {
var option = select.options[select.selectedIndex];
var value = option ? option.value : '';
if (preview) {
preview.className = 'announcement-icon-picker__toggle-icon bi bi-' + value;
}
if (label) {
label.textContent = option ? option.textContent : 'Select an icon';
}
root.querySelectorAll('[data-icon-picker-option]').forEach(function (optionButton) {
var selected = optionButton.getAttribute('data-icon-key') === value;
optionButton.classList.toggle('is-selected', selected);
optionButton.setAttribute('aria-pressed', selected ? 'true' : 'false');
});
}
function render(query) {
var normalizedQuery = String(query || '').trim().toLowerCase();
var selectedValue = String(select.value || '').trim();
var options = normalizedQuery
? catalogOptions().filter(function (option) { return (option.value + ' ' + option.label).toLowerCase().indexOf(normalizedQuery) !== -1; })
: initialOptions.map(function (option) { return { value: option.getAttribute('data-icon-key') || '', label: option.getAttribute('data-icon-label') || '' }; });
var visibleOptions = options.slice(0, getOptionLimit());
grid.innerHTML = '';
visibleOptions.forEach(function (option) { grid.appendChild(createOption(option, selectedValue)); });
if (empty) empty.hidden = options.length > 0;
updatePreview();
}
function closeMenu() {
menu.hidden = true;
toggle.setAttribute('aria-expanded', 'false');
}
toggle.addEventListener('click', function (event) {
event.preventDefault();
menu.hidden = !menu.hidden;
toggle.setAttribute('aria-expanded', menu.hidden ? 'false' : 'true');
if (!menu.hidden) {
render(search ? search.value : '');
if (search) search.focus();
}
});
if (close) close.addEventListener('click', function (event) { event.preventDefault(); closeMenu(); });
grid.addEventListener('click', function (event) {
var option = event.target.closest ? event.target.closest('[data-icon-picker-option]') : null;
if (!option) return;
select.value = option.getAttribute('data-icon-key') || '';
select.dispatchEvent(new Event('change', { bubbles: true }));
closeMenu();
});
if (search) search.addEventListener('input', function () { render(search.value); });
select.addEventListener('change', updatePreview);
document.addEventListener('click', function (event) {
if (!menu.hidden && !root.contains(event.target)) closeMenu();
});
updatePreview();
}
window.PulseIconPicker = { init: initIconPicker };
document.querySelectorAll('[data-icon-picker]').forEach(initIconPicker);
}());
@@ -1665,6 +1665,7 @@
var slidePickerSlides = [];
var lastDurationPointerDown = null;
var lastDurationPointerUp = null;
var defaultSlideDuration = Number(tbody.getAttribute('data-default-slide-duration')) || 10;
if (!tbody || !form) {
return;
@@ -1961,8 +1962,8 @@
videoDurationSeconds: slide.videoDurationSeconds,
disableAudio: slide.disableAudio,
title: slide.title || 'Slide',
duration_seconds: 10,
durationSeconds: 10,
duration_seconds: defaultSlideDuration,
durationSeconds: defaultSlideDuration,
scheduleRules: [],
summary: 'Always visible'
});
+8
View File
@@ -105,6 +105,13 @@
return document.querySelector('[data-permission-row-id="' + targetId + '"]');
}
function markPermissionFormDirty(control) {
var form = control && (control.form || (typeof control.closest === 'function' ? control.closest('form') : null));
if (form && form.dataset) {
form.dataset.dirty = 'true';
}
}
function handleRowChange(event) {
var checkbox = event.target && event.target.matches ? event.target : null;
if (!checkbox || checkbox.tagName !== 'INPUT' || checkbox.type !== 'checkbox') {
@@ -162,6 +169,7 @@
if (control.hasAttribute('data-permission-row-toggle')) {
toggleRowCheckboxes(findPermissionRow(targetId));
markPermissionFormDirty(control);
}
});
+27 -6
View File
@@ -76,17 +76,31 @@
function buildEditorCardContext(context) {
var defaultConfig = context && context.uploadConfig ? context.uploadConfig : {};
var configuredTypes = Array.isArray(context && context.uploadMimeTypes)
? context.uploadMimeTypes.filter(function (mimeType) { return String(mimeType || '').indexOf('image/') === 0; })
: [];
var accept = configuredTypes.length
? configuredTypes
: (Array.isArray(defaultConfig.accept) ? defaultConfig.accept : ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml']);
var acceptLabels = accept.map(function (mimeType) {
var subtype = String(mimeType).split('/')[1] || '';
return subtype === 'jpeg' ? 'JPG' : subtype === 'svg+xml' ? 'SVG' : subtype.toUpperCase();
});
var acceptLabel = acceptLabels.length > 1
? acceptLabels.slice(0, -1).join(', ') + ' or ' + acceptLabels[acceptLabels.length - 1]
: (acceptLabels[0] || 'supported image');
return {
region: context.region,
current: String(context.current || ''),
regionRatio: String(context.regionRatio || '1:1'),
uploadConfig: {
accept: Array.isArray(defaultConfig.accept) ? defaultConfig.accept : ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml'],
limitBytes: defaultConfig.limitBytes,
accept: accept,
limitBytes: defaultConfig.limitBytes || context.uploadMaxBytes,
limitLabel: defaultConfig.limitLabel || context.uploadMaxLabel || '100 MB',
helpText: defaultConfig.helpText || 'PNG, JPG, GIF, WebP, or SVG'
helpText: defaultConfig.helpText || acceptLabel
},
uploadMaxLabel: context.uploadMaxLabel || '100 MB'
uploadMaxLabel: context.uploadMaxLabel || '100 MB',
uploadMimeTypes: context.uploadMimeTypes || []
};
}
@@ -108,10 +122,17 @@
var uploadConfig = context.uploadConfig || {};
var accept = uploadConfig.accept || ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml'];
var acceptLabels = accept.map(function (mimeType) {
var subtype = String(mimeType).split('/')[1] || '';
return subtype === 'jpeg' ? 'JPG' : subtype === 'svg+xml' ? 'SVG' : subtype.toUpperCase();
});
var acceptLabel = acceptLabels.length > 1
? acceptLabels.slice(0, -1).join(', ') + (acceptLabels.length > 2 ? ', or ' : ' or ') + acceptLabels[acceptLabels.length - 1]
: (acceptLabels[0] || 'supported image');
var limitBytes = Number(uploadConfig.limitBytes || 100 * 1024 * 1024);
if (!utils.fileMatchesAccept || !utils.fileMatchesAccept(context.file, accept)) {
showUploadWarning('This image region accepts PNG, JPG, GIF, WebP, or SVG files.');
showUploadWarning('This image region accepts ' + acceptLabel + ' files.');
return false;
}
@@ -133,7 +154,7 @@
return {
accept: ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml'],
limitBytes: defaultConfig.limitBytes,
limitBytes: defaultConfig.limitBytes || context.uploadMaxBytes,
limitLabel: defaultConfig.limitLabel,
helpText: 'PNG, JPG, GIF, WebP, or SVG'
};
+25 -6
View File
@@ -82,17 +82,30 @@
function buildEditorCardContext(context) {
var defaultConfig = context && context.uploadConfig ? context.uploadConfig : {};
var configuredTypes = Array.isArray(context && context.uploadMimeTypes)
? context.uploadMimeTypes.filter(function (mimeType) { return String(mimeType || '').indexOf('video/') === 0; })
: [];
var accept = configuredTypes.length
? configuredTypes
: (Array.isArray(defaultConfig.accept) ? defaultConfig.accept : ['video/mp4', 'video/webm', 'video/ogg']);
var acceptLabels = accept.map(function (mimeType) {
return (String(mimeType).split('/')[1] || '').toUpperCase();
});
var acceptLabel = acceptLabels.length > 1
? acceptLabels.slice(0, -1).join(', ') + ' or ' + acceptLabels[acceptLabels.length - 1]
: (acceptLabels[0] || 'supported video');
return {
region: context.region,
current: String(context.current || ''),
currentDuration: String(context.currentDuration || ''),
uploadConfig: {
accept: Array.isArray(defaultConfig.accept) ? defaultConfig.accept : ['video/mp4', 'video/webm', 'video/ogg'],
limitBytes: defaultConfig.limitBytes,
accept: accept,
limitBytes: defaultConfig.limitBytes || context.uploadVideoMaxBytes,
limitLabel: defaultConfig.limitLabel || context.uploadVideoMaxLabel || '1 GB',
helpText: defaultConfig.helpText || 'MP4, WebM, or Ogg'
helpText: defaultConfig.helpText || acceptLabel
},
uploadVideoMaxLabel: context.uploadVideoMaxLabel || '1 GB'
uploadVideoMaxLabel: context.uploadVideoMaxLabel || '1 GB',
uploadMimeTypes: context.uploadMimeTypes || []
};
}
@@ -114,10 +127,16 @@
var uploadConfig = context.uploadConfig || {};
var accept = uploadConfig.accept || ['video/mp4', 'video/webm', 'video/ogg'];
var acceptLabels = accept.map(function (mimeType) {
return (String(mimeType).split('/')[1] || '').toUpperCase();
});
var acceptLabel = acceptLabels.length > 1
? acceptLabels.slice(0, -1).join(', ') + (acceptLabels.length > 2 ? ', or ' : ' or ') + acceptLabels[acceptLabels.length - 1]
: (acceptLabels[0] || 'supported video');
var limitBytes = Number(uploadConfig.limitBytes || 1024 * 1024 * 1024);
if (!utils.fileMatchesAccept || !utils.fileMatchesAccept(context.file, accept)) {
showUploadWarning('This video region accepts MP4, WebM, or Ogg files.');
showUploadWarning('This video region accepts ' + acceptLabel + ' files.');
return false;
}
@@ -163,7 +182,7 @@
return {
accept: ['video/mp4', 'video/webm', 'video/ogg'],
limitBytes: defaultConfig.limitBytes,
limitBytes: defaultConfig.limitBytes || context.uploadVideoMaxBytes,
limitLabel: defaultConfig.limitLabel,
helpText: 'MP4, WebM, or Ogg'
};
+392
View File
@@ -0,0 +1,392 @@
(function () {
'use strict';
function initIconSuggestions() {
var searchInput = document.querySelector('[data-icon-suggestions-search]');
var countNode = document.querySelector('[data-icon-suggestions-count]');
var selectedList = document.querySelector('[data-selected-icon-list]');
var noSelectedNode = document.querySelector('[data-no-selected-icons]');
var addButton = document.querySelector('[data-add-icon-button]');
var iconForm = document.querySelector('[data-settings-form="icons"]');
var options = Array.prototype.slice.call(document.querySelectorAll('[data-icon-suggestion-option]'));
var maxSelected = 48;
var draggedItem = null;
var initialOrder = [];
if (!searchInput || !countNode || !selectedList || !options.length) {
return;
}
function getOptionByKey(key) {
return options.find(function (option) {
var checkbox = option.querySelector('[data-icon-picker-checkbox]');
return checkbox && checkbox.value === key;
}) || null;
}
function getCheckedKeys() {
return options.filter(function (option) {
var checkbox = option.querySelector('[data-icon-picker-checkbox]');
return checkbox && checkbox.checked;
}).map(function (option) {
return option.querySelector('[data-icon-picker-checkbox]').value;
});
}
function getSelectedOrder() {
return Array.prototype.slice.call(selectedList.querySelectorAll('[data-selected-icon-item]')).map(function (item) {
return item.getAttribute('data-icon-key');
});
}
function createSelectedItem(key) {
var option = getOptionByKey(key);
var checkbox = option && option.querySelector('[data-icon-picker-checkbox]');
if (!option || !checkbox) {
return null;
}
var item = document.createElement('div');
var content = document.createElement('div');
var dragButton = document.createElement('button');
var dragIcon = document.createElement('i');
var icon = document.createElement('i');
var label = document.createElement('span');
var hiddenInput = document.createElement('input');
var removeButton = document.createElement('button');
var removeIcon = document.createElement('i');
var iconLabel = String(option.getAttribute('data-icon-label') || key);
item.className = 'col-12 col-sm-6 col-xl-3';
content.className = 'd-flex align-items-center gap-2 border rounded p-2 h-100';
item.draggable = true;
item.setAttribute('data-selected-icon-item', '');
item.setAttribute('data-icon-key', key);
dragButton.type = 'button';
dragButton.className = 'btn btn-link text-body-secondary p-1';
dragButton.setAttribute('data-drag-icon', '');
dragButton.title = 'Drag to reorder';
dragButton.setAttribute('aria-label', 'Drag ' + iconLabel + ' to reorder');
dragIcon.className = 'bi bi-grip-vertical';
dragIcon.setAttribute('aria-hidden', 'true');
dragButton.appendChild(dragIcon);
icon.className = 'bi bi-' + key + ' fs-5 text-primary';
icon.setAttribute('aria-hidden', 'true');
label.className = 'flex-grow-1';
label.textContent = iconLabel;
hiddenInput.type = 'hidden';
hiddenInput.name = 'suggested_icons[]';
hiddenInput.value = key;
hiddenInput.setAttribute('data-ordered-icon-input', '');
removeButton.type = 'button';
removeButton.className = 'btn btn-link text-danger p-1';
removeButton.setAttribute('data-remove-icon', '');
removeButton.title = 'Remove ' + iconLabel;
removeButton.setAttribute('aria-label', 'Remove ' + iconLabel);
removeIcon.className = 'bi bi-x-lg';
removeIcon.setAttribute('aria-hidden', 'true');
removeButton.appendChild(removeIcon);
content.appendChild(dragButton);
content.appendChild(icon);
content.appendChild(label);
content.appendChild(hiddenInput);
content.appendChild(removeButton);
item.appendChild(content);
return item;
}
function renderSelectedItems(order) {
selectedList.querySelectorAll('[data-selected-icon-item]').forEach(function (item) {
item.remove();
});
order.forEach(function (key) {
var item = createSelectedItem(key);
if (item) {
selectedList.insertBefore(item, noSelectedNode);
}
});
}
function syncSelectionState() {
var checkedKeys = getCheckedKeys();
var checkedSet = new Set(checkedKeys);
var currentOrder = getSelectedOrder().filter(function (key) {
return checkedSet.has(key);
});
checkedKeys.forEach(function (key) {
if (currentOrder.indexOf(key) === -1) {
currentOrder.push(key);
}
});
renderSelectedItems(currentOrder);
countNode.textContent = String(currentOrder.length);
if (noSelectedNode) {
noSelectedNode.hidden = currentOrder.length > 0;
}
if (addButton) {
addButton.disabled = currentOrder.length >= maxSelected;
addButton.setAttribute('aria-disabled', addButton.disabled ? 'true' : 'false');
}
options.forEach(function (option) {
var checkbox = option.querySelector('[data-icon-picker-checkbox]');
if (checkbox) {
checkbox.disabled = !checkbox.checked && currentOrder.length >= maxSelected;
}
});
}
function resetIconForm() {
var initialSet = new Set(initialOrder);
options.forEach(function (option) {
var checkbox = option.querySelector('[data-icon-picker-checkbox]');
if (checkbox) {
checkbox.checked = initialSet.has(checkbox.value);
}
});
renderSelectedItems(initialOrder.slice());
syncSelectionState();
if (iconForm) {
iconForm.dataset.dirty = 'false';
}
}
function markIconFormDirty() {
if (iconForm) {
iconForm.dataset.dirty = 'true';
}
}
function filterOptions() {
var query = String(searchInput.value || '').trim().toLowerCase();
options.forEach(function (option) {
var label = String(option.getAttribute('data-icon-label') || '').toLowerCase();
option.hidden = Boolean(query && label.indexOf(query) === -1);
});
}
options.forEach(function (option) {
var checkbox = option.querySelector('[data-icon-picker-checkbox]');
if (checkbox) {
checkbox.addEventListener('change', function () {
markIconFormDirty();
syncSelectionState();
});
}
});
selectedList.addEventListener('click', function (event) {
var removeButton = event.target.closest ? event.target.closest('[data-remove-icon]') : null;
if (!removeButton) {
return;
}
var item = removeButton.closest('[data-selected-icon-item]');
var key = item && item.getAttribute('data-icon-key');
var option = getOptionByKey(key);
var checkbox = option && option.querySelector('[data-icon-picker-checkbox]');
if (checkbox) {
checkbox.checked = false;
}
markIconFormDirty();
syncSelectionState();
});
selectedList.addEventListener('dragstart', function (event) {
var item = event.target.closest ? event.target.closest('[data-selected-icon-item]') : null;
if (!item) {
return;
}
draggedItem = item;
item.classList.add('opacity-50');
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', item.getAttribute('data-icon-key') || '');
});
selectedList.addEventListener('dragover', function (event) {
if (!draggedItem) {
return;
}
event.preventDefault();
var target = event.target.closest ? event.target.closest('[data-selected-icon-item]') : null;
if (!target || target === draggedItem) {
return;
}
var bounds = target.getBoundingClientRect();
var insertBefore = event.clientY < bounds.top + bounds.height / 2;
selectedList.insertBefore(draggedItem, insertBefore ? target : target.nextSibling);
});
selectedList.addEventListener('dragend', function () {
if (!draggedItem) {
return;
}
draggedItem.classList.remove('opacity-50');
draggedItem = null;
markIconFormDirty();
syncSelectionState();
});
searchInput.addEventListener('input', filterOptions);
syncSelectionState();
initialOrder = getSelectedOrder();
document.addEventListener('settings:reset', resetIconForm);
}
function initDefaultAnnouncementIconPicker() {
return;
var shell = document.querySelector('[data-settings-default-icon-picker]');
if (!shell) return;
var select = shell.querySelector('select');
var toggle = shell.querySelector('[data-settings-default-icon-toggle]');
var menu = shell.querySelector('[data-settings-default-icon-menu]');
var preview = shell.querySelector('[data-settings-default-icon-preview]');
var label = shell.querySelector('[data-settings-default-icon-label]');
var search = shell.querySelector('[data-settings-default-icon-search]');
var empty = shell.querySelector('[data-settings-default-icon-empty]');
var options = Array.prototype.slice.call(shell.querySelectorAll('[data-settings-default-icon-option]'));
function getCatalogOptions() {
return Array.prototype.slice.call(select.options).map(function (option) {
return { value: option.value, label: option.textContent || option.label || option.value };
});
}
function createOption(option, selectedValue) {
var item = document.createElement('button');
item.type = 'button';
item.className = 'announcement-icon-picker__option';
item.setAttribute('data-settings-default-icon-option', '');
item.setAttribute('data-icon-key', option.value);
item.setAttribute('data-icon-label', option.label);
item.setAttribute('data-icon-search-terms', option.value + ' ' + option.label);
item.setAttribute('aria-pressed', option.value === selectedValue ? 'true' : 'false');
item.title = option.label;
item.innerHTML = '<i class="bi bi-' + option.value + '" aria-hidden="true"></i><span class="visually-hidden">' + option.label + '</span>';
return item;
}
function renderOptions(query) {
var normalizedQuery = String(query || '').trim().toLowerCase();
var selectedValue = String(select.value || '').trim();
var sourceOptions = normalizedQuery ? getCatalogOptions().filter(function (option) {
return (option.value + ' ' + option.label).toLowerCase().indexOf(normalizedQuery) !== -1;
}) : options.map(function (option) {
return { value: option.getAttribute('data-icon-key') || '', label: option.getAttribute('data-icon-label') || '' };
});
var grid = shell.querySelector('[data-settings-default-icon-grid]');
grid.innerHTML = '';
sourceOptions.forEach(function (option) {
grid.appendChild(createOption(option, selectedValue));
});
options = Array.prototype.slice.call(grid.querySelectorAll('[data-settings-default-icon-option]'));
options.forEach(bindOption);
empty.hidden = Boolean(sourceOptions.length);
}
function bindOption(item) {
item.addEventListener('click', function () {
select.value = item.getAttribute('data-icon-key') || '';
select.dispatchEvent(new Event('change', { bubbles: true }));
menu.hidden = true;
toggle.setAttribute('aria-expanded', 'false');
});
}
function render() {
var option = select.options[select.selectedIndex];
var key = option ? option.value : '';
var text = option ? option.textContent : 'Select an icon';
preview.className = 'announcement-icon-picker__toggle-icon bi bi-' + key;
if (label) label.textContent = text;
options.forEach(function (item) {
item.classList.toggle('is-selected', item.getAttribute('data-icon-key') === key);
});
}
toggle.addEventListener('click', function () {
menu.hidden = !menu.hidden;
toggle.setAttribute('aria-expanded', menu.hidden ? 'false' : 'true');
if (!menu.hidden && search) search.focus();
});
shell.querySelector('[data-settings-default-icon-close]').addEventListener('click', function () {
menu.hidden = true;
toggle.setAttribute('aria-expanded', 'false');
});
options.forEach(bindOption);
if (search) search.addEventListener('input', function () {
renderOptions(search.value);
});
select.addEventListener('change', render);
render();
}
function initSettingsSectionNavigation() {
var links = Array.prototype.slice.call(document.querySelectorAll('[data-settings-section-link]'));
var sections = Array.prototype.slice.call(document.querySelectorAll('[data-settings-section]'));
if (!links.length || !sections.length) {
return;
}
function setActiveSection(sectionId) {
links.forEach(function (link) {
var isActive = link.getAttribute('href') === '#' + sectionId;
link.classList.toggle('active', isActive);
link.setAttribute('aria-current', isActive ? 'page' : 'false');
});
sections.forEach(function (section) {
section.hidden = section.id !== sectionId;
});
}
function hasDirtySettingsForm() {
return Array.prototype.some.call(document.querySelectorAll('[data-settings-form]'), function (form) {
return form.dataset && form.dataset.dirty === 'true';
});
}
function resetSettingsForms() {
document.querySelectorAll('[data-settings-form]').forEach(function (form) {
if (typeof form.reset === 'function') {
form.reset();
}
form.dataset.dirty = 'false';
});
document.dispatchEvent(new CustomEvent('settings:reset'));
}
links.forEach(function (link) {
link.addEventListener('click', function (event) {
var target = document.getElementById(String(link.getAttribute('href') || '').replace(/^#/, ''));
if (!target) {
return;
}
var currentSection = sections.find(function (section) { return !section.hidden; });
if (currentSection && target.id !== currentSection.id && hasDirtySettingsForm()) {
if (!window.confirm('You have unsaved settings. Switch sections anyway?')) {
event.preventDefault();
return;
}
resetSettingsForms();
}
event.preventDefault();
setActiveSection(target.id);
window.history.replaceState(null, '', '#' + target.id);
});
});
var hashSectionId = String(window.location.hash || '').replace(/^#/, '');
var initialSection = sections.some(function (section) {
return section.id === hashSectionId;
}) ? hashSectionId : sections[0].id;
setActiveSection(initialSection);
}
document.addEventListener('DOMContentLoaded', function () {
initIconSuggestions();
initDefaultAnnouncementIconPicker();
initSettingsSectionNavigation();
});
}());
+14 -2
View File
@@ -3,6 +3,7 @@ export function createSlideFormEditorController(options) {
var templateFields = settings.templateFields || null;
var templateSelectorLock = settings.templateSelectorLock || null;
var requestPreviewRender = typeof settings.requestPreviewRender === 'function' ? settings.requestPreviewRender : function () {};
var markFormDirty = typeof settings.markFormDirty === 'function' ? settings.markFormDirty : function () {};
var defaultFontSize = Math.max(1, Number(settings.defaultFontSize || 32));
var fontFamilyFormats = String(settings.fontFamilyFormats || '').trim();
var fontStylesheetHref = String(settings.fontStylesheetHref || '').trim();
@@ -17,8 +18,16 @@ export function createSlideFormEditorController(options) {
var imageUploadMaxBytes = Math.max(1, Number(settings.imageUploadMaxBytes || 2 * 1024 * 1024));
var imageUploadLimitLabel = String(settings.imageUploadLimitLabel || '').trim() || Math.max(1, Math.round(imageUploadMaxBytes / (1024 * 1024))) + ' MB';
var imageUploadContext = String(settings.imageUploadContext || 'wysiwyg').trim() || 'wysiwyg';
var imageUploadAllowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'];
var imageUploadAllowedMimeTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml'];
var configuredImageMimeTypes = Array.isArray(settings.uploadMimeTypes)
? settings.uploadMimeTypes.filter(function (mimeType) { return String(mimeType || '').indexOf('image/') === 0; })
: [];
var imageUploadAllowedMimeTypes = configuredImageMimeTypes.length
? configuredImageMimeTypes
: ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml'];
var imageUploadAllowedExtensions = imageUploadAllowedMimeTypes.map(function (mimeType) {
var subtype = String(mimeType).split('/')[1] || '';
return subtype === 'jpeg' ? 'jpg' : subtype === 'svg+xml' ? 'svg' : subtype;
});
var imageUploadFileTypes = imageUploadAllowedExtensions.join(',');
var wysiwygEditorHeightStep = Math.max(1, Number(settings.wysiwygEditorHeightStep || 80));
var wysiwygEditorHeightMin = Math.max(1, Number(settings.wysiwygEditorHeightMin || 240));
@@ -542,6 +551,9 @@ export function createSlideFormEditorController(options) {
['change', 'keyup', 'undo', 'redo', 'Paste', 'input'].forEach(function (eventName) {
editor.on(eventName, function () {
syncState(true);
if (hydrated) {
markFormDirty();
}
});
});
}
@@ -30,6 +30,9 @@ export function createSlideFormRegionHelpers(options) {
var defaultFontSize = Math.max(1, Number(settings.defaultFontSize || 32));
var uploadMaxLabel = String(settings.uploadMaxLabel || '100 MB');
var uploadVideoMaxLabel = String(settings.uploadVideoMaxLabel || '1 GB');
var uploadMaxBytes = Number(settings.uploadMaxBytes || 100 * 1024 * 1024);
var uploadVideoMaxBytes = Number(settings.uploadVideoMaxBytes || 1024 * 1024 * 1024);
var uploadMimeTypes = Array.isArray(settings.uploadMimeTypes) ? settings.uploadMimeTypes : [];
function sanitizeFontSize(value) {
var raw = String(value || '').trim().toLowerCase();
@@ -539,6 +542,9 @@ export function createSlideFormRegionHelpers(options) {
regionRatio: reduceAspectRatio(region.width, region.height),
uploadMaxLabel: uploadMaxLabel,
uploadVideoMaxLabel: uploadVideoMaxLabel,
uploadMaxBytes: uploadMaxBytes,
uploadVideoMaxBytes: uploadVideoMaxBytes,
uploadMimeTypes: uploadMimeTypes,
disableAudio: (existingContent[region.region_key] || {}).disable_audio,
config: region.region_type === 'api' ? getCurrentApiConfig(region) : region.region_type === 'timetable' ? getCurrentTimetableConfig(region) : getCurrentRssConfig(region),
style: getCurrentTextStyle(region),
@@ -579,6 +585,9 @@ export function createSlideFormRegionHelpers(options) {
regionRatio: reduceAspectRatio(region.width, region.height),
uploadMaxLabel: uploadMaxLabel,
uploadVideoMaxLabel: uploadVideoMaxLabel,
uploadMaxBytes: uploadMaxBytes,
uploadVideoMaxBytes: uploadVideoMaxBytes,
uploadMimeTypes: uploadMimeTypes,
disableAudio: currentContent.disable_audio,
config: region.region_type === 'api' ? apiConfig : region.region_type === 'timetable' ? getCurrentTimetableConfig(region) : rssConfig,
style: textStyle,
+23 -6
View File
@@ -6,6 +6,14 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
(function () {
var DEFAULT_FONT_SIZE = 32;
function formatUploadLimitLabel(bytes) {
var megabytes = Number(bytes || 0) / 1024 / 1024;
if (megabytes >= 1024 && megabytes % 1024 === 0) {
return String(megabytes / 1024) + ' GB';
}
return (Number.isInteger(megabytes) ? String(megabytes) : megabytes.toFixed(2).replace(/0+$/, '').replace(/\.$/, '')) + ' MB';
}
var dataElement = document.getElementById('slide-editor-data');
if (!dataElement) {
return;
@@ -37,12 +45,14 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
var slidePreviewEmpty = document.getElementById('slide-preview-empty');
var slidePreviewOverlay = document.getElementById('slide-preview-overlay');
var openPreviewPopupButton = document.getElementById('open-preview-popup');
var uploadMaxBytes = 100 * 1024 * 1024;
var uploadMaxLabel = '100 MB';
var uploadVideoMaxBytes = 1024 * 1024 * 1024;
var uploadVideoMaxLabel = '1 GB';
var wysiwygImageUploadMaxBytes = 2 * 1024 * 1024;
var wysiwygImageUploadLimitLabel = '2 MB';
var uploadLimits = slideEditorData.uploadLimits || {};
var uploadMaxBytes = Number(uploadLimits.imageMaxBytes || 100 * 1024 * 1024);
var uploadMaxLabel = formatUploadLimitLabel(uploadMaxBytes);
var uploadVideoMaxBytes = Number(uploadLimits.videoMaxBytes || 1024 * 1024 * 1024);
var uploadVideoMaxLabel = formatUploadLimitLabel(uploadVideoMaxBytes);
var wysiwygImageUploadMaxBytes = Number(uploadLimits.wysiwygImageMaxBytes || 2 * 1024 * 1024);
var wysiwygImageUploadLimitLabel = formatUploadLimitLabel(wysiwygImageUploadMaxBytes);
var uploadMimeTypes = Array.isArray(uploadLimits.allowedMimeTypes) ? uploadLimits.allowedMimeTypes : [];
var videoDurationCache = Object.create(null);
var previewRenderFrame = 0;
var previewPopupWindow = null;
@@ -111,7 +121,11 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
imageUploadMaxBytes: wysiwygImageUploadMaxBytes,
imageUploadLimitLabel: wysiwygImageUploadLimitLabel,
imageUploadContext: 'wysiwyg',
uploadMimeTypes: uploadMimeTypes,
getRegionTypeModule: getRegionTypeModule,
markFormDirty: function () {
slideForm.dataset.dirty = 'true';
},
getEditorBackgroundColor: function () {
var template = getTemplateById(templateSelect.value);
return template && template.background_color ? String(template.background_color) : '#111111';
@@ -128,6 +142,9 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
defaultFontSize: DEFAULT_FONT_SIZE,
uploadMaxLabel: uploadMaxLabel,
uploadVideoMaxLabel: uploadVideoMaxLabel,
uploadMaxBytes: uploadMaxBytes,
uploadVideoMaxBytes: uploadVideoMaxBytes,
uploadMimeTypes: uploadMimeTypes,
escapeHtml: escapeHtml,
getRegionTypeModule: getRegionTypeModule,
requestPreviewRender: requestPreviewRender
@@ -23,6 +23,14 @@
var listenersAttached = false;
var uploadMaxBytes = 100 * 1024 * 1024;
var uploadMaxLabel = '100 MB';
var editorDataElement = document.getElementById('slide-editor-data');
try {
var editorData = JSON.parse(editorDataElement && (editorDataElement.value || editorDataElement.textContent) || '{}') || {};
uploadMaxBytes = Number(editorData.uploadLimits && editorData.uploadLimits.imageMaxBytes) || uploadMaxBytes;
var megabytes = uploadMaxBytes / 1024 / 1024;
uploadMaxLabel = Number.isInteger(megabytes) ? String(megabytes) + ' MB' : megabytes.toFixed(2).replace(/0+$/, '').replace(/\.$/, '') + ' MB';
} catch (_error) {
}
function getRegionId(input) {
var match = String(input && input.name || '').match(/^region_image_(\d+)$/);