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]'); const addMediaButton = gallery && gallery.querySelector('[data-media-library-add]'); 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 uploadModal = document.getElementById('media-library-upload-modal'); 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 showModal(target) { if (window.pulseModal && typeof window.pulseModal.show === 'function') { window.pulseModal.show(target); } else if (window.bootstrap && window.bootstrap.Modal) { window.bootstrap.Modal.getOrCreateInstance(target).show(); } } function hideModal(target) { if (window.pulseModal && typeof window.pulseModal.hide === 'function') { window.pulseModal.hide(target); } else if (window.bootstrap && window.bootstrap.Modal) { window.bootstrap.Modal.getOrCreateInstance(target).hide(); } } if (addMediaButton && uploadModal) { addMediaButton.addEventListener('click', function () { showModal(uploadModal); }); } 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 = ''; 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 = 'Media is in use'; 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 = ''; 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'); showModal(cropModal); } } 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) { hideModal(cropModal); 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); hideModal(cropModal); submitUpload(); }, outputType, 0.92); }); } if (cropCancel) { cropCancel.addEventListener('click', function () { clearCropper(); if (uploadInput) uploadInput.value = ''; if (uploadName) uploadName.textContent = 'No file selected'; hideModal(cropModal); }); } 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 ? 'Cancel' : '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)); card.classList.toggle('media-gallery-card-selection-disabled', selectionMode && item.dataset.mediaUsed === 'true'); } }); } 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') { showUploadMessage('This media is in use and cannot be selected.'); 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 showPreviewModal() { 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); showPreviewModal(); }); modal.addEventListener('hidden.bs.modal', function () { body.replaceChildren(); }); }