Files
bruno f753b27542
CI / build (push) Failing after 6m2s
Add asset pipeline and professionalize theme
Introduce a Node build system for minified CSS/JS with content hashes,
add PWA support, i18n helpers, accessibility improvements, and more
translatable strings. Move theme configuration from bookmarks to
localStorage, add Playwright tests, and set up CI workflows.
2026-08-09 09:25:03 -04:00

276 lines
10 KiB
JavaScript

// =====================================================================
// 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();
}
});
});