// ===================================================================== // Bulk actions (multi-select) for Shaarli Pro. // ===================================================================== document.addEventListener('DOMContentLoaded', () => { 'use strict'; const i18n = window.ShaarliProI18n || { t: function (k) { return k; } }; const selectModeBtn = document.getElementById('select-mode-btn'); const bulkActionsBar = document.getElementById('bulk-actions-bar'); const bulkCount = document.getElementById('bulk-count'); const bulkSelectAll = document.getElementById('bulk-select-all'); const bulkCancel = document.getElementById('bulk-cancel'); const bulkDelete = document.getElementById('bulk-delete'); const bulkPublic = document.getElementById('bulk-public'); const bulkPrivate = document.getElementById('bulk-private'); const bulkModal = document.getElementById('bulk-confirm-modal'); const bulkModalTitle = document.getElementById('bulk-modal-title'); const bulkModalSubtitle = document.getElementById('bulk-modal-subtitle'); const bulkModalList = document.getElementById('bulk-modal-list'); const bulkModalConfirm = document.getElementById('bulk-modal-confirm'); const bulkModalCancel = document.getElementById('bulk-modal-cancel'); const bulkModalClose = document.getElementById('bulk-modal-close'); let selectionMode = false; let selectedIds = new Set(); let pendingBulkAction = null; let bulkFocusCleanup = null; const a11y = window.ShaarliProA11y || { focusFirst: function () {}, trapFocus: function () { return function () {}; } }; const BULK_ACTION_COPY = { delete: { title: (count) => i18n.t('Are you sure to delete {count} link{plural}?', { count: count, plural: count > 1 ? 's' : '' }), subtitle: i18n.t('The following links will be irretrievably deleted:'), confirm: (count) => i18n.t('DELETE {count} LINK{plural}', { count: count, plural: count > 1 ? 'S' : '' }).toUpperCase(), confirmClass: 'bulk-confirm-delete' }, public: { title: (count) => i18n.t('Are you sure to set {count} link{plural} public?', { count: count, plural: count > 1 ? 's' : '' }), subtitle: i18n.t('The following links will be set as public:'), confirm: (count) => i18n.t('SET {count} LINK{plural} PUBLIC', { count: count, plural: count > 1 ? 'S' : '' }).toUpperCase(), confirmClass: 'bulk-confirm-public' }, private: { title: (count) => i18n.t('Are you sure to set {count} link{plural} private?', { count: count, plural: count > 1 ? 's' : '' }), subtitle: i18n.t('The following links will be set as private:'), confirm: (count) => i18n.t('SET {count} LINK{plural} PRIVATE', { count: count, plural: count > 1 ? 'S' : '' }).toUpperCase(), confirmClass: 'bulk-confirm-private' } }; function updateBulkUI() { if (bulkCount) { bulkCount.textContent = selectedIds.size; } document.querySelectorAll('.link-outer').forEach(card => { const id = card.dataset.id; if (selectedIds.has(id)) { card.classList.add('selected'); } else { card.classList.remove('selected'); } }); document.querySelectorAll('.link-checkbox').forEach(cb => { cb.checked = selectedIds.has(cb.dataset.id); }); } function enterSelectionMode() { selectionMode = true; document.body.classList.add('selection-mode'); bulkActionsBar?.classList.add('show'); selectModeBtn?.classList.add('active'); } function exitSelectionMode() { selectionMode = false; selectedIds.clear(); document.body.classList.remove('selection-mode'); bulkActionsBar?.classList.remove('show'); selectModeBtn?.classList.remove('active'); updateBulkUI(); } function toggleSelection(id) { if (selectedIds.has(id)) { selectedIds.delete(id); } else { selectedIds.add(id); } if (selectedIds.size > 0 && !selectionMode) { enterSelectionMode(); } else if (selectedIds.size === 0 && selectionMode) { exitSelectionMode(); } updateBulkUI(); } selectModeBtn?.addEventListener('click', () => { if (selectionMode) { exitSelectionMode(); } else { enterSelectionMode(); } }); bulkCancel?.addEventListener('click', exitSelectionMode); bulkSelectAll?.addEventListener('click', () => { document.querySelectorAll('.link-outer').forEach(card => { if (card.dataset.id) { selectedIds.add(card.dataset.id); } }); if (!selectionMode && selectedIds.size > 0) { enterSelectionMode(); } updateBulkUI(); }); document.addEventListener('change', (e) => { if (e.target.classList.contains('link-checkbox')) { toggleSelection(e.target.dataset.id); } }); document.addEventListener('click', (e) => { if (!selectionMode) return; const card = e.target.closest('.link-outer'); if (card && card.dataset.id) { if (e.target.closest('.link-actions') || e.target.closest('.link-hover-actions') || e.target.tagName === 'A' || e.target.tagName === 'INPUT' || e.target.closest('label')) { return; } toggleSelection(card.dataset.id); } }); function getSelectedIds() { return Array.from(selectedIds); } function populateBulkModalList(ids) { if (!bulkModalList) return; bulkModalList.innerHTML = ''; ids.forEach((id) => { const card = document.querySelector(`.link-outer[data-id="${id}"]`); const title = card?.querySelector('.link-title')?.textContent?.trim() || card?.querySelector('.link-url')?.textContent?.trim() || i18n.t('Untitled link'); const item = document.createElement('div'); item.className = 'bulk-modal-item'; const idSpan = document.createElement('span'); idSpan.className = 'bulk-modal-item-id'; idSpan.textContent = `#${id}`; const titleSpan = document.createElement('span'); titleSpan.className = 'bulk-modal-item-title'; titleSpan.textContent = title; item.appendChild(idSpan); item.appendChild(titleSpan); bulkModalList.appendChild(item); }); } function openBulkModal(actionType) { if (!bulkModal || selectedIds.size === 0) return; const meta = BULK_ACTION_COPY[actionType]; if (!meta) return; const ids = getSelectedIds(); pendingBulkAction = actionType; if (bulkModalTitle) { bulkModalTitle.textContent = meta.title(ids.length); } if (bulkModalSubtitle) { bulkModalSubtitle.textContent = meta.subtitle; } if (bulkModalConfirm) { bulkModalConfirm.textContent = meta.confirm(ids.length); bulkModalConfirm.className = `bulk-modal-btn bulk-modal-confirm ${meta.confirmClass}`; } populateBulkModalList(ids); bulkModal.classList.add('show'); bulkModal.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; a11y.focusFirst(bulkModal); bulkFocusCleanup = a11y.trapFocus(bulkModal); } function closeBulkModal() { if (!bulkModal) return; pendingBulkAction = null; bulkModal.classList.remove('show'); bulkModal.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; if (typeof bulkFocusCleanup === 'function') { bulkFocusCleanup(); bulkFocusCleanup = null; } } function buildIdsParam(ids) { return ids.map(id => encodeURIComponent(id)).join('+'); } function executeBulkAction() { if (!pendingBulkAction || selectedIds.size === 0) return; const ids = getSelectedIds(); const token = (window.shaarli?.token || '').trim() || document.querySelector('input[name="token"]')?.value || ''; if (!token) { alert(i18n.t('Missing security token. Please refresh the page and try again.')); return; } const basePath = window.shaarli?.basePath || ''; const idsParam = buildIdsParam(ids); let targetUrl = ''; if (pendingBulkAction === 'delete') { targetUrl = `${basePath}/admin/shaare/delete?id=${idsParam}&token=${encodeURIComponent(token)}`; } else { const visibility = pendingBulkAction === 'public' ? 'public' : 'private'; targetUrl = `${basePath}/admin/shaare/visibility?token=${encodeURIComponent(token)}&newVisibility=${visibility}&id=${idsParam}`; } closeBulkModal(); window.location.href = targetUrl; } bulkDelete?.addEventListener('click', () => { if (selectedIds.size === 0) return; openBulkModal('delete'); }); bulkPublic?.addEventListener('click', () => { if (selectedIds.size === 0) return; openBulkModal('public'); }); bulkPrivate?.addEventListener('click', () => { if (selectedIds.size === 0) return; openBulkModal('private'); }); bulkModalConfirm?.addEventListener('click', executeBulkAction); bulkModalCancel?.addEventListener('click', closeBulkModal); bulkModalClose?.addEventListener('click', closeBulkModal); bulkModal?.addEventListener('click', (event) => { if (event.target === bulkModal) { closeBulkModal(); } }); document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && bulkModal?.classList.contains('show')) { closeBulkModal(); } }); });