283 lines
11 KiB
JavaScript
283 lines
11 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, '&').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 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 = '';
|
|
});
|
|
})();
|