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:
+238
-65
@@ -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) {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user