feat: implémenter modal confirmation actions bulk avec preview liste liens (ID + titre), styles dédiés (max-width 520px, list scrollable 320px, boutons colorés par action delete/public/private), synchronisation mode thème vers bookmark via fetch/POST formulaire, refactoring actions bulk (delete/visibility) avec URLs GET params au lieu de forms POST, ajout token shaarli dans includes.html, correction tag config bookmark (shaarit_config→themes), et support fermeture modal ESC/overlay-click

This commit is contained in:
2026-04-20 13:54:33 -04:00
parent 1c469b2329
commit bf1c2c6172
5 changed files with 387 additions and 71 deletions
+238 -65
View File
@@ -65,6 +65,77 @@ document.addEventListener('DOMContentLoaded', () => {
}
}
async function syncModeToBookmark(mode) {
try {
if (!window.shaarli || !window.shaarli.basePath) return;
// Fetch current config bookmark
const res = await fetch(window.shaarli.basePath + '/?searchtags=themes');
const text = await res.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
let config = { version: 2, themes: [], default: 'DEFAULT', mode: mode };
let editUrl = null;
// Find existing config
const linkCard = doc.querySelector('.linklist-item');
if (linkCard) {
const descEl = linkCard.querySelector('.link-description p');
if (descEl) {
try {
const parsed = JSON.parse(descEl.textContent.trim());
// Preserve existing data
if (parsed.themes) config.themes = parsed.themes;
if (parsed.default) config.default = parsed.default;
// Update mode
config.mode = mode;
} catch(e) {}
}
const editBtn = linkCard.querySelector('.link-action-edit');
if (editBtn) editUrl = editBtn.getAttribute('href');
}
// Prepare form data
const formData = new FormData();
formData.append('lf_title', 'themes');
formData.append('lf_url', 'https://shaarit.app/config/themes');
formData.append('lf_tags', 'themes');
formData.append('lf_description', JSON.stringify(config));
formData.append('lf_private', 'on');
formData.append('save_edit', 'Save');
let token = '';
if (editUrl) {
// Update existing
const editRes = await fetch(editUrl);
const editDoc = parser.parseFromString(await editRes.text(), 'text/html');
const tokenEl = editDoc.querySelector('input[name="token"]');
const idEl = editDoc.querySelector('input[name="lf_id"]');
if (tokenEl) token = tokenEl.value;
if (idEl) formData.append('lf_id', idEl.value);
} else {
// Create new
const addRes = await fetch(window.shaarli.basePath + '/admin/add-shaare');
const addDoc = parser.parseFromString(await addRes.text(), 'text/html');
const tokenEl = addDoc.querySelector('input[name="token"]');
if (tokenEl) token = tokenEl.value;
}
if (token) {
formData.append('token', token);
await fetch(window.shaarli.basePath + '/admin/shaare', {
method: 'POST',
body: formData
});
console.log('[shaarit] Mode synced to bookmark:', mode);
}
} catch (e) {
console.error('[shaarit] Failed to sync mode to bookmark', e);
}
}
// Handle themeChanged event fired by tools.html
window.addEventListener('themeChanged', () => {
updateTheme(localStorage.getItem('theme') || 'dark');
@@ -80,6 +151,8 @@ document.addEventListener('DOMContentLoaded', () => {
if (checkThemeRestrictions()) return; // Extra safety
const next = themeCheckbox.checked ? 'dark' : 'light';
updateTheme(next);
// Sync mode to bookmark in background
syncModeToBookmark(next);
});
}
@@ -1041,9 +1114,38 @@ document.addEventListener('DOMContentLoaded', () => {
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;
const BULK_ACTION_COPY = {
delete: {
title: (count) => `Are you sure to delete ${count} link${count > 1 ? 's' : ''}?`,
subtitle: 'The following links will be irretrievably deleted:',
confirm: (count) => `DELETE ${count} LINK${count > 1 ? 'S' : ''}`,
confirmClass: 'bulk-confirm-delete'
},
public: {
title: (count) => `Are you sure to set ${count} link${count > 1 ? 's' : ''} public?`,
subtitle: 'The following links will be set as public:',
confirm: (count) => `SET ${count} LINK${count > 1 ? 'S' : ''} PUBLIC`,
confirmClass: 'bulk-confirm-public'
},
private: {
title: (count) => `Are you sure to set ${count} link${count > 1 ? 's' : ''} private?`,
subtitle: 'The following links will be set as private:',
confirm: (count) => `SET ${count} LINK${count > 1 ? 'S' : ''} PRIVATE`,
confirmClass: 'bulk-confirm-private'
}
};
function updateBulkUI() {
if (bulkCount) {
@@ -1114,6 +1216,9 @@ document.addEventListener('DOMContentLoaded', () => {
selectedIds.add(card.dataset.id);
}
});
if (!selectionMode && selectedIds.size > 0) {
enterSelectionMode();
}
updateBulkUI();
});
@@ -1140,72 +1245,129 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
// Bulk actions
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() ||
'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';
}
function closeBulkModal() {
if (!bulkModal) return;
pendingBulkAction = null;
bulkModal.classList.remove('show');
bulkModal.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
}
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('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;
}
// Bulk actions with confirmation modal
bulkDelete?.addEventListener('click', () => {
if (selectedIds.size === 0) return;
if (!confirm(`Delete ${selectedIds.size} bookmark(s)?`)) return;
// Submit form with selected IDs
const form = document.createElement('form');
form.method = 'POST';
form.action = shaarli.basePath + '/admin/shaare/delete';
selectedIds.forEach(id => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'id[]';
input.value = id;
form.appendChild(input);
});
const tokenInput = document.createElement('input');
tokenInput.type = 'hidden';
tokenInput.name = 'token';
tokenInput.value = document.querySelector('input[name="token"]')?.value || '';
form.appendChild(tokenInput);
document.body.appendChild(form);
form.submit();
openBulkModal('delete');
});
bulkPublic?.addEventListener('click', () => {
if (selectedIds.size === 0) return;
bulkVisibilityChange('public');
openBulkModal('public');
});
bulkPrivate?.addEventListener('click', () => {
if (selectedIds.size === 0) return;
bulkVisibilityChange('private');
openBulkModal('private');
});
function bulkVisibilityChange(visibility) {
const form = document.createElement('form');
form.method = 'POST';
form.action = shaarli.basePath + '/admin/shaare/visibility';
selectedIds.forEach(id => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'id[]';
input.value = id;
form.appendChild(input);
});
const visInput = document.createElement('input');
visInput.type = 'hidden';
visInput.name = 'visibility';
visInput.value = visibility;
form.appendChild(visInput);
const tokenInput = document.createElement('input');
tokenInput.type = 'hidden';
tokenInput.name = 'token';
tokenInput.value = document.querySelector('input[name="token"]')?.value || '';
form.appendChild(tokenInput);
document.body.appendChild(form);
form.submit();
}
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();
}
});
// ===== Thumbnail Update =====
const thumbnailsPage = document.querySelector('.page-thumbnails');
@@ -3313,7 +3475,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Run sync after a slight delay to avoid blocking initial load
if (!window.shaarli || !window.shaarli.basePath) return;
fetch(window.shaarli.basePath + '/?searchtags=shaarit_config')
fetch(window.shaarli.basePath + '/?searchtags=themes')
.then(res => res.text())
.then(text => {
const parser = new DOMParser();
@@ -3324,23 +3486,34 @@ document.addEventListener('DOMContentLoaded', () => {
if (descEl) {
try {
const config = JSON.parse(descEl.textContent.trim());
const remoteThemeId = config.theme;
const remoteThemeId = config.default;
const remoteMode = config.mode;
const localThemeId = localStorage.getItem('shaarit_theme_id') || 'DEFAULT';
const localMode = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
// Sync theme if different
if (remoteThemeId && remoteThemeId !== localThemeId) {
localStorage.setItem('shaarit_theme_id', remoteThemeId);
document.documentElement.setAttribute('data-theme-id', remoteThemeId);
// Enforce light/dark constraints based on new theme
const darkOnlyThemes = ['LINEAR', 'SPOTIFY', 'NOTION', 'DISCORD', 'DRACULA', 'ONE_DARK_PRO', 'TOKYO_NIGHT', 'NORD', 'NIGHT_OWL', 'ANTHRACITE', 'CYBERPUNK', 'NAVY_ELEGANCE', 'EARTHY'];
if (darkOnlyThemes.indexOf(remoteThemeId) !== -1) {
localStorage.setItem('theme', 'dark');
document.documentElement.setAttribute('data-theme', 'dark');
}
window.dispatchEvent(new Event('themeChanged'));
console.log('[shaarit] Theme synced from bookmark:', remoteThemeId);
}
// Sync mode if different
if (remoteMode && remoteMode !== localMode) {
localStorage.setItem('theme', remoteMode);
document.documentElement.setAttribute('data-theme', remoteMode);
console.log('[shaarit] Mode synced from bookmark:', remoteMode);
}
// Enforce light/dark constraints based on synced theme
const darkOnlyThemes = ['LINEAR', 'SPOTIFY', 'NOTION', 'DISCORD', 'DRACULA', 'ONE_DARK_PRO', 'TOKYO_NIGHT', 'NORD', 'NIGHT_OWL', 'ANTHRACITE', 'CYBERPUNK', 'NAVY_ELEGANCE', 'EARTHY'];
const syncedThemeId = remoteThemeId || localThemeId;
if (darkOnlyThemes.indexOf(syncedThemeId) !== -1) {
localStorage.setItem('theme', 'dark');
document.documentElement.setAttribute('data-theme', 'dark');
}
window.dispatchEvent(new Event('themeChanged'));
} catch(e) {}
}
}