Files
pulse-signage/src/web/public/js/media-picker.js
T
lzstealth 2fd059d35a
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m13s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 33s
Release v2.15.5
2026-10-17 16:55:37 +01:00

436 lines
17 KiB
JavaScript

// 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 explicitInputId = activeButton.getAttribute('data-media-picker-file-input');
if (explicitInputId) {
var explicitInput = document.getElementById(explicitInputId);
if (explicitInput) {
return explicitInput;
}
}
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);
var isVideo = type === 'video';
var isQrImage = String(hidden.name || '').indexOf('existing_region_qr_image_') === 0;
var mediaLabel = isVideo ? 'video' : isQrImage ? 'QR image' : 'image';
var removeAction = isVideo ? 'region-video' : isQrImage ? 'region-qr-image' : 'region-image';
var regionId = escapeHtml(region.getAttribute('data-region-id') || '');
previewBox.innerHTML = '<div class="slide-image-region-preview-shell" data-remove-' + removeAction + '="' + regionId + '" role="button" tabindex="0" aria-label="Remove ' + mediaLabel + '">' +
(isVideo
? '<video class="slide-image-region-preview" src="' + safePath + '" muted playsinline preload="metadata"></video>'
: '<img class="slide-image-region-preview" src="' + safePath + '" alt="Selected ' + mediaLabel + ' preview" />') +
'<span class="slide-image-region-preview-remove" aria-hidden="true"><i class="bi bi-trash3" aria-hidden="true"></i></span>' +
'</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';
}
}
function queueUploadCleanup(input, uploadPath) {
var path = String(uploadPath || '').trim();
var cleanupUrl = String(input && input.dataset.uploadCleanupUrl || '').trim();
if (!path || !cleanupUrl) {
return;
}
var payload = JSON.stringify({ uploadPaths: [path] });
if (navigator.sendBeacon) {
try {
navigator.sendBeacon(cleanupUrl, new Blob([payload], { type: 'application/json' }));
return;
} catch (_error) {
// Fall through to fetch when sendBeacon is unavailable.
}
}
fetch(cleanupUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: payload,
credentials: 'same-origin',
keepalive: true
}).catch(function () {});
}
async function uploadSelectedFile(input, file) {
var uploadUrl = String(input.dataset.uploadUrl || '').trim();
var hidden = document.getElementById(String(input.dataset.uploadHidden || '').trim());
if (!uploadUrl || !hidden || !file) {
return;
}
var maxBytes = Number(input.dataset.imageMaxBytes || 0);
if (maxBytes > 0 && file.size > maxBytes) {
input.value = '';
window.PulseUpload.showMessage('This file is larger than the current upload limit of ' + window.PulseUpload.formatUploadLimit(maxBytes) + '.');
return;
}
var formData = new FormData();
formData.append('file', file, file.name || 'upload');
input.setCustomValidity('Upload is still in progress.');
try {
var response = await fetch(uploadUrl, {
method: 'POST',
body: formData,
credentials: 'same-origin',
headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
});
var payload = await response.json().catch(function () { return {}; });
if (!response.ok || !payload.path) {
throw new Error(payload.error || 'Unable to upload media.');
}
var previousPath = String(input.dataset.uploadedPath || '').trim();
if (previousPath && previousPath !== payload.path) {
queueUploadCleanup(input, previousPath);
}
input.dataset.uploadedPath = payload.path;
input.dataset.uploadedNeedsCleanup = '1';
input.value = '';
input.setCustomValidity('');
hidden.value = payload.path;
hidden.dispatchEvent(new Event('change', { bubbles: true }));
updatePreview(hidden, payload.path, 'image');
} catch (error) {
input.value = '';
var currentPath = String(hidden.value || '').trim();
var backgroundPreview = document.getElementById('background-preview');
var backgroundEmpty = document.getElementById('background-empty');
if (currentPath) {
updatePreview(hidden, currentPath, 'image');
} else if (backgroundPreview) {
backgroundPreview.removeAttribute('src');
backgroundPreview.style.display = 'none';
if (backgroundEmpty) backgroundEmpty.style.display = 'flex';
}
input.setCustomValidity('');
window.PulseUpload.showMessage(error.message || 'Unable to upload media.');
} finally {
}
}
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();
});
document.addEventListener('click', function (event) {
var removeBackground = event.target && event.target.closest ? event.target.closest('#remove-background-image') : null;
if (!removeBackground) {
return;
}
var input = document.getElementById('background-image');
if (input && input.dataset.uploadedNeedsCleanup === '1') {
queueUploadCleanup(input, input.dataset.uploadedPath);
delete input.dataset.uploadedNeedsCleanup;
delete input.dataset.uploadedPath;
}
});
if (search) search.addEventListener('input', resetVisibleItemLimit);
if (sort) sort.addEventListener('change', resetVisibleItemLimit);
document.addEventListener('change', function (event) {
var input = event.target;
if (!input || input.type !== 'file' || !input.dataset) {
return;
}
var file = input.files && input.files[0];
if (input.dataset.uploadUrl) {
input.setCustomValidity('');
if (file) {
uploadSelectedFile(input, file);
}
return;
}
if (!input.dataset.uploadMaxBytes) {
return;
}
var maxBytes = Number(input.dataset.uploadMaxBytes);
if (!file || !Number.isFinite(maxBytes) || maxBytes <= 0 || file.size <= maxBytes) {
input.setCustomValidity('');
return;
}
input.value = '';
input.setCustomValidity('Image must be ' + Math.round(maxBytes / 1024 / 1024) + ' MB or smaller.');
input.reportValidity();
});
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;
}
var input = getActiveFileInput();
if (input && input.dataset.uploadedNeedsCleanup === '1' && input.dataset.uploadedPath !== pendingPath) {
queueUploadCleanup(input, input.dataset.uploadedPath);
delete input.dataset.uploadedNeedsCleanup;
delete input.dataset.uploadedPath;
}
hidden.value = pendingPath;
updatePreview(hidden, pendingPath, pendingType);
hidden.dispatchEvent(new Event('change', { bubbles: true }));
closePicker();
});
if (cancelButton) cancelButton.addEventListener('click', closePicker);
document.querySelectorAll('input[type="file"][data-upload-url]').forEach(function (input) {
var form = input.form;
if (form) {
form.addEventListener('submit', function () {
delete input.dataset.uploadedNeedsCleanup;
});
}
});
window.addEventListener('beforeunload', function () {
document.querySelectorAll('input[type="file"][data-upload-url][data-uploaded-needs-cleanup="1"]').forEach(function (input) {
queueUploadCleanup(input, input.dataset.uploadedPath);
});
});
modal.addEventListener('hidden.bs.modal', function () {
activeButton = null;
currentPath = '';
pendingPath = '';
pendingType = '';
});
})();