feat: ajouter boutons de suppression de tags interactifs avec pills redesignés (border-radius 999px), système de filtrage des tags techniques (note, shaarli-pin, note-color-, notefilter-, notebg-), fonction removeTagFromEntity pour suppression via API, synchronisation bidirectionnelle entre cartes et modale, et amélioration du style avec flexbox, gaps optimisés, et support complet thème clair/sombre

This commit is contained in:
2026-02-17 22:46:23 -05:00
parent 5631c8935b
commit 6f58f6cd67
4 changed files with 321 additions and 38 deletions
+208 -12
View File
@@ -22,7 +22,18 @@ document.addEventListener("DOMContentLoaded", function () {
if (searchTags === "todo") {
initTodoView(linkList, container);
} else if (searchTags === "note") {
// Pour la vue notes, parser les notes AVANT de supprimer les tags techniques
// afin que les propriétés visuelles (couleur, fond, etc.) soient correctement extraites
initNoteView(linkList, container);
// Puis supprimer les tags techniques de l'affichage
if (typeof initTagDisplayAndRemoval === "function") {
initTagDisplayAndRemoval();
}
} else {
// Vue standard : supprimer les tags techniques
if (typeof initTagDisplayAndRemoval === "function") {
initTagDisplayAndRemoval();
}
}
};
@@ -151,6 +162,191 @@ const NOTE_COLOR_TAG_PREFIX = "note-color-";
const NOTE_FILTER_TAG_PREFIX = "notefilter-";
const NOTE_BACKGROUND_TAG_PREFIX = "notebg-";
function isTechnicalTag(tag) {
if (typeof tag !== "string") return false;
const t = tag.trim();
if (!t) return false;
if (t === "note") return true;
if (t === "shaarli-pin") return true;
if (t.startsWith(NOTE_FONT_COLOR_TAG_PREFIX)) return true;
if (t.startsWith(NOTE_COLOR_TAG_PREFIX)) return true;
if (t.startsWith(NOTE_FILTER_TAG_PREFIX)) return true;
if (t.startsWith(NOTE_BACKGROUND_TAG_PREFIX)) return true;
if (t.startsWith("notefilter-")) return true;
if (t.startsWith("notebg-")) return true;
if (t.startsWith("note-color-")) return true;
// Legacy note color tags: note-<color>
if (t.startsWith("note-")) {
const candidate = t.substring(5);
if (NOTE_COLOR_OPTIONS.some((opt) => opt.key === candidate)) return true;
}
return false;
}
function createTagPill({ tag, onRemoveClass = "tag-remove-btn", tagClass = "tag-pill" }) {
const wrapper = document.createElement("span");
wrapper.className = tagClass;
wrapper.dataset.tag = tag;
const text = document.createElement("span");
text.className = `${tagClass}-text`;
text.textContent = tag;
wrapper.appendChild(text);
const canRemove = arguments[0] && Object.prototype.hasOwnProperty.call(arguments[0], "canRemove") ? !!arguments[0].canRemove : true;
if (!canRemove) return wrapper;
const btn = document.createElement("button");
btn.type = "button";
btn.className = onRemoveClass;
btn.dataset.tag = tag;
btn.setAttribute("aria-label", `Supprimer le tag ${tag}`);
btn.title = "Supprimer";
btn.innerHTML = "&times;";
wrapper.appendChild(btn);
return wrapper;
}
function removeTagFromEntity(editUrl, tag) {
return fetch(editUrl)
.then((response) => response.text())
.then((html) => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
const form = doc.querySelector('form[name="linkform"]');
if (!form) throw new Error("Could not find edit form");
const formData = new URLSearchParams();
const inputs = form.querySelectorAll("input, textarea");
inputs.forEach((input) => {
if (input.type === "checkbox") {
if (input.checked) formData.append(input.name, input.value || "on");
} else if (input.name) {
formData.append(input.name, input.value);
}
});
let currentTags = formData.get("lf_tags") || "";
let tagsArray = currentTags.split(/[\s,]+/).filter((t) => t.trim() !== "");
tagsArray = tagsArray.filter((t) => t !== tag);
formData.set("lf_tags", tagsArray.join(" "));
formData.append("save_edit", "1");
return fetch(form.action, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: formData.toString(),
});
})
.then((response) => {
if (!response.ok) throw new Error("Failed to update tags");
return response;
});
}
let tagDisplayRemovalInitialized = false;
function initTagDisplayAndRemoval() {
if (tagDisplayRemovalInitialized) return;
// Filter technical tags on bookmarks list (standard view).
// Keep shaarli-pin in DOM (hidden) so initPinnedItems can still detect it.
document.querySelectorAll(".link-tag-list .link-tag").forEach((el) => {
const tag = (el.dataset.tag || el.textContent || "").trim();
if (!tag) return;
if (tag === "shaarli-pin") {
el.classList.add("is-tech-tag");
return;
}
if (isTechnicalTag(tag)) {
el.remove();
}
});
// Delegate remove button clicks (bookmarks + notes + modal)
document.addEventListener("click", function (e) {
const btn = e.target.closest(".tag-remove-btn, .note-tag-remove-btn");
if (!btn) return;
e.preventDefault();
e.stopPropagation();
const tag = (btn.dataset.tag || "").trim();
if (!tag) return;
const tagEl = btn.closest(".link-tag, .note-tag");
const card = btn.closest(".link-outer, .note-card, .note-modal");
let editUrl = "";
if (card && card.classList.contains("note-card")) {
editUrl = card.dataset.editUrl || "";
} else if (card && card.classList.contains("note-modal")) {
editUrl = card.dataset.editUrl || "";
} else {
const actionsEl = card ? card.querySelector(".link-actions") : null;
const editLink = actionsEl ? actionsEl.querySelector('a[href*="admin/shaare"]') : null;
editUrl = editLink ? editLink.href : "";
}
if (!editUrl) return;
removeTagFromEntity(editUrl, tag)
.then(() => {
if (tagEl) tagEl.remove();
// Sync note datasets + modal tag list if applicable
if (card && card.classList.contains("note-card")) {
let tags = (card.dataset.tags || "").split("||").filter((t) => t);
tags = tags.filter((t) => t !== tag);
card.dataset.tags = tags.join("||");
const modal = document.querySelector(".note-modal-overlay");
if (modal && modal.currentNote && String(modal.currentNote.id) === String(card.dataset.id)) {
modal.currentNote.tags = (modal.currentNote.tags || []).filter((t) => t !== tag);
const tagsContainer = modal.querySelector("#note-modal-tags");
renderModalTags(tagsContainer, modal.currentNote.tags);
}
}
if (card && card.classList.contains("note-modal")) {
let tags = (card.dataset.tags || "").split("||").filter((t) => t);
tags = tags.filter((t) => t !== tag);
card.dataset.tags = tags.join("||");
const modal = document.querySelector(".note-modal-overlay");
if (modal && modal.currentNote) {
modal.currentNote.tags = (modal.currentNote.tags || []).filter((t) => t !== tag);
const tagsContainer = modal.querySelector("#note-modal-tags");
renderModalTags(tagsContainer, modal.currentNote.tags);
}
// Also update corresponding card if visible
const noteId = card.dataset.noteId;
const noteCard = noteId ? document.querySelector(`.note-card[data-id="${noteId}"]`) : null;
if (noteCard) {
const pill = noteCard.querySelector(`.note-tag[data-tag="${CSS.escape(tag)}"]`);
if (pill) pill.remove();
let noteTags = (noteCard.dataset.tags || "").split("||").filter((t) => t);
noteTags = noteTags.filter((t) => t !== tag);
noteCard.dataset.tags = noteTags.join("||");
}
}
})
.catch((err) => {
console.error("Error removing tag:", err);
alert("Erreur lors de la suppression du tag.");
});
});
tagDisplayRemovalInitialized = true;
}
function resolveThemeAssetBasePath() {
const cssLink = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).find(
(link) => link.href && link.href.includes("/custom_views.css"),
@@ -1810,6 +2006,8 @@ function renderNotes(container, notes, viewMode) {
const card = document.createElement("div");
card.className = "note-card";
card.dataset.id = note.id;
card.dataset.editUrl = note.editUrl || "";
card.dataset.tags = (note.tags || []).filter((t) => t).join("||");
applyNoteVisualState(card, note);
if (viewMode === "list") card.classList.add("list-mode");
@@ -1857,12 +2055,9 @@ function renderNotes(container, notes, viewMode) {
const tagContainer = document.createElement("div");
tagContainer.className = "note-tags";
note.tags.forEach((t) => {
if (t !== "note") {
const span = document.createElement("span");
span.className = "note-tag";
span.textContent = t;
tagContainer.appendChild(span);
}
if (isTechnicalTag(t)) return;
const pill = createTagPill({ tag: t, onRemoveClass: "note-tag-remove-btn", tagClass: "note-tag", canRemove: !!note.editUrl && note.editUrl !== "#" });
tagContainer.appendChild(pill);
});
inner.appendChild(tagContainer);
}
@@ -1929,7 +2124,10 @@ function renderModalTags(container, tags) {
if (!container) return;
container.innerHTML = "";
const visibleTags = (tags || []).filter((tag) => tag && tag !== "note");
const visibleTags = (tags || []).filter((tag) => tag && !isTechnicalTag(tag));
const modal = document.querySelector(".note-modal-overlay");
const canRemove = !!(modal && modal.currentNote && modal.currentNote.editUrl && modal.currentNote.editUrl !== "#");
if (visibleTags.length === 0) {
container.classList.add("is-empty");
@@ -1938,10 +2136,8 @@ function renderModalTags(container, tags) {
container.classList.remove("is-empty");
visibleTags.forEach((tag) => {
const tagEl = document.createElement("span");
tagEl.className = "note-tag";
tagEl.textContent = tag;
container.appendChild(tagEl);
const pill = createTagPill({ tag, onRemoveClass: "note-tag-remove-btn", tagClass: "note-tag", canRemove });
container.appendChild(pill);
});
}
@@ -1966,7 +2162,7 @@ function openNoteModal(note) {
modalCard.dataset.deleteUrl = note.deleteUrl || "";
modalCard.dataset.background = note.background || "none";
const visibleTags = (note.tags || []).filter((tag) => tag && tag !== "note");
const visibleTags = (note.tags || []).filter((tag) => tag && !isTechnicalTag(tag));
modalCard.dataset.tags = visibleTags.join("||");
title.textContent = note.title || "Sans titre";