Release 2.7.3
Publish Docker Image / build-and-push (./build/Dockerfile, git.lzstealth.com/lzstealth/pulse-signage-web, web) (push) Successful in 1m15s
Publish Docker Image / build-and-push (./build/Dockerfile.player, git.lzstealth.com/lzstealth/pulse-signage-player, player) (push) Successful in 32s

This commit is contained in:
2026-08-15 11:43:07 +01:00
parent 15dc6eb7f2
commit f0177e6628
25 changed files with 871 additions and 127 deletions
+110 -5
View File
@@ -8,6 +8,8 @@ export function createSlideFormEditorController(options) {
var fontStylesheetHref = String(settings.fontStylesheetHref || '').trim();
var defaultEditorFontFamily = 'Arial, Helvetica, sans-serif';
var editorInstances = new Map();
var editorHeights = new Map();
var editorSizeControlsBound = false;
var getRegionTypeModule = typeof settings.getRegionTypeModule === 'function' ? settings.getRegionTypeModule : function () {
return null;
};
@@ -15,9 +17,13 @@ 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', 'bmp', 'avif', 'tif', 'tiff'];
var imageUploadAllowedMimeTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/bmp', 'image/avif', 'image/tiff'];
var imageUploadAllowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'];
var imageUploadAllowedMimeTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml'];
var imageUploadFileTypes = imageUploadAllowedExtensions.join(',');
var wysiwygEditorHeightStep = Math.max(1, Number(settings.wysiwygEditorHeightStep || 80));
var wysiwygEditorHeightMin = Math.max(1, Number(settings.wysiwygEditorHeightMin || 240));
var wysiwygEditorHeightMax = Math.max(wysiwygEditorHeightMin, Number(settings.wysiwygEditorHeightMax || 960));
var wysiwygEditorHeightDefault = Math.max(wysiwygEditorHeightMin, Number(settings.wysiwygEditorHeightDefault || 360));
var editorImageUploadPaths = new Set();
var committedEditorImageUploadPaths = new Set();
var pendingEditorImageUploadCleanupPaths = new Set();
@@ -64,6 +70,99 @@ export function createSlideFormEditorController(options) {
return '';
}
function normalizeEditorHeightValue(value) {
var numericValue = Number(value);
if (!Number.isFinite(numericValue) || numericValue <= 0) {
numericValue = wysiwygEditorHeightDefault;
}
return Math.min(wysiwygEditorHeightMax, Math.max(wysiwygEditorHeightMin, Math.round(numericValue)));
}
function getEditorHolder(regionId) {
if (!templateFields) {
return null;
}
return templateFields.querySelector('.editor-holder[data-region-id="' + regionId + '"]');
}
function getEditorHeight(regionId) {
var key = String(regionId || '');
var storedHeight = editorHeights.get(key);
if (storedHeight !== undefined && storedHeight !== null) {
return normalizeEditorHeightValue(storedHeight);
}
var holder = getEditorHolder(key);
var holderHeight = holder && holder.dataset ? holder.dataset.editorHeight : '';
var normalizedHeight = normalizeEditorHeightValue(holderHeight || wysiwygEditorHeightDefault);
editorHeights.set(key, normalizedHeight);
return normalizedHeight;
}
function applyEditorHeight(regionId, height, options) {
var key = String(regionId || '');
var normalizedHeight = normalizeEditorHeightValue(height);
var holder = getEditorHolder(key);
var editor = editorInstances.get(key);
var container = editor && typeof editor.getContainer === 'function' ? editor.getContainer() : null;
var shouldRequestPreview = !(options && options.silent);
editorHeights.set(key, normalizedHeight);
if (holder) {
holder.dataset.editorHeight = String(normalizedHeight);
holder.style.height = normalizedHeight + 'px';
holder.style.minHeight = normalizedHeight + 'px';
}
if (container) {
container.style.height = normalizedHeight + 'px';
container.style.minHeight = normalizedHeight + 'px';
}
if (editor && typeof editor.dispatch === 'function') {
editor.dispatch('ResizeEditor');
}
if (shouldRequestPreview) {
requestPreviewRender();
}
}
function adjustEditorHeight(regionId, delta) {
applyEditorHeight(regionId, getEditorHeight(regionId) + Number(delta || 0));
}
function bindEditorSizeControls() {
if (editorSizeControlsBound || !templateFields) {
return;
}
editorSizeControlsBound = true;
templateFields.addEventListener('click', function (event) {
var button = event.target && typeof event.target.closest === 'function' ? event.target.closest('[data-editor-size-action]') : null;
if (!button) {
return;
}
var action = String(button.getAttribute('data-editor-size-action') || '').trim();
if (action !== 'increase' && action !== 'decrease') {
return;
}
var card = typeof button.closest === 'function' ? button.closest('[data-region-id]') : null;
var regionId = card ? card.getAttribute('data-region-id') : '';
if (!regionId) {
return;
}
event.preventDefault();
adjustEditorHeight(regionId, action === 'increase' ? wysiwygEditorHeightStep : -wysiwygEditorHeightStep);
});
}
function getEditorHiddenInput(regionId) {
if (!templateFields) {
return null;
@@ -314,15 +413,15 @@ export function createSlideFormEditorController(options) {
}
if (mimeType && imageUploadAllowedMimeTypes.indexOf(mimeType) === -1) {
return 'This editor accepts PNG, JPG, GIF, WebP, BMP, AVIF, or TIFF images.';
return 'This editor accepts PNG, JPG, GIF, WebP, or SVG images.';
}
if (!mimeType && extension && imageUploadAllowedExtensions.indexOf(extension) === -1) {
return 'This editor accepts PNG, JPG, GIF, WebP, BMP, AVIF, or TIFF images.';
return 'This editor accepts PNG, JPG, GIF, WebP, or SVG images.';
}
if (!mimeType && !extension) {
return 'This editor accepts PNG, JPG, GIF, WebP, BMP, AVIF, or TIFF images.';
return 'This editor accepts PNG, JPG, GIF, WebP, or SVG images.';
}
return '';
@@ -470,6 +569,8 @@ export function createSlideFormEditorController(options) {
source.id = 'slide-editor-region-' + regionId;
}
applyEditorHeight(regionId, getEditorHeight(regionId), { silent: true });
function registerInlineFormat(editor, formatName, styles) {
editor.formatter.register(formatName, {
inline: 'span',
@@ -515,6 +616,8 @@ export function createSlideFormEditorController(options) {
toolbar: 'undo redo | fontfamily fontsizeinput | forecolor backcolor bold italic underlineformats removeformat | align lineheight indent outdent bullist numlist table image chip | fullscreen',
toolbar_mode: 'sliding',
license_key: 'gpl',
height: getEditorHeight(regionId),
min_height: wysiwygEditorHeightMin,
table_default_attributes: {
border: '1',
cellpadding: '0',
@@ -601,6 +704,7 @@ export function createSlideFormEditorController(options) {
}
editorInstances.set(regionId, editor);
applyEditorHeight(regionId, getEditorHeight(regionId), { silent: true });
if (editor.targetElm) {
editor.targetElm.value = editor.getContent({ format: 'html' });
}
@@ -617,6 +721,7 @@ export function createSlideFormEditorController(options) {
return Promise.resolve([]);
}
bindEditorSizeControls();
watchThemeChanges();
var holders = Array.prototype.slice.call(templateFields.querySelectorAll('.editor-holder'));
+137 -3
View File
@@ -14,6 +14,7 @@
var currentInput = null;
var currentFile = null;
var currentObjectUrl = '';
var cropperFrame = modal.querySelector('.slide-image-cropper-frame');
var flipX = 1;
var flipY = 1;
var currentAspectRatio = NaN;
@@ -40,6 +41,12 @@
status.textContent = String(message || '');
}
function setCropperLoading(loading) {
if (cropperFrame) {
cropperFrame.classList.toggle('is-loading', Boolean(loading));
}
}
function showModal() {
if (window.pulseModal && typeof window.pulseModal.show === 'function') {
window.pulseModal.show(modal);
@@ -79,6 +86,7 @@
function resetModalState() {
destroyCropper();
revokeObjectUrl();
setCropperLoading(false);
currentInput = null;
currentFile = null;
flipX = 1;
@@ -118,6 +126,75 @@
return width / height;
}
function isSvgFile(file) {
if (!file) {
return false;
}
return String(file.type || '').toLowerCase() === 'image/svg+xml' || /\.svg$/i.test(String(file.name || ''));
}
function isGifFile(file) {
if (!file) {
return false;
}
return String(file.type || '').toLowerCase() === 'image/gif' || /\.gif$/i.test(String(file.name || ''));
}
function isSpecialRasterizationFile(file) {
return isSvgFile(file) || isGifFile(file);
}
function getSpecialFileWarning(file) {
if (isGifFile(file)) {
return 'GIF selected. If you crop, rotate, or flip this image, it will be rasterized and the animation will be lost. Leave the full image selected to keep the original GIF.';
}
return 'SVG selected. If you crop, rotate, or flip this image, it will be rasterized. Leave the full image selected to keep the original SVG.';
}
function isOriginalSpecialSelection() {
if (!cropper || !currentFile || !isSpecialRasterizationFile(currentFile) || typeof cropper.getData !== 'function' || typeof cropper.getImageData !== 'function') {
return false;
}
var cropData = cropper.getData(true) || {};
var imageData = cropper.getImageData() || {};
var width = Number(imageData.naturalWidth || 0);
var height = Number(imageData.naturalHeight || 0);
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
return false;
}
return Math.abs(Number(cropData.x || 0)) < 0.5 &&
Math.abs(Number(cropData.y || 0)) < 0.5 &&
Math.abs(Number(cropData.width || 0) - width) < 0.5 &&
Math.abs(Number(cropData.height || 0) - height) < 0.5 &&
Math.abs(Number(cropData.rotate || 0)) < 0.5 &&
Number(cropData.scaleX || 1) === 1 &&
Number(cropData.scaleY || 1) === 1;
}
function getRasterizedFileName(sourceName) {
var name = String(sourceName || '').trim();
if (!name) {
return 'slide-region-image.png';
}
if (/\.svg$/i.test(name)) {
return name.replace(/\.svg$/i, '.png');
}
if (/\.[a-z0-9]+$/i.test(name)) {
return name.replace(/\.[a-z0-9]+$/i, '.png');
}
return name + '.png';
}
function setActiveRatioButton(value) {
var targetValue = ratioLabelForValue(value);
modal.querySelectorAll('[data-slide-image-cropper-action="ratio"]').forEach(function (button) {
@@ -183,6 +260,8 @@
zoomOnTouch: true,
zoomOnWheel: true,
ready: function () {
setCropperLoading(false);
if (cropper && cropper.container) {
cropper.container.style.width = '100%';
cropper.container.style.height = '560px';
@@ -209,7 +288,10 @@
setButtonState(false);
setActiveRatioButton(currentAspectRatio === undefined ? 'free' : currentAspectRatio);
setStatus('Use the toolbar to crop, rotate, or flip the image before applying it.');
if (isSpecialRasterizationFile(currentFile)) {
setStatus(getSpecialFileWarning(currentFile));
}
}
function openEditor(input, file) {
@@ -220,8 +302,9 @@
currentAspectRatio = 'free';
currentRegionAspectRatio = parseAspectRatio(input && input.dataset && input.dataset.slideImageCropperRegionRatio);
currentRegionAspectRatioLabel = String(input && input.dataset && input.dataset.slideImageCropperRegionRatioLabel || 'Region').trim() || 'Region';
setCropperLoading(true);
setButtonState(true);
setStatus('Loading image editor...');
setStatus(isSpecialRasterizationFile(file) ? getSpecialFileWarning(file) : '');
modal.querySelectorAll('[data-slide-image-cropper-action="ratio"][data-slide-image-cropper-ratio="region"]').forEach(function (button) {
button.title = currentRegionAspectRatioLabel ? 'Region ratio ' + currentRegionAspectRatioLabel : 'Region ratio';
@@ -286,11 +369,62 @@
hideModal();
}
function finalizeCurrentSvgSelection() {
if (!currentFile) {
return;
}
if (isOriginalSpecialSelection()) {
finalizeCropFile(currentFile);
return;
}
setButtonState(true);
setStatus('Creating a rasterized image from the crop...');
var canvas = cropper && typeof cropper.getCroppedCanvas === 'function' ? cropper.getCroppedCanvas({
fillColor: 'transparent',
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high'
}) : null;
if (!canvas) {
setStatus('Unable to create the cropped image.');
setButtonState(false);
return;
}
canvas.toBlob(function (blob) {
if (!blob) {
setStatus('Unable to create the cropped image.');
setButtonState(false);
return;
}
var finalFile = new File([blob], getRasterizedFileName(currentFile.name), {
lastModified: Date.now(),
type: blob.type || 'image/png'
});
finalizeCropFile(finalFile);
}, 'image/png');
}
function commitCrop() {
if (!currentInput || !currentFile) {
return;
}
if (isSpecialRasterizationFile(currentFile)) {
if (!cropper) {
finalizeCropFile(currentFile);
return;
}
finalizeCurrentSvgSelection();
return;
}
if (!cropper) {
finalizeCropFile(currentFile);
return;
@@ -381,7 +515,7 @@
return;
}
if (!/^image\//i.test(file.type || '')) {
if (!/^image\//i.test(file.type || '') && !/\.svg$/i.test(String(file.name || ''))) {
input.value = '';
return;
}