CI / build (push) Failing after 6m2s
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.
276 lines
10 KiB
JavaScript
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();
|
|
}
|
|
});
|
|
});
|