feat: implémenter vue Archive complète avec navigation sidebar/header, filtrage notes archivées via tag shaarli-archive, bouton archivage/désarchivage dans modal et hover actions, styles dédiés avec titre centré et icône archive, layout responsive identique à Notes, et correction logique filtrage renderNotes pour exclusion mutuelle archive/notes normales

This commit is contained in:
Bruno Charest 2026-02-20 14:22:25 -05:00
parent 777fe93b7e
commit 4280ae171b
3 changed files with 343 additions and 7 deletions

View File

@ -2112,3 +2112,71 @@ body.view-notes .content-container {
align-items: center; align-items: center;
gap: 10px; gap: 10px;
} }
/* --- ARCHIVE VIEW --- */
body.view-archive .content-container {
padding: 2rem;
background-color: var(--bg-body);
min-height: 100vh;
}
[data-theme="dark"] body.view-archive .content-container {
background-color: var(--bg-body);
}
/* Archive Title */
.archive-title-container {
text-align: center;
margin-bottom: 1rem;
}
.archive-title {
font-size: 1.75rem;
font-weight: 600;
color: var(--text-color, #202124);
margin: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.archive-title i {
font-size: 1.5rem;
color: var(--primary-color, #2563eb);
}
[data-theme="dark"] .archive-title {
color: #e8eaed;
}
.archive-subtitle {
font-size: 0.875rem;
color: var(--text-light, #5f6368);
margin: 0.25rem 0 0 0;
}
[data-theme="dark"] .archive-subtitle {
color: #9aa0a6;
}
/* Archive wrapper */
.archive-wrapper {
max-width: 1200px;
margin: 0 auto;
}
/* Archive top bar adjustments */
.archive-top-bar {
flex-direction: column;
align-items: center;
padding-right: 0;
gap: 1rem;
}
.archive-top-bar .notes-tools {
position: relative;
right: auto;
top: auto;
transform: none;
}

View File

@ -33,6 +33,13 @@ document.addEventListener("DOMContentLoaded", function () {
if (typeof initTagDisplayAndRemoval === "function") { if (typeof initTagDisplayAndRemoval === "function") {
initTagDisplayAndRemoval(); initTagDisplayAndRemoval();
} }
} else if (searchTags === "shaarli-archive") {
// Vue Archive - similaire à Notes mais pour les notes archivées
initArchiveView(linkList, container);
// Puis supprimer les tags techniques de l'affichage
if (typeof initTagDisplayAndRemoval === "function") {
initTagDisplayAndRemoval();
}
} else { } else {
// Vue standard : supprimer les tags techniques // Vue standard : supprimer les tags techniques
if (typeof initTagDisplayAndRemoval === "function") { if (typeof initTagDisplayAndRemoval === "function") {
@ -1964,11 +1971,11 @@ function initNoteView(linkList, container) {
const editUrl = modalCard.dataset.editUrl; const editUrl = modalCard.dataset.editUrl;
if (!noteId || !editUrl) return; if (!noteId || !editUrl) return;
addTagToNote(editUrl, "shaarli-archiver") addTagToNote(editUrl, "shaarli-archive")
.then(() => { .then(() => {
if (modalOverlay.currentNote) { if (modalOverlay.currentNote) {
if (!modalOverlay.currentNote.tags.includes("shaarli-archiver")) { if (!modalOverlay.currentNote.tags.includes("shaarli-archive")) {
modalOverlay.currentNote.tags.push("shaarli-archiver"); modalOverlay.currentNote.tags.push("shaarli-archive");
} }
} }
@ -1993,7 +2000,231 @@ function initNoteView(linkList, container) {
}); });
document.addEventListener("click", (e) => { document.addEventListener("click", (e) => {
if (e.target.closest(".note-hover-actions .palette-popup") || e.target.closest('.note-hover-actions [id^="palette-"]')) { if (!e.target.closest(".note-hover-actions .palette-popup") || e.target.closest('.note-hover-actions [id^="palette-"]')) {
return;
}
document.querySelectorAll(".note-hover-actions .palette-popup.open").forEach((p) => {
p.classList.remove("open");
});
});
}
/**
* Initialize the Archive view (similar to Notes but for archived notes)
*/
function initArchiveView(linkList, container) {
document.body.classList.add("view-notes", "view-archive");
// Hide standard toolbar
const toolbar = document.querySelector(".content-toolbar");
if (toolbar) toolbar.style.display = "none";
// 1. Create Layout Wrapper
const wrapper = document.createElement("div");
wrapper.className = "notes-wrapper archive-wrapper";
// 2. Create Title Area (Top)
const topBar = document.createElement("div");
topBar.className = "notes-top-bar archive-top-bar";
// Title for archive view
const titleContainer = document.createElement("div");
titleContainer.className = "archive-title-container";
titleContainer.innerHTML = `
<h1 class="archive-title"><i class="mdi mdi-archive-arrow-down-outline"></i> Archive</h1>
<p class="archive-subtitle">Notes archivées</p>
`;
topBar.appendChild(titleContainer);
// View Toggle and other tools
const tools = document.createElement("div");
tools.className = "notes-tools";
tools.innerHTML = `
<button class="icon-btn active" id="btn-view-grid" title="Vue grille"><i class="mdi mdi-view-dashboard-outline"></i></button>
<button class="icon-btn" id="btn-view-list" title="Vue liste"><i class="mdi mdi-view-agenda-outline"></i></button>
`;
topBar.appendChild(tools);
wrapper.appendChild(topBar);
// 3. Content Area
const contentArea = document.createElement("div");
contentArea.className = "notes-content-area";
const links = Array.from(linkList.querySelectorAll(".link-outer"));
const notes = links.map((link) => parseNoteFromLink(link));
// Filter only archived notes
const archivedNotes = notes.filter((note) => (note.tags || []).includes("shaarli-archive"));
// Initial Render (Grid)
renderNotes(contentArea, archivedNotes, "grid", true); // true = archive mode
wrapper.appendChild(contentArea);
// Replace original list
linkList.style.display = "none";
if (linkList.parentNode) {
linkList.parentNode.insertBefore(wrapper, linkList);
} else {
container.appendChild(wrapper);
}
// Modal Container (reuse the same modal as notes)
const modalOverlay = document.createElement("div");
modalOverlay.className = "note-modal-overlay";
modalOverlay.innerHTML = `
<div class="note-modal note-color-default">
<div class="note-modal-header">
<h2 class="note-title" id="note-modal-title"></h2>
<button type="button" class="note-modal-pin-toggle" id="note-modal-pin" title="Épingler">
<i class="mdi mdi-pin-outline"></i>
</button>
</div>
<div class="note-modal-content"></div>
<div class="note-modal-tags is-empty" id="note-modal-tags"></div>
<div class="note-modal-actions">
<div class="note-modal-actions-left">
<div class="note-modal-color-picker">
<button type="button" id="note-modal-color-btn" title="Couleur"><i class="mdi mdi-palette-outline"></i></button>
<div class="palette-popup note-modal-palette" id="note-modal-color-popup"></div>
</div>
<button type="button" title="Rappel"><i class="mdi mdi-bell-outline"></i></button>
<button type="button" title="Collaborateur"><i class="mdi mdi-account-plus-outline"></i></button>
<button type="button" title="Image"><i class="mdi mdi-image-outline"></i></button>
<button type="button" id="note-modal-unarchive" title="Désarchiver"><i class="mdi mdi-archive-arrow-up-outline"></i></button>
<a href="#" id="note-modal-edit" title="Modifier"><i class="mdi mdi-pencil-outline"></i></a>
<button type="button" id="note-modal-delete" title="Supprimer"><i class="mdi mdi-dots-vertical"></i></button>
</div>
<button type="button" class="note-modal-close-btn" id="note-modal-close">Fermer</button>
</div>
</div>
`;
document.body.appendChild(modalOverlay);
// Event Listeners for Toggles
const btnGrid = wrapper.querySelector("#btn-view-grid");
const btnList = wrapper.querySelector("#btn-view-list");
btnGrid.addEventListener("click", () => {
btnGrid.classList.add("active");
btnList.classList.remove("active");
renderNotes(contentArea, archivedNotes, "grid", true);
});
btnList.addEventListener("click", () => {
btnList.classList.add("active");
btnGrid.classList.remove("active");
renderNotes(contentArea, archivedNotes, "list", true);
});
// Close Modal
modalOverlay.querySelector("#note-modal-close").addEventListener("click", () => {
modalOverlay.classList.remove("open");
});
modalOverlay.addEventListener("click", (e) => {
if (e.target === modalOverlay) modalOverlay.classList.remove("open");
});
const modalPinBtn = modalOverlay.querySelector("#note-modal-pin");
const modalColorBtn = modalOverlay.querySelector("#note-modal-color-btn");
const modalColorPopup = modalOverlay.querySelector("#note-modal-color-popup");
modalColorBtn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const modalCard = modalOverlay.querySelector(".note-modal");
openBackgroundStudioPanel({
anchorEl: modalColorBtn,
mode: "modal",
entityId: modalCard ? modalCard.dataset.noteId || "" : "",
editUrl: modalCard ? modalCard.dataset.editUrl || "" : "",
currentColor: modalCard ? getElementVisualColor(modalCard) : "default",
currentFilter: modalCard ? getElementVisualFilter(modalCard) : "none",
currentBackground: modalCard ? getElementVisualBackground(modalCard) : "none",
currentFontColor: modalCard ? getElementVisualFontColor(modalCard) : "auto",
title: "Mes images & couleurs",
});
});
modalPinBtn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const modalCard = modalOverlay.querySelector(".note-modal");
const noteId = modalCard.dataset.noteId;
const editUrl = modalCard.dataset.editUrl;
if (!noteId || !editUrl) return;
togglePinTag(noteId, editUrl, modalPinBtn);
const isPinned = modalPinBtn.classList.contains("active");
let tags = (modalCard.dataset.tags || "")
.split("||")
.filter((t) => t);
if (isPinned) {
if (!tags.includes("shaarli-pin")) tags.push("shaarli-pin");
} else {
tags = tags.filter((t) => t !== "shaarli-pin");
}
modalCard.dataset.tags = tags.join("||");
renderModalTags(modalOverlay.querySelector("#note-modal-tags"), tags);
if (modalOverlay.currentNote) {
modalOverlay.currentNote.isPinned = isPinned;
modalOverlay.currentNote.tags = tags;
}
});
modalOverlay.querySelector("#note-modal-delete").addEventListener("click", () => {
const modalCard = modalOverlay.querySelector(".note-modal");
const deleteUrl = modalCard.dataset.deleteUrl;
if (deleteUrl && deleteUrl !== "#") {
window.location.href = deleteUrl;
}
});
// Unarchive button in modal
modalOverlay.querySelector("#note-modal-unarchive").addEventListener("click", (e) => {
e.preventDefault();
const modalCard = modalOverlay.querySelector(".note-modal");
const noteId = modalCard.dataset.noteId;
const editUrl = modalCard.dataset.editUrl;
if (!noteId || !editUrl) return;
removeTagFromEntity(editUrl, "shaarli-archive")
.then(() => {
if (modalOverlay.currentNote) {
modalOverlay.currentNote.tags = (modalOverlay.currentNote.tags || []).filter((t) => t !== "shaarli-archive");
}
const noteCard = document.querySelector(`.note-card[data-id="${noteId}"]`);
if (noteCard) noteCard.remove();
const index = archivedNotes.findIndex((n) => String(n.id) === String(noteId));
if (index > -1) archivedNotes.splice(index, 1);
modalOverlay.classList.remove("open");
})
.catch((err) => {
console.error("Error unarchiving note:", err);
alert("Erreur lors du désarchivage de la note.");
});
});
modalOverlay.addEventListener("click", (e) => {
if (!e.target.closest(".note-modal-color-picker")) {
modalColorPopup.classList.remove("open");
}
});
document.addEventListener("click", (e) => {
if (!e.target.closest(".note-hover-actions .palette-popup") || e.target.closest('.note-hover-actions [id^="palette-"]')) {
return; return;
} }
@ -2064,11 +2295,19 @@ function parseNoteFromLink(linkEl) {
return { id, title, descHtml, descText, coverImage, url, tags, color, filter, background, fontColor, editUrl, deleteUrl, pinUrl, isPinned }; return { id, title, descHtml, descText, coverImage, url, tags, color, filter, background, fontColor, editUrl, deleteUrl, pinUrl, isPinned };
} }
function renderNotes(container, notes, viewMode) { function renderNotes(container, notes, viewMode, isArchiveMode = false) {
container.innerHTML = ""; container.innerHTML = "";
container.className = viewMode === "grid" ? "notes-masonry" : "notes-list-view"; container.className = viewMode === "grid" ? "notes-masonry" : "notes-list-view";
const visibleNotes = notes.filter((note) => !(note.tags || []).includes("shaarli-archiver")); // Filter notes based on archive mode
let visibleNotes;
if (isArchiveMode) {
// In archive mode: show only notes with shaarli-archive tag
visibleNotes = notes.filter((note) => (note.tags || []).includes("shaarli-archive"));
} else {
// In normal notes mode: hide archived notes
visibleNotes = notes.filter((note) => !(note.tags || []).includes("shaarli-archive"));
}
// Sort: Pinned items first // Sort: Pinned items first
visibleNotes.sort((a, b) => { visibleNotes.sort((a, b) => {
@ -2143,6 +2382,7 @@ function renderNotes(container, notes, viewMode) {
// Palette Button Logic // Palette Button Logic
const paletteBtnId = `palette-${note.id}`; const paletteBtnId = `palette-${note.id}`;
const archiveBtnId = `archive-${note.id}`;
actions.innerHTML = ` actions.innerHTML = `
<button title="Rappel"><i class="mdi mdi-bell-outline"></i></button> <button title="Rappel"><i class="mdi mdi-bell-outline"></i></button>
@ -2151,7 +2391,7 @@ function renderNotes(container, notes, viewMode) {
<button title="Couleur" id="${paletteBtnId}"><i class="mdi mdi-palette-outline"></i></button> <button title="Couleur" id="${paletteBtnId}"><i class="mdi mdi-palette-outline"></i></button>
</div> </div>
<button title="Image"><i class="mdi mdi-image-outline"></i></button> <button title="Image"><i class="mdi mdi-image-outline"></i></button>
<button title="Archiver"><i class="mdi mdi-archive-arrow-down-outline"></i></button> <button title="Archiver" id="${archiveBtnId}"><i class="mdi mdi-archive-arrow-down-outline"></i></button>
<div class="spacer"></div> <div class="spacer"></div>
<!-- Real Actions --> <!-- Real Actions -->
<a href="${note.pinUrl}" title="${note.isPinned ? "Unpin" : "Pin"}" class="${note.isPinned ? "active" : ""}"><i class="mdi mdi-pin${note.isPinned ? "" : "-outline"}"></i></a> <a href="${note.pinUrl}" title="${note.isPinned ? "Unpin" : "Pin"}" class="${note.isPinned ? "active" : ""}"><i class="mdi mdi-pin${note.isPinned ? "" : "-outline"}"></i></a>
@ -2176,6 +2416,26 @@ function renderNotes(container, notes, viewMode) {
}); });
}); });
// Archive button handler
const archiveBtn = actions.querySelector(`#${archiveBtnId}`);
archiveBtn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
if (!note.editUrl || note.editUrl === "#") return;
addTagToNote(note.editUrl, "shaarli-archive")
.then(() => {
// Remove the card from the view
card.remove();
console.log(`Note ${note.id} archived`);
})
.catch((err) => {
console.error("Error archiving note:", err);
alert("Erreur lors de l'archivage de la note.");
});
});
inner.appendChild(actions); inner.appendChild(actions);
card.appendChild(inner); card.appendChild(inner);

View File

@ -57,6 +57,10 @@ Bookmarklet detection logic
<i class="mdi mdi-note-text-outline" aria-hidden="true"></i> <i class="mdi mdi-note-text-outline" aria-hidden="true"></i>
<span>Notes</span> <span>Notes</span>
</a> </a>
<a href="{$base_path}/?searchtags=shaarli-archive" class="sidebar-link{if="isset($search_tags) && preg_match('/(^|[\s,])shaarli-archive([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Archive">
<i class="mdi mdi-archive-arrow-down-outline" aria-hidden="true"></i>
<span>Archive</span>
</a>
<a href="{$base_path}/?searchtags=readitlater" class="sidebar-link{if="isset($search_tags) && preg_match('/(^|[\s,])readitlater([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Read It Later"> <a href="{$base_path}/?searchtags=readitlater" class="sidebar-link{if="isset($search_tags) && preg_match('/(^|[\s,])readitlater([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Read It Later">
<i class="mdi mdi-bookmark-outline" aria-hidden="true"></i> <i class="mdi mdi-bookmark-outline" aria-hidden="true"></i>
<span>Read It Later</span> <span>Read It Later</span>
@ -151,6 +155,10 @@ Bookmarklet detection logic
<i class="mdi mdi-note-text-outline" aria-hidden="true"></i> <i class="mdi mdi-note-text-outline" aria-hidden="true"></i>
<span>NOTES</span> <span>NOTES</span>
</a> </a>
<a href="{$base_path}/?searchtags=shaarli-archive" class="header-nav-link{if="isset($search_tags) && preg_match('/(^|[\s,])shaarli-archive([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Archive">
<i class="mdi mdi-archive-arrow-down-outline" aria-hidden="true"></i>
<span>ARCHIVE</span>
</a>
<a href="{$base_path}/?searchtags=readitlater" class="header-nav-link{if="isset($search_tags) && preg_match('/(^|[\s,])readitlater([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Read It Later"> <a href="{$base_path}/?searchtags=readitlater" class="header-nav-link{if="isset($search_tags) && preg_match('/(^|[\s,])readitlater([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Read It Later">
<i class="mdi mdi-bookmark-outline" aria-hidden="true"></i> <i class="mdi mdi-bookmark-outline" aria-hidden="true"></i>
<span>READ IT LATER</span> <span>READ IT LATER</span>