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:
Bruno Charest 2026-02-17 22:46:23 -05:00
parent 5631c8935b
commit 6f58f6cd67
4 changed files with 321 additions and 38 deletions

View File

@ -457,29 +457,53 @@ body.view-notes .content-container {
} }
.note-modal-tags .note-tag { .note-modal-tags .note-tag {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(0, 0, 0, 0.06); background: rgba(0, 0, 0, 0.06);
border: 1px solid rgba(0, 0, 0, 0.14); border: 1px solid rgba(0, 0, 0, 0.14);
color: inherit; color: inherit;
border-radius: 999px; border-radius: 999px;
padding: 4px 10px; padding: 4px 8px;
font-size: 0.72rem; font-size: 0.72rem;
letter-spacing: 0.02em; letter-spacing: 0.02em;
} }
[data-theme="dark"] .note-modal-tags .note-tag { .note-modal-tags .note-tag-text {
background: rgba(255, 255, 255, 0.12); line-height: 1.2;
border-color: rgba(255, 255, 255, 0.22); }
.note-modal-tags .note-tag-remove-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border: none;
border-radius: 999px;
background: rgba(0, 0, 0, 0.12);
color: currentColor;
cursor: pointer;
padding: 0;
line-height: 1;
font-size: 14px;
opacity: 0.9;
}
[data-theme="dark"] .note-modal-tags .note-tag-remove-btn {
background: rgba(255, 255, 255, 0.18);
} }
.note-modal-actions { .note-modal-actions {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 10px; gap: 6px;
padding: 8px 12px; padding: 8px 12px;
border-top: 1px solid rgba(0, 0, 0, 0.12); border-top: 1px solid rgba(0, 0, 0, 0.12);
background: rgba(0, 0, 0, 0.03); background: rgba(0, 0, 0, 0.03);
flex-shrink: 0; flex-shrink: 0;
flex-wrap: wrap;
} }
[data-theme="dark"] .note-modal-actions { [data-theme="dark"] .note-modal-actions {
@ -574,17 +598,13 @@ body.view-notes .content-container {
border: 1px solid #e0e0e0; border: 1px solid #e0e0e0;
border-radius: 8px; border-radius: 8px;
margin-bottom: 16px; margin-bottom: 16px;
/* spacing for masonry */
break-inside: avoid; break-inside: avoid;
/* Prevent split */
position: relative; position: relative;
transition: box-shadow 0.2s, transform 0.2s, background-color 0.2s; transition: box-shadow 0.2s, transform 0.2s, background-color 0.2s;
overflow: visible; overflow: visible;
/* allow palette popup to escape card bounds */
color: var(--note-card-fg, #202124); color: var(--note-card-fg, #202124);
} }
/* Dark Mode Card */
[data-theme="dark"] .note-card { [data-theme="dark"] .note-card {
background-color: #202124; background-color: #202124;
border: 1px solid #5f6368; border: 1px solid #5f6368;
@ -597,10 +617,14 @@ body.view-notes .content-container {
[data-theme="dark"] .note-card:hover { [data-theme="dark"] .note-card:hover {
background-color: #202124; background-color: #202124;
/* Keep lightens on hover? usually same but controls appear */
} }
/* Cover Image */ /* Cover Image */
.note-cover {
overflow: hidden;
border-radius: 8px 8px 0 0;
}
.note-cover img { .note-cover img {
width: 100%; width: 100%;
height: auto; height: auto;
@ -608,11 +632,6 @@ body.view-notes .content-container {
object-fit: cover; object-fit: cover;
} }
.note-cover {
overflow: hidden;
border-radius: 8px 8px 0 0;
}
/* Inner Content */ /* Inner Content */
.note-inner { .note-inner {
padding: 12px 16px; padding: 12px 16px;
@ -640,14 +659,12 @@ body.view-notes .content-container {
line-height: 1.25rem; line-height: 1.25rem;
color: var(--text-color, #202124); color: var(--text-color, #202124);
word-wrap: break-word; word-wrap: break-word;
/* Limit to ~12 lines */
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 12; -webkit-line-clamp: 12;
line-clamp: 12; line-clamp: 12;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
max-height: 300px; max-height: 300px;
/* Fallback */
} }
[data-theme="dark"] .note-body { [data-theme="dark"] .note-body {
@ -663,9 +680,12 @@ body.view-notes .content-container {
} }
.note-tag { .note-tag {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(0, 0, 0, 0.06); background: rgba(0, 0, 0, 0.06);
padding: 2px 6px; padding: 2px 8px;
border-radius: 4px; border-radius: 999px;
font-size: 0.7rem; font-size: 0.7rem;
color: var(--text-light); color: var(--text-light);
} }
@ -675,6 +695,31 @@ body.view-notes .content-container {
color: #9aa0a6; color: #9aa0a6;
} }
.note-tag-text {
line-height: 1.2;
}
.note-tag-remove-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border: none;
border-radius: 999px;
background: rgba(0, 0, 0, 0.12);
color: currentColor;
cursor: pointer;
padding: 0;
line-height: 1;
font-size: 14px;
opacity: 0.9;
}
[data-theme="dark"] .note-tag-remove-btn {
background: rgba(255, 255, 255, 0.18);
}
/* Hover Actions */ /* Hover Actions */
.note-hover-actions { .note-hover-actions {
display: flex; display: flex;

View File

@ -1359,22 +1359,64 @@ input:checked+.theme-slider:before {
min-width: 0; min-width: 0;
} }
.link-tag a { .link-tag {
display: inline-block; display: inline-flex;
padding: 0.25rem 0.625rem; align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: var(--tag-bg); background: var(--tag-bg);
color: var(--tag-text); color: var(--tag-text);
border-radius: 0.375rem; border-radius: 999px;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 500; font-weight: 500;
transition: all 0.15s ease; transition: all 0.15s ease;
} }
.link-tag a:hover { .link-tag.is-tech-tag {
display: none;
}
.link-tag .link-tag-link {
color: inherit;
text-decoration: none;
display: inline-block;
line-height: 1.2;
}
.link-tag:hover {
background: var(--primary); background: var(--primary);
color: white; color: white;
} }
.link-tag:hover .link-tag-link {
color: inherit;
}
.link-tag .tag-remove-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border: none;
border-radius: 999px;
background: rgba(0, 0, 0, 0.12);
color: currentColor;
cursor: pointer;
padding: 0;
line-height: 1;
font-size: 14px;
opacity: 0.9;
}
[data-theme="dark"] .link-tag .tag-remove-btn {
background: rgba(255, 255, 255, 0.18);
}
.link-tag:hover .tag-remove-btn {
background: rgba(255, 255, 255, 0.22);
}
.view-grid .link-tag-list { .view-grid .link-tag-list {
align-self: stretch; align-self: stretch;
justify-content: flex-end; justify-content: flex-end;

View File

@ -22,7 +22,18 @@ document.addEventListener("DOMContentLoaded", function () {
if (searchTags === "todo") { if (searchTags === "todo") {
initTodoView(linkList, container); initTodoView(linkList, container);
} else if (searchTags === "note") { } 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); 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_FILTER_TAG_PREFIX = "notefilter-";
const NOTE_BACKGROUND_TAG_PREFIX = "notebg-"; 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() { function resolveThemeAssetBasePath() {
const cssLink = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).find( const cssLink = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).find(
(link) => link.href && link.href.includes("/custom_views.css"), (link) => link.href && link.href.includes("/custom_views.css"),
@ -1810,6 +2006,8 @@ function renderNotes(container, notes, viewMode) {
const card = document.createElement("div"); const card = document.createElement("div");
card.className = "note-card"; card.className = "note-card";
card.dataset.id = note.id; card.dataset.id = note.id;
card.dataset.editUrl = note.editUrl || "";
card.dataset.tags = (note.tags || []).filter((t) => t).join("||");
applyNoteVisualState(card, note); applyNoteVisualState(card, note);
if (viewMode === "list") card.classList.add("list-mode"); if (viewMode === "list") card.classList.add("list-mode");
@ -1857,12 +2055,9 @@ function renderNotes(container, notes, viewMode) {
const tagContainer = document.createElement("div"); const tagContainer = document.createElement("div");
tagContainer.className = "note-tags"; tagContainer.className = "note-tags";
note.tags.forEach((t) => { note.tags.forEach((t) => {
if (t !== "note") { if (isTechnicalTag(t)) return;
const span = document.createElement("span"); const pill = createTagPill({ tag: t, onRemoveClass: "note-tag-remove-btn", tagClass: "note-tag", canRemove: !!note.editUrl && note.editUrl !== "#" });
span.className = "note-tag"; tagContainer.appendChild(pill);
span.textContent = t;
tagContainer.appendChild(span);
}
}); });
inner.appendChild(tagContainer); inner.appendChild(tagContainer);
} }
@ -1929,7 +2124,10 @@ function renderModalTags(container, tags) {
if (!container) return; if (!container) return;
container.innerHTML = ""; 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) { if (visibleTags.length === 0) {
container.classList.add("is-empty"); container.classList.add("is-empty");
@ -1938,10 +2136,8 @@ function renderModalTags(container, tags) {
container.classList.remove("is-empty"); container.classList.remove("is-empty");
visibleTags.forEach((tag) => { visibleTags.forEach((tag) => {
const tagEl = document.createElement("span"); const pill = createTagPill({ tag, onRemoveClass: "note-tag-remove-btn", tagClass: "note-tag", canRemove });
tagEl.className = "note-tag"; container.appendChild(pill);
tagEl.textContent = tag;
container.appendChild(tagEl);
}); });
} }
@ -1966,7 +2162,7 @@ function openNoteModal(note) {
modalCard.dataset.deleteUrl = note.deleteUrl || ""; modalCard.dataset.deleteUrl = note.deleteUrl || "";
modalCard.dataset.background = note.background || "none"; 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("||"); modalCard.dataset.tags = visibleTags.join("||");
title.textContent = note.title || "Sans titre"; title.textContent = note.title || "Sans titre";

View File

@ -85,8 +85,8 @@
{if="$value.description"}<div class="link-description">{$value.description}</div>{/if} {if="$value.description"}<div class="link-description">{$value.description}</div>{/if}
<div class="link-footer"> <div class="link-footer">
<div class="link-tag-list"> <div class="link-tag-list">
{loop="$value.taglist"}<span class="link-tag"><a {loop="$value.taglist"}<span class="link-tag" data-tag="{$value}"><a
href="{$base_path}/add-tag/{$value|urlencode}">{$value}</a></span>{/loop} class="link-tag-link" href="{$base_path}/add-tag/{$value|urlencode}">{$value}</a>{if="$is_logged_in"}<button type="button" class="tag-remove-btn" data-tag="{$value}" aria-label="Supprimer le tag {$value}" title="Supprimer">&times;</button>{/if}</span>{/loop}
</div> </div>
<div class="link-actions"> <div class="link-actions">
{if="$is_logged_in"} {if="$is_logged_in"}