diff --git a/shaarli-pro/css/custom_views.css b/shaarli-pro/css/custom_views.css index 33fad95..794b8a2 100644 --- a/shaarli-pro/css/custom_views.css +++ b/shaarli-pro/css/custom_views.css @@ -457,29 +457,53 @@ body.view-notes .content-container { } .note-modal-tags .note-tag { + display: inline-flex; + align-items: center; + gap: 6px; background: rgba(0, 0, 0, 0.06); border: 1px solid rgba(0, 0, 0, 0.14); color: inherit; border-radius: 999px; - padding: 4px 10px; + padding: 4px 8px; font-size: 0.72rem; letter-spacing: 0.02em; } -[data-theme="dark"] .note-modal-tags .note-tag { - background: rgba(255, 255, 255, 0.12); - border-color: rgba(255, 255, 255, 0.22); +.note-modal-tags .note-tag-text { + line-height: 1.2; +} + +.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 { display: flex; align-items: center; justify-content: space-between; - gap: 10px; + gap: 6px; padding: 8px 12px; border-top: 1px solid rgba(0, 0, 0, 0.12); background: rgba(0, 0, 0, 0.03); flex-shrink: 0; + flex-wrap: wrap; } [data-theme="dark"] .note-modal-actions { @@ -574,17 +598,13 @@ body.view-notes .content-container { border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 16px; - /* spacing for masonry */ break-inside: avoid; - /* Prevent split */ position: relative; transition: box-shadow 0.2s, transform 0.2s, background-color 0.2s; overflow: visible; - /* allow palette popup to escape card bounds */ color: var(--note-card-fg, #202124); } -/* Dark Mode Card */ [data-theme="dark"] .note-card { background-color: #202124; border: 1px solid #5f6368; @@ -597,10 +617,14 @@ body.view-notes .content-container { [data-theme="dark"] .note-card:hover { background-color: #202124; - /* Keep lightens on hover? usually same but controls appear */ } /* Cover Image */ +.note-cover { + overflow: hidden; + border-radius: 8px 8px 0 0; +} + .note-cover img { width: 100%; height: auto; @@ -608,11 +632,6 @@ body.view-notes .content-container { object-fit: cover; } -.note-cover { - overflow: hidden; - border-radius: 8px 8px 0 0; -} - /* Inner Content */ .note-inner { padding: 12px 16px; @@ -640,14 +659,12 @@ body.view-notes .content-container { line-height: 1.25rem; color: var(--text-color, #202124); word-wrap: break-word; - /* Limit to ~12 lines */ display: -webkit-box; -webkit-line-clamp: 12; line-clamp: 12; -webkit-box-orient: vertical; overflow: hidden; max-height: 300px; - /* Fallback */ } [data-theme="dark"] .note-body { @@ -663,9 +680,12 @@ body.view-notes .content-container { } .note-tag { + display: inline-flex; + align-items: center; + gap: 6px; background: rgba(0, 0, 0, 0.06); - padding: 2px 6px; - border-radius: 4px; + padding: 2px 8px; + border-radius: 999px; font-size: 0.7rem; color: var(--text-light); } @@ -675,6 +695,31 @@ body.view-notes .content-container { 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 */ .note-hover-actions { display: flex; diff --git a/shaarli-pro/css/style.css b/shaarli-pro/css/style.css index 3ac2989..55e780b 100644 --- a/shaarli-pro/css/style.css +++ b/shaarli-pro/css/style.css @@ -1359,22 +1359,64 @@ input:checked+.theme-slider:before { min-width: 0; } -.link-tag a { - display: inline-block; - padding: 0.25rem 0.625rem; +.link-tag { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0.5rem; background: var(--tag-bg); color: var(--tag-text); - border-radius: 0.375rem; + border-radius: 999px; font-size: 0.75rem; font-weight: 500; 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); 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 { align-self: stretch; justify-content: flex-end; diff --git a/shaarli-pro/js/custom_views.js b/shaarli-pro/js/custom_views.js index 04e9f4c..2794a69 100644 --- a/shaarli-pro/js/custom_views.js +++ b/shaarli-pro/js/custom_views.js @@ -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- + 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 = "×"; + 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"; diff --git a/shaarli-pro/linklist.html b/shaarli-pro/linklist.html index 5258ec4..cbb1269 100644 --- a/shaarli-pro/linklist.html +++ b/shaarli-pro/linklist.html @@ -85,8 +85,8 @@ {if="$value.description"}{/if}