Release v2.13.1
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m15s
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-09-11 20:46:37 +01:00
parent 0a03cdd0b7
commit 394d23bb4d
47 changed files with 2408 additions and 559 deletions
+282
View File
@@ -0,0 +1,282 @@
// Shared media library picker for editor forms.
(function () {
var modal = document.getElementById('media-picker-modal');
if (!modal) {
return;
}
var activeButton = null;
var items = Array.prototype.slice.call(modal.querySelectorAll('[data-media-picker-item]'));
var grid = modal.querySelector('[data-media-picker-grid]');
var search = modal.querySelector('[data-media-picker-search]');
var sort = modal.querySelector('[data-media-picker-sort]');
var noResults = modal.querySelector('[data-media-picker-no-results]');
var loadMoreWrap = modal.querySelector('[data-media-picker-load-more-wrap]');
var loadMoreButton = modal.querySelector('[data-media-picker-load-more]');
var confirmButton = modal.querySelector('[data-media-picker-confirm]');
var cancelButton = modal.querySelector('[data-media-picker-cancel]');
var uploadButton = modal.querySelector('[data-media-picker-upload]');
var currentPath = '';
var pendingPath = '';
var pendingType = '';
var visibleRowLimit = 4;
var loadedAssets = [];
var mediaOffset = 0;
var mediaHasMore = false;
var refreshSequence = 0;
function getColumnsPerRow() {
return window.innerWidth >= 992 ? 4 : window.innerWidth >= 768 ? 3 : 2;
}
function renderItems(assets) {
if (!grid) {
return;
}
grid.innerHTML = (Array.isArray(assets) ? assets : []).map(function (asset) {
var type = String(asset.media_type || '');
var path = escapeHtml(asset.media_path);
var name = escapeHtml(asset.original_name);
var createdAt = escapeHtml(asset.created_at);
var preview = type === 'video'
? '<video src="' + path + '" muted playsinline preload="metadata"></video>'
: '<img src="' + path + '" alt="" loading="lazy" />';
return '<div class="col-6 col-md-4 col-lg-3" data-media-picker-item data-media-type="' + escapeHtml(type) + '" data-media-name="' + name + '" data-media-created-at="' + createdAt + '">' +
'<button type="button" class="media-picker-item w-100 text-start" data-media-picker-path="' + path + '" data-media-picker-type="' + escapeHtml(type) + '">' +
'<span class="media-picker-item-preview">' + preview + '</span>' +
'<span class="media-picker-item-name text-truncate d-block">' + name + '</span>' +
'<span class="media-picker-item-statuses"><span class="media-picker-item-current" data-media-picker-current hidden><i class="bi bi-check-circle me-1" aria-hidden="true"></i>Current</span><span class="media-picker-item-selected" data-media-picker-selected hidden><i class="bi bi-check-circle-fill me-1" aria-hidden="true"></i>Selected</span></span>' +
'</button></div>';
}).join('');
items = Array.prototype.slice.call(modal.querySelectorAll('[data-media-picker-item]'));
}
function getQueryParams(offset, limit) {
var params = new URLSearchParams();
var type = activeButton ? activeButton.getAttribute('data-media-picker-type') || 'image' : 'image';
params.set('offset', String(offset));
params.set('limit', String(limit));
params.set('search', String(search && search.value || '').trim());
params.set('type', type);
params.set('sort', sort && sort.value || 'date-desc');
return params.toString();
}
async function refreshItems() {
var requestSequence = ++refreshSequence;
try {
var response = await fetch('/media-library/assets?' + getQueryParams(0, 4 * getColumnsPerRow()), { credentials: 'same-origin', headers: { Accept: 'application/json' } });
if (!response.ok) {
return;
}
var payload = await response.json();
if (requestSequence !== refreshSequence) {
return;
}
loadedAssets = Array.isArray(payload.assets) ? payload.assets : [];
mediaOffset = Number(payload.nextOffset || loadedAssets.length);
mediaHasMore = Boolean(payload.hasMore);
renderItems(loadedAssets);
updateSelection();
applyFilterAndSort();
} catch (_error) {
// Keep the server-rendered list if refreshing is unavailable.
}
}
function escapeHtml(value) {
return String(value || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
function showModal() {
if (window.pulseModal && typeof window.pulseModal.show === 'function') {
window.pulseModal.show(modal);
} else if (window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modal).show();
}
}
function hideModal() {
if (window.pulseModal && typeof window.pulseModal.hide === 'function') {
window.pulseModal.hide(modal);
} else if (window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modal).hide();
}
}
function applyFilterAndSort() {
var visibleItems = items.slice();
var columnsPerRow = getColumnsPerRow();
var visibleItemLimit = visibleRowLimit * columnsPerRow;
items.forEach(function (item) { item.hidden = true; });
visibleItems.slice(0, visibleItemLimit).forEach(function (item) {
item.hidden = false;
if (grid) grid.appendChild(item);
});
if (noResults) noResults.hidden = visibleItems.length > 0;
if (loadMoreWrap) loadMoreWrap.hidden = !mediaHasMore || visibleItems.length === 0;
}
function resetVisibleItemLimit() {
visibleRowLimit = 4;
if (activeButton) {
refreshItems();
}
}
function updateSelection() {
items.forEach(function (item) {
var itemButton = item.querySelector('[data-media-picker-path]');
var path = itemButton && itemButton.getAttribute('data-media-picker-path');
var current = Boolean(currentPath && path === currentPath);
var selected = Boolean(pendingPath && path === pendingPath);
item.classList.toggle('media-picker-item-current-card', current);
item.classList.toggle('media-picker-item-selected-card', selected);
if (itemButton) {
itemButton.setAttribute('aria-pressed', selected ? 'true' : 'false');
var currentLabel = itemButton.querySelector('[data-media-picker-current]');
if (currentLabel) currentLabel.hidden = !current;
var selectedLabel = itemButton.querySelector('[data-media-picker-selected]');
if (selectedLabel) selectedLabel.hidden = !selected;
}
});
}
function getActiveHidden() {
var hiddenKey = activeButton && activeButton.getAttribute('data-media-picker-hidden');
return hiddenKey && (document.getElementById(hiddenKey) || document.querySelector('[name="' + hiddenKey + '"]'));
}
function closePicker() {
activeButton = null;
currentPath = '';
pendingPath = '';
pendingType = '';
hideModal();
}
function getActiveFileInput() {
if (!activeButton) {
return null;
}
var hiddenKey = activeButton.getAttribute('data-media-picker-hidden');
if (hiddenKey) {
var inputFromHidden = document.getElementById(hiddenKey.replace(/^existing_/, ''));
if (inputFromHidden) {
return inputFromHidden;
}
}
var inputId = activeButton.getAttribute('for');
if (inputId) {
return document.getElementById(inputId);
}
var region = activeButton.closest ? activeButton.closest('[data-region-id]') : null;
return region ? region.querySelector('input[type="file"]') : null;
}
function updatePreview(hidden, path, type) {
var region = hidden.closest ? hidden.closest('[data-region-id]') : null;
if (region) {
var previewBox = region.querySelector('.slide-image-region-preview-box');
if (previewBox) {
var safePath = escapeHtml(path);
previewBox.innerHTML = type === 'video'
? '<div class="slide-image-region-preview-shell"><video class="slide-image-region-preview" src="' + safePath + '" muted playsinline preload="metadata"></video></div>'
: '<div class="slide-image-region-preview-shell"><img class="slide-image-region-preview" src="' + safePath + '" alt="Selected media preview" /></div>';
}
return;
}
var backgroundPreview = document.getElementById('background-preview');
var backgroundEmpty = document.getElementById('background-empty');
if (backgroundPreview && path) {
backgroundPreview.src = path;
backgroundPreview.style.display = 'block';
if (backgroundEmpty) backgroundEmpty.style.display = 'none';
}
}
document.addEventListener('click', function (event) {
var uploadTrigger = event.target && event.target.closest ? event.target.closest('[data-media-picker-upload]') : null;
if (uploadTrigger) {
event.preventDefault();
var fileInput = getActiveFileInput();
if (fileInput) {
fileInput.addEventListener('click', function (inputEvent) {
inputEvent.stopPropagation();
}, { once: true });
fileInput.click();
closePicker();
}
return;
}
var button = event.target && event.target.closest ? event.target.closest('[data-media-picker]') : null;
if (button) {
event.preventDefault();
activeButton = button;
var hidden = getActiveHidden();
currentPath = hidden ? String(hidden.value || '') : '';
pendingPath = currentPath;
pendingType = button.getAttribute('data-media-picker-type') || 'image';
if (search) search.value = '';
if (sort) sort.value = 'date-desc';
visibleRowLimit = 4;
updateSelection();
applyFilterAndSort();
showModal();
refreshItems();
return;
}
var itemButton = event.target && event.target.closest ? event.target.closest('[data-media-picker-path]') : null;
if (!itemButton || !activeButton) {
return;
}
pendingPath = itemButton.getAttribute('data-media-picker-path') || '';
pendingType = itemButton.getAttribute('data-media-picker-type') || activeButton.getAttribute('data-media-picker-type') || 'image';
updateSelection();
});
if (search) search.addEventListener('input', resetVisibleItemLimit);
if (sort) sort.addEventListener('change', resetVisibleItemLimit);
if (loadMoreButton) loadMoreButton.addEventListener('click', async function () {
loadMoreButton.disabled = true;
try {
var response = await fetch('/media-library/assets?' + getQueryParams(mediaOffset, 4 * getColumnsPerRow()), { credentials: 'same-origin', headers: { Accept: 'application/json' } });
if (!response.ok) {
return;
}
var payload = await response.json();
loadedAssets = loadedAssets.concat(Array.isArray(payload.assets) ? payload.assets : []);
mediaOffset = Number(payload.nextOffset || mediaOffset);
mediaHasMore = Boolean(payload.hasMore);
renderItems(loadedAssets);
visibleRowLimit += 4;
updateSelection();
applyFilterAndSort();
} finally {
loadMoreButton.disabled = false;
}
});
if (confirmButton) confirmButton.addEventListener('click', function () {
var hidden = getActiveHidden();
if (!hidden || !pendingPath) {
closePicker();
return;
}
hidden.value = pendingPath;
updatePreview(hidden, pendingPath, pendingType);
hidden.dispatchEvent(new Event('change', { bubbles: true }));
closePicker();
});
if (cancelButton) cancelButton.addEventListener('click', closePicker);
modal.addEventListener('hidden.bs.modal', function () {
activeButton = null;
currentPath = '';
pendingPath = '';
pendingType = '';
});
})();
+2 -2
View File
@@ -41,12 +41,12 @@
bodyHtml: '' +
'<div class="row g-3 align-items-start">' +
'<div class="col-12 col-md-8 d-flex flex-column">' +
'<label class="slide-image-region-upload-zone" data-region-upload-zone="' + region.id + '" for="region_image_' + region.id + '">' +
'<label class="slide-image-region-upload-zone" data-region-upload-zone="' + region.id + '" data-media-picker data-media-picker-type="image" data-media-picker-hidden="existing_region_image_' + region.id + '" for="region_image_' + region.id + '">' +
'<input type="file" id="region_image_' + region.id + '" name="region_image_' + region.id + '" class="visually-hidden" accept="' + escapeHtml(uploadAccept) + '" data-slide-image-cropper-region-ratio="' + escapeHtml(regionRatio) + '" data-slide-image-cropper-region-ratio-label="Region" />' +
'<span class="slide-image-region-upload-zone-content">' +
'<span class="slide-image-region-upload-zone-icon"><i class="bi bi-cloud-arrow-up" aria-hidden="true"></i></span>' +
'<span class="slide-image-region-upload-zone-copy">' +
'<strong>Drop an image here or click to upload</strong>' +
'<strong>Drop an image here or click to select existing</strong>' +
'<span>' + escapeHtml(uploadHelpText) + '</span>' +
'<span class="slide-image-region-upload-zone-limit">Max ' + escapeHtml(uploadMaxLabel) + ' per file</span>' +
'</span>' +
+2 -2
View File
@@ -982,12 +982,12 @@
var imageOptionsHtml = '' +
'<div class="row g-3 align-items-start">' +
'<div class="col-12 col-md-8">' +
'<label class="slide-image-region-upload-zone" data-region-upload-zone="' + region.id + '" for="region_qr_image_' + region.id + '">' +
'<label class="slide-image-region-upload-zone" data-region-upload-zone="' + region.id + '" data-media-picker data-media-picker-type="image" data-media-picker-hidden="existing_region_qr_image_' + region.id + '" for="region_qr_image_' + region.id + '">' +
'<input type="file" id="region_qr_image_' + region.id + '" name="region_qr_image_' + region.id + '" class="visually-hidden" accept="image/*" />' +
'<span class="slide-image-region-upload-zone-content">' +
'<span class="slide-image-region-upload-zone-icon"><i class="bi bi-cloud-arrow-up" aria-hidden="true"></i></span>' +
'<span class="slide-image-region-upload-zone-copy">' +
'<strong>Drop an image here or click to upload</strong>' +
'<strong>Drop an image here or click to select existing</strong>' +
'<span>PNG, JPG, GIF, or WebP</span>' +
'<span class="slide-image-region-upload-zone-limit">Replaces the QR image logo</span>' +
'</span>' +
+2 -2
View File
@@ -46,12 +46,12 @@
bodyHtml: '' +
'<div class="row g-3 align-items-start">' +
'<div class="col-12 col-md-8 d-flex flex-column">' +
'<label class="slide-image-region-upload-zone" data-region-upload-zone="' + region.id + '" for="region_video_' + region.id + '">' +
'<label class="slide-image-region-upload-zone" data-region-upload-zone="' + region.id + '" data-media-picker data-media-picker-type="video" data-media-picker-hidden="existing_region_video_' + region.id + '" for="region_video_' + region.id + '">' +
'<input type="file" id="region_video_' + region.id + '" name="region_video_' + region.id + '" class="visually-hidden" accept="' + escapeHtml(uploadAccept) + '" />' +
'<span class="slide-image-region-upload-zone-content">' +
'<span class="slide-image-region-upload-zone-icon"><i class="bi bi-camera-video" aria-hidden="true"></i></span>' +
'<span class="slide-image-region-upload-zone-copy">' +
'<strong>Drop a video here or click to upload</strong>' +
'<strong>Drop a video here or click to select existing</strong>' +
'<span>' + escapeHtml(uploadHelpText) + '</span>' +
'<span class="slide-image-region-upload-zone-limit">Max ' + escapeHtml(uploadVideoMaxLabel) + ' per file</span>' +
'</span>' +
+559
View File
@@ -0,0 +1,559 @@
const modal = document.getElementById('media-gallery-preview-modal');
const body = modal && modal.querySelector('[data-media-gallery-preview-body]');
const title = modal && modal.querySelector('#media-gallery-preview-modal-label');
const gallery = document.querySelector('.media-gallery');
const search = gallery && gallery.querySelector('[data-media-gallery-search]');
const typeFilter = gallery && gallery.querySelector('[data-media-gallery-type-filter]');
const usageFilter = gallery && gallery.querySelector('[data-media-gallery-usage-filter]');
const sort = gallery && gallery.querySelector('[data-media-gallery-sort]');
const galleryCount = gallery && gallery.querySelector('[data-media-gallery-count]');
const noResults = gallery && gallery.querySelector('[data-media-gallery-no-results]');
const loadMoreWrap = gallery && gallery.querySelector('[data-media-gallery-load-more-wrap]');
const loadMoreButton = gallery && gallery.querySelector('[data-media-gallery-load-more]');
const selectModeButton = gallery && gallery.querySelector('[data-media-gallery-select-mode]');
const deleteSelectedButton = gallery && gallery.querySelector('[data-media-gallery-delete-selected]');
let galleryItems = gallery ? Array.from(gallery.querySelectorAll('[data-media-gallery-column]')) : [];
let visibleRowLimit = 4;
let galleryOffset = gallery ? Number(gallery.dataset.mediaGalleryOffset || galleryItems.length) : 0;
let galleryHasMore = gallery ? gallery.dataset.mediaGalleryHasMore === 'true' : false;
let galleryTotal = galleryCount ? Number(galleryCount.textContent || galleryItems.length) : galleryItems.length;
let galleryRefreshSequence = 0;
let selectionMode = false;
const selectedAssetIds = new Set();
const uploadForm = document.querySelector('form[action="/media-library"]');
const uploadInput = document.querySelector('[data-media-library-upload]');
const uploadZone = document.querySelector('[data-media-library-upload-zone]');
const uploadName = document.querySelector('[data-media-library-upload-name]');
const cropModal = document.getElementById('media-library-crop-modal');
const cropImage = cropModal && cropModal.querySelector('#media-library-crop-image');
const cropApply = cropModal && cropModal.querySelector('[data-media-library-crop-apply]');
const cropCancel = cropModal && cropModal.querySelector('[data-media-library-crop-cancel]');
const cropStatus = cropModal && cropModal.querySelector('#media-library-crop-status');
const cropFrame = cropModal && cropModal.querySelector('.slide-image-cropper-frame');
let cropper = null;
let cropObjectUrl = '';
let cropSourceFile = null;
let cropFlipX = 1;
let cropFlipY = 1;
function getColumnsPerRow() {
return window.innerWidth >= 1200 ? 6 : window.innerWidth >= 992 ? 3 : window.innerWidth >= 576 ? 2 : 1;
}
function showUploadMessage(message) {
if (typeof window.showToast === 'function') {
window.showToast(message, 'warning');
return;
}
window.alert(message);
}
function showUploadModal() {
if (window.pulseModal && typeof window.pulseModal.show === 'function') {
window.pulseModal.show(cropModal);
} else if (window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(cropModal).show();
}
}
function hideUploadModal() {
if (window.pulseModal && typeof window.pulseModal.hide === 'function') {
window.pulseModal.hide(cropModal);
} else if (window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(cropModal).hide();
}
}
function clearCropper() {
if (cropper) {
cropper.destroy();
cropper = null;
}
if (cropObjectUrl) {
URL.revokeObjectURL(cropObjectUrl);
cropObjectUrl = '';
}
cropSourceFile = null;
cropFlipX = 1;
cropFlipY = 1;
if (cropStatus) cropStatus.textContent = '';
if (cropFrame) cropFrame.classList.remove('is-loading');
}
function setCropperActionState(disabled) {
if (!cropModal) return;
cropModal.querySelectorAll('[data-media-library-crop-action]').forEach(function (button) {
button.disabled = Boolean(disabled);
});
}
function appendGalleryAsset(asset) {
if (!gallery || !asset) return;
const column = document.createElement('div');
column.className = 'col-12 col-sm-6 col-lg-4 col-xl-2';
column.dataset.mediaGalleryColumn = '';
column.dataset.mediaId = String(asset.id || '');
column.dataset.mediaType = asset.media_type || '';
column.dataset.mediaUsed = asset.inUse ? 'true' : 'false';
const article = document.createElement('article');
article.className = 'media-gallery-card';
article.dataset.mediaCreatedAt = asset.created_at || '';
article.dataset.mediaFileSize = asset.file_size || '0';
const preview = document.createElement('button');
preview.type = 'button';
preview.className = 'media-gallery-preview';
preview.dataset.mediaGalleryPreview = '';
preview.dataset.mediaPath = asset.media_path || '';
preview.dataset.mediaType = asset.media_type || '';
preview.dataset.mediaName = asset.original_name || '';
preview.setAttribute('aria-label', 'Preview ' + (asset.original_name || 'media'));
const media = asset.media_type === 'video' ? document.createElement('video') : document.createElement('img');
media.src = asset.media_path || '';
if (asset.media_type === 'video') {
media.muted = true;
media.playsInline = true;
media.preload = 'metadata';
} else {
media.alt = asset.original_name || '';
media.loading = 'lazy';
}
preview.appendChild(media);
if (asset.media_type === 'video') {
const play = document.createElement('span');
play.className = 'media-gallery-play';
play.setAttribute('aria-hidden', 'true');
play.innerHTML = '<i class="bi bi-play-fill"></i>';
preview.appendChild(play);
}
const format = document.createElement('span');
format.className = 'media-gallery-type';
format.textContent = asset.mediaFormat || '';
preview.appendChild(format);
const details = document.createElement('div');
details.className = 'media-gallery-details';
const header = document.createElement('div');
header.className = 'media-gallery-header';
const name = document.createElement('h4');
name.className = 'media-gallery-name';
name.title = asset.original_name || '';
name.textContent = asset.original_name || '';
header.appendChild(name);
if (asset.canDelete) {
if (asset.inUse) {
const lock = document.createElement('button');
lock.type = 'button';
lock.className = 'btn btn-outline-secondary btn-sm media-gallery-action';
lock.disabled = true;
lock.title = 'Media is in use';
lock.innerHTML = '<i class="bi bi-lock" aria-hidden="true"></i><span class="visually-hidden">Media is in use</span>';
header.appendChild(lock);
} else {
const form = document.createElement('form');
form.method = 'post';
form.action = '/media-library/' + encodeURIComponent(asset.id) + '/delete';
form.dataset.confirmMessage = 'Delete this media?';
form.className = 'media-gallery-action';
form.innerHTML = '<button type="submit" class="btn btn-outline-danger btn-sm" title="Delete media"><i class="bi bi-trash" aria-hidden="true"></i><span class="visually-hidden">Delete media</span></button>';
header.appendChild(form);
}
}
const meta = document.createElement('div');
meta.className = 'media-gallery-meta';
meta.textContent = (asset.displayFileSize || '') + ' · Used ' + Number(asset.referenceCount || 0) + ' time' + (Number(asset.referenceCount || 0) === 1 ? '' : 's');
details.appendChild(header);
details.appendChild(meta);
article.appendChild(preview);
article.appendChild(details);
column.appendChild(article);
gallery.querySelector('.media-gallery-scroll .row').appendChild(column);
galleryItems.push(column);
column.querySelector('.media-gallery-card').classList.toggle('media-gallery-card-selected', selectedAssetIds.has(String(asset.id || '')));
}
function applyCropRatio(value) {
if (!cropper) return;
cropper.setAspectRatio(value === 'free' ? NaN : Number(value.split(':')[0]) / Number(value.split(':')[1]));
cropModal.querySelectorAll('[data-media-library-crop-action="ratio"]').forEach(function (button) {
const active = button.dataset.mediaLibraryCropRatio === value;
button.classList.toggle('active', active);
button.setAttribute('aria-pressed', active ? 'true' : 'false');
});
}
function initMediaCropper() {
if (!cropSourceFile || !cropImage || !window.Cropper || cropper) return;
if (cropFrame) cropFrame.classList.add('is-loading');
cropper = new window.Cropper(cropImage, {
aspectRatio: NaN,
autoCropArea: 1,
background: false,
dragMode: 'move',
initialAspectRatio: NaN,
movable: true,
responsive: true,
rotatable: true,
scalable: true,
viewMode: 1,
zoomOnTouch: true,
zoomOnWheel: true,
ready: function () {
if (cropFrame) cropFrame.classList.remove('is-loading');
if (cropper && cropper.container) {
cropper.container.style.width = '100%';
cropper.container.style.height = '560px';
cropper.container.style.maxHeight = '70vh';
}
const containerData = cropper.getContainerData();
const imageData = cropper.getImageData();
const fitRatio = Math.min(Number(containerData.width || 0) / Number(imageData.naturalWidth || 1), Number(containerData.height || 0) / Number(imageData.naturalHeight || 1), 1);
if (fitRatio > 0) cropper.zoomTo(fitRatio);
applyCropRatio('free');
setCropperActionState(false);
}
});
}
function setUploadFile(file, shouldCrop) {
if (!uploadInput || !file) {
return;
}
const allowedTypes = String(uploadInput.accept || '').split(',').map(function (value) { return value.trim().toLowerCase(); }).filter(Boolean);
const fileType = String(file.type || '').toLowerCase();
const matchesType = allowedTypes.length === 0 || allowedTypes.indexOf(fileType) !== -1;
const maxBytes = fileType.indexOf('video/') === 0 ? Number(uploadInput.dataset.videoMaxBytes) : Number(uploadInput.dataset.imageMaxBytes);
if (!matchesType) {
showUploadMessage('This media type is disabled in Media Uploads settings.');
uploadInput.value = '';
return;
}
if (maxBytes && file.size > maxBytes) {
showUploadMessage('This file is larger than the configured upload limit.');
uploadInput.value = '';
return;
}
const transfer = new DataTransfer();
transfer.items.add(file);
uploadInput.files = transfer.files;
if (uploadName) {
uploadName.textContent = file.name;
}
if (shouldCrop && fileType.indexOf('image/') === 0 && cropModal && cropImage && window.Cropper) {
clearCropper();
setCropperActionState(true);
cropSourceFile = file;
cropObjectUrl = URL.createObjectURL(file);
cropImage.src = cropObjectUrl;
if (cropFrame) cropFrame.classList.add('is-loading');
showUploadModal();
}
}
function submitUpload() {
if (!uploadForm) {
return;
}
if (typeof uploadForm.requestSubmit === 'function') {
uploadForm.requestSubmit();
} else {
uploadForm.submit();
}
}
if (uploadInput) {
uploadInput.addEventListener('change', function () {
const file = uploadInput.files && uploadInput.files[0];
if (file) {
setUploadFile(file, true);
if (String(file.type || '').toLowerCase().indexOf('image/') !== 0 || !(cropModal && cropImage && window.Cropper)) {
submitUpload();
}
}
});
}
if (uploadZone) {
uploadZone.addEventListener('dragover', function (event) {
event.preventDefault();
uploadZone.classList.add('is-dragover');
});
uploadZone.addEventListener('dragleave', function () {
uploadZone.classList.remove('is-dragover');
});
uploadZone.addEventListener('drop', function (event) {
event.preventDefault();
uploadZone.classList.remove('is-dragover');
const file = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0];
if (file) {
setUploadFile(file, true);
if (String(file.type || '').toLowerCase().indexOf('image/') !== 0 || !(cropModal && cropImage && window.Cropper)) {
submitUpload();
}
}
});
}
if (cropApply) {
cropApply.addEventListener('click', function () {
if (!cropper || !cropSourceFile || !uploadInput) {
hideUploadModal();
return;
}
const outputType = cropSourceFile.type === 'image/jpeg' || cropSourceFile.type === 'image/webp' ? cropSourceFile.type : 'image/png';
cropper.getCroppedCanvas().toBlob(function (blob) {
if (!blob) {
showUploadMessage('Unable to crop this image.');
return;
}
const extension = outputType === 'image/jpeg' ? '.jpg' : outputType === 'image/webp' ? '.webp' : '.png';
const croppedFile = new File([blob], cropSourceFile.name.replace(/\.[^.]+$/, '') + extension, { type: outputType, lastModified: Date.now() });
clearCropper();
setUploadFile(croppedFile, false);
hideUploadModal();
submitUpload();
}, outputType, 0.92);
});
}
if (cropCancel) {
cropCancel.addEventListener('click', function () {
clearCropper();
if (uploadInput) uploadInput.value = '';
if (uploadName) uploadName.textContent = 'No file selected';
hideUploadModal();
});
}
if (cropModal) {
cropModal.addEventListener('click', function (event) {
const button = event.target && event.target.closest ? event.target.closest('[data-media-library-crop-action]') : null;
if (!button || !cropper) return;
const action = button.dataset.mediaLibraryCropAction;
if (action === 'rotate-left') cropper.rotate(-90);
if (action === 'rotate-right') cropper.rotate(90);
if (action === 'flip-horizontal') {
cropFlipX *= -1;
cropper.scaleX(cropFlipX);
}
if (action === 'flip-vertical') {
cropFlipY *= -1;
cropper.scaleY(cropFlipY);
}
if (action === 'ratio') applyCropRatio(button.dataset.mediaLibraryCropRatio || 'free');
if (action === 'reset') {
cropper.reset();
cropFlipX = 1;
cropFlipY = 1;
applyCropRatio('free');
}
});
}
if (cropModal) {
cropModal.addEventListener('shown.bs.modal', function () {
if (cropSourceFile && cropImage && window.Cropper && !cropper) {
initMediaCropper();
}
});
cropModal.addEventListener('hidden.bs.modal', function () {
clearCropper();
});
}
if (uploadForm) {
uploadForm.addEventListener('submit', function (event) {
if (uploadInput && !uploadInput.files.length) {
event.preventDefault();
showUploadMessage('Choose an image or video to upload.');
}
});
}
function getGalleryQuery(offset, limit) {
const params = new URLSearchParams();
params.set('offset', String(offset));
params.set('limit', String(limit));
params.set('search', String(search && search.value || '').trim());
params.set('type', typeFilter ? typeFilter.value : 'all');
params.set('usage', usageFilter ? usageFilter.value : 'all');
params.set('sort', sort ? sort.value : 'date-desc');
return params.toString();
}
function updateSearchResults() {
const columnsPerRow = getColumnsPerRow();
const visibleItemLimit = visibleRowLimit * columnsPerRow;
let visibleCount = 0;
galleryItems.forEach(function (card) {
card.hidden = visibleCount >= visibleItemLimit;
if (!card.hidden) {
visibleCount += 1;
}
});
if (galleryCount) {
galleryCount.textContent = String(galleryTotal);
}
if (noResults) {
noResults.hidden = galleryTotal > 0;
}
if (loadMoreWrap) {
loadMoreWrap.hidden = !galleryHasMore || galleryTotal === 0;
}
updateBulkDeleteActions();
}
function updateBulkDeleteActions() {
if (selectModeButton) {
selectModeButton.setAttribute('aria-pressed', selectionMode ? 'true' : 'false');
selectModeButton.textContent = selectionMode ? 'Done' : 'Select';
}
if (deleteSelectedButton) {
deleteSelectedButton.hidden = !selectionMode;
deleteSelectedButton.disabled = selectedAssetIds.size === 0;
}
galleryItems.forEach(function (item) {
const id = String(item.dataset.mediaId || '');
const card = item.querySelector('.media-gallery-card');
if (card) card.classList.toggle('media-gallery-card-selected', selectionMode && selectedAssetIds.has(id));
});
}
function clearSelectedAssets() {
selectedAssetIds.clear();
}
function setSelectionMode(enabled) {
selectionMode = Boolean(enabled);
if (!selectionMode) clearSelectedAssets();
if (gallery) gallery.classList.toggle('media-gallery-selection-mode', selectionMode);
updateBulkDeleteActions();
}
function submitBulkDelete() {
if (!selectedAssetIds.size || !window.confirm('Delete ' + selectedAssetIds.size + ' selected media asset' + (selectedAssetIds.size === 1 ? '' : 's') + '?')) {
return;
}
const form = document.createElement('form');
form.method = 'post';
form.action = '/media-library/delete';
selectedAssetIds.forEach(function (id) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'assetIds[]';
input.value = id;
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
async function refreshGallery() {
const requestSequence = ++galleryRefreshSequence;
try {
const response = await fetch('/media-library/assets?' + getGalleryQuery(0, 4 * getColumnsPerRow()), { headers: { Accept: 'application/json' } });
if (!response.ok) throw new Error('Unable to refresh media.');
const result = await response.json();
if (requestSequence !== galleryRefreshSequence) return;
galleryItems.forEach(function (item) { item.remove(); });
galleryItems = [];
clearSelectedAssets();
(result.assets || []).forEach(appendGalleryAsset);
galleryOffset = Number(result.nextOffset || 0);
galleryHasMore = Boolean(result.hasMore);
galleryTotal = Number(result.total || 0);
visibleRowLimit = 4;
updateSearchResults();
} catch (error) {
showUploadMessage(error.message || 'Unable to refresh media.');
}
}
function resetVisibleItemLimit() {
visibleRowLimit = 4;
refreshGallery();
}
if (search) {
search.addEventListener('input', resetVisibleItemLimit);
}
if (typeFilter) typeFilter.addEventListener('change', resetVisibleItemLimit);
if (usageFilter) usageFilter.addEventListener('change', resetVisibleItemLimit);
if (sort) sort.addEventListener('change', resetVisibleItemLimit);
if (gallery) {
gallery.addEventListener('click', function (event) {
if (!selectionMode) return;
const column = event.target && event.target.closest ? event.target.closest('[data-media-gallery-column]') : null;
if (!column || !gallery.contains(column)) return;
event.preventDefault();
event.stopPropagation();
if (column.dataset.mediaUsed === 'true') return;
const id = String(column.dataset.mediaId || '');
if (!id) return;
if (selectedAssetIds.has(id)) selectedAssetIds.delete(id);
else selectedAssetIds.add(id);
updateBulkDeleteActions();
});
}
if (selectModeButton) selectModeButton.addEventListener('click', function () { setSelectionMode(!selectionMode); });
if (deleteSelectedButton) deleteSelectedButton.addEventListener('click', submitBulkDelete);
if (loadMoreButton) {
loadMoreButton.addEventListener('click', async function () {
loadMoreButton.disabled = true;
try {
const response = await fetch('/media-library/assets?' + getGalleryQuery(galleryOffset, 4 * getColumnsPerRow()), { headers: { Accept: 'application/json' } });
if (!response.ok) throw new Error('Unable to load more media.');
const result = await response.json();
(result.assets || []).forEach(appendGalleryAsset);
galleryOffset = Number(result.nextOffset || galleryOffset);
galleryHasMore = Boolean(result.hasMore);
galleryTotal = Number(result.total || galleryTotal);
visibleRowLimit += 4;
updateSearchResults();
} catch (error) {
showUploadMessage(error.message || 'Unable to load more media.');
} finally {
loadMoreButton.disabled = false;
}
});
}
updateSearchResults();
function showModal() {
if (window.pulseModal && typeof window.pulseModal.show === 'function') {
window.pulseModal.show(modal);
} else if (window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modal).show();
}
}
if (modal && body) {
document.addEventListener('click', function (event) {
const preview = event.target && event.target.closest ? event.target.closest('[data-media-gallery-preview]') : null;
if (!preview) {
return;
}
const path = preview.getAttribute('data-media-path') || '';
const type = preview.getAttribute('data-media-type') || 'image';
const name = preview.getAttribute('data-media-name') || 'Media preview';
body.replaceChildren();
if (title) {
title.textContent = name;
}
const media = document.createElement(type === 'video' ? 'video' : 'img');
media.src = path;
media.alt = type === 'video' ? '' : name;
if (type === 'video') {
media.controls = true;
media.playsInline = true;
}
body.appendChild(media);
showModal();
});
modal.addEventListener('hidden.bs.modal', function () {
body.replaceChildren();
});
}
@@ -10,6 +10,7 @@ export function createSlideFormRegionHelpers(options) {
var apiSources = Array.isArray(settings.apiSources) ? settings.apiSources : [];
var timetableGroups = Array.isArray(settings.timetableGroups) ? settings.timetableGroups : [];
var weatherLocations = Array.isArray(settings.weatherLocations) ? settings.weatherLocations : [];
var mediaAssets = Array.isArray(settings.mediaAssets) ? settings.mediaAssets : [];
var getRegionTypeModule = typeof settings.getRegionTypeModule === 'function' ? settings.getRegionTypeModule : function () { return null; };
var requestPreviewRender = typeof settings.requestPreviewRender === 'function' ? settings.requestPreviewRender : function () {};
var placeholderUtils = window.placeholderUtils || {};
@@ -570,7 +571,8 @@ export function createSlideFormRegionHelpers(options) {
: [],
sampleDataPreview: buildApiSampleDataMarkup(getCurrentApiConfig(region).source_id, getCurrentApiConfig(region).item_number, getCurrentApiConfig(region).items_path),
timetableGroups: timetableGroups,
weatherLocations: weatherLocations
weatherLocations: weatherLocations,
mediaAssets: mediaAssets
}, existingContent, region.region_type === 'rss' ? rssFeeds : region.region_type === 'timetable' ? timetableGroups : region.region_type === 'weather' ? weatherLocations : apiSources);
}
@@ -612,7 +614,8 @@ export function createSlideFormRegionHelpers(options) {
: [],
sampleDataPreview: buildApiSampleDataMarkup(apiConfig.source_id, apiConfig.item_number, apiItemsPath),
timetableGroups: timetableGroups,
weatherLocations: weatherLocations
weatherLocations: weatherLocations,
mediaAssets: mediaAssets
};
}
+2 -1
View File
@@ -169,6 +169,7 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
apiSources: apiSources,
timetableGroups: timetableGroups,
weatherLocations: weatherLocations,
mediaAssets: Array.isArray(slideEditorData.mediaAssets) ? slideEditorData.mediaAssets : [],
defaultFontSize: DEFAULT_FONT_SIZE,
uploadMaxLabel: uploadMaxLabel,
uploadVideoMaxLabel: uploadVideoMaxLabel,
@@ -927,7 +928,7 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
}
templateSelect.addEventListener('change', renderTemplate);
templateFields.addEventListener('change', function () {
templateFields.addEventListener('change', function (event) {
var webpageInput = event.target && typeof event.target.matches === 'function' && event.target.matches('input[type="url"][name^="region_webpage_"]');
if (webpageInput) {
templateSelectorLock.arm();
@@ -1776,16 +1776,25 @@
renderAddRegionOptions();
}
backgroundInput.addEventListener('change', function () {
var file = backgroundInput.files && backgroundInput.files[0];
if (file) {
updateBackgroundPreview(file);
}
});
if (backgroundInput) {
backgroundInput.addEventListener('change', function () {
var file = backgroundInput.files && backgroundInput.files[0];
if (file) {
updateBackgroundPreview(file);
}
});
}
if (removeBackgroundButton && removeBackgroundFlag) {
removeBackgroundButton.addEventListener('click', function () {
removeBackgroundFlag.checked = true;
backgroundInput.value = '';
var backgroundPathInput = document.getElementById('existing-background-image-path');
if (backgroundPathInput) {
backgroundPathInput.value = '';
backgroundPathInput.dispatchEvent(new Event('change', { bubbles: true }));
}
if (backgroundInput) {
backgroundInput.value = '';
}
backgroundPreview.removeAttribute('src');
backgroundPreview.style.display = 'none';
backgroundEmpty.style.display = 'block';