// 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' ? '' : ''; return '
' + '
'; }).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, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } 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 = '
' + (isVideo ? '' : 'Selected ' + mediaLabel + ' preview') + '' + '
'; } 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 = ''; }); })();