document.addEventListener("DOMContentLoaded", function () { // Check URL parameters for custom views const urlParams = new URLSearchParams(window.location.search); const searchTags = urlParams.get("searchtags"); const linkList = document.getElementById("links-list"); const container = document.querySelector(".content-container"); const startViewInitialization = function () { if (typeof initBookmarkPaletteButtons === "function") { initBookmarkPaletteButtons(); } // Always init Pinned Items logic (sorting and listeners) // This function is defined at the end of the file if (typeof initPinnedItems === "function") { initPinnedItems(); } if (!linkList || !container) return; if (searchTags === "todo") { initTodoView(linkList, container); } else if (searchTags === "note") { initNoteView(linkList, container); } }; // Charger les backgrounds dynamiquement, puis initialiser les vues initThemeModeBackgroundSync(); if (typeof loadBackgroundOptions === "function") { loadBackgroundOptions() .then(() => { startViewInitialization(); }) .catch((err) => { console.error("Erreur lors du chargement des backgrounds:", err); // Initialiser quand même même si le chargement échoue startViewInitialization(); }); } else { startViewInitialization(); } }); const NOTE_COLOR_OPTIONS = [ { key: "default", label: "Par défaut", light: "#ffffff", dark: "#20293A" }, { key: "red", label: "Rouge", light: "#f28b82", dark: "#9c2116" }, { key: "orange", label: "Orange", light: "#fbbc04", dark: "#9c7a16" }, { key: "yellow", label: "Jaune", light: "#fff475", dark: "#9c9116" }, { key: "green", label: "Vert", light: "#ccff90", dark: "#5e9c16" }, { key: "teal", label: "Menthe", light: "#a7ffeb", dark: "#169c7d" }, { key: "blue", label: "Bleu clair", light: "#cbf0f8", dark: "#16849c" }, { key: "darkblue", label: "Bleu", light: "#aecbfa", dark: "#16499c" }, { key: "purple", label: "Violet", light: "#d7aefb", dark: "#5d169c" }, { key: "pink", label: "Rose", light: "#fdcfe8", dark: "#9c165f" }, { key: "brown", label: "Beige", light: "#e6c9a8", dark: "#9c5d16" }, { key: "grey", label: "Gris", light: "#e8eaed", dark: "#4e5764" } ]; const NOTE_BACKGROUND_TAG_PREFIX = "notebg-"; function resolveThemeAssetBasePath() { const cssLink = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).find( (link) => link.href && link.href.includes("/custom_views.css"), ); if (cssLink && cssLink.href) { const cssUrl = new URL(cssLink.href, window.location.origin); const cssPath = cssUrl.pathname.replace(/\/css\/custom_views\.css$/, ""); if (cssPath) return cssPath; } const jsScript = Array.from(document.querySelectorAll("script[src]")).find( (script) => script.src && script.src.includes("/custom_views.js"), ); if (jsScript && jsScript.src) { const jsUrl = new URL(jsScript.src, window.location.origin); const jsPath = jsUrl.pathname.replace(/\/js\/custom_views\.js$/, ""); if (jsPath) return jsPath; } if (window.shaarli && typeof window.shaarli.assetPath === "string" && window.shaarli.assetPath.trim() !== "") { return window.shaarli.assetPath.replace(/\/$/, ""); } return "/tpl/shaarli-pro"; } const NOTE_BACKGROUND_ASSET_ROOT = `${resolveThemeAssetBasePath().replace(/\/$/, "")}/img`; // Liste des backgrounds - chargée dynamiquement depuis le serveur let NOTE_BACKGROUND_OPTIONS = []; let isBackgroundOptionsLoaded = false; let backgroundOptionsLoadPromise = null; window.__shaarliCustomViewsVersion = "1.0.5-inline-bg-manifest"; if (typeof console !== "undefined" && console && typeof console.info === "function") { console.info("[custom_views] loaded", window.__shaarliCustomViewsVersion); } const NOTE_BACKGROUND_MANIFEST_INLINE = [ { key: "cafe", label: "Café", files: { light: "bg-light-cafe.jpg", dark: "bg-dark-cafe.jpg" } }, { key: "codes", label: "Codes", files: { light: "bg-light-codes.jpg", dark: "bg-dark-codes.jpg" } }, { key: "dune", label: "Dune", files: { light: "bg-light-dune.jpg", dark: "bg-dark-dune.jpg" } }, { key: "feuille-ligne", label: "Feuille Ligne", files: { light: "bg-light-feuilleLigne.jpg", dark: "bg-dark-feuilleLigne.jpg" } }, { key: "feuille-quadrillage", label: "Feuille Quadrillage", files: { light: "bg-light-feuilleQuadrille.jpg", dark: "bg-dark-feuilleQuadrille.jpg" } }, { key: "fleurs", label: "Fleurs", files: { light: "bg-light-fleurs.jpg", dark: "bg-dark-fleurs.jpg" } }, { key: "foret", label: "Forêt", files: { light: "bg-light-foret.jpg", dark: "bg-dark-foret.jpg" } }, { key: "grid", label: "Grid", files: { light: "bg-light-grid.jpg", dark: "bg-dark-grid.jpg" } }, { key: "journal", label: "Journal", files: { light: "bg-light-journal.jpg", dark: "bg-dark-journal.jpg" } }, { key: "lecture", label: "Lecture", files: { light: "bg-light-lecture.jpg", dark: "bg-dark-lecture.jpg" } }, { key: "legumes", label: "Légumes", files: { light: "bg-light-legumes.jpg", dark: "bg-dark-legumes.jpg" } }, { key: "montagnes", label: "Montagnes", files: { light: "bg-light-montagnes.jpg", dark: "bg-dark-montagnes.jpg" } }, { key: "ocean", label: "Océan", files: { light: "bg-light-ocean.jpg", dark: "bg-dark-ocean.jpg" } }, { key: "sports", label: "Sports", files: { light: "bg-light-sports.jpg", dark: "bg-dark-sports.jpg" } }, { key: "vague1", label: "Vague 1", files: { light: "bg-light-vague1.jpg", dark: "bg-dark-vague1.jpg" } }, { key: "vague2", label: "Vague 2", files: { light: "bg-light-vague2.jpg", dark: "bg-dark-vague2.jpg" } }, { key: "ville", label: "Ville", files: { light: "bg-light-ville.jpg", dark: "bg-dark-ville.jpg" } }, { key: "voyage", label: "Voyage", files: { light: "bg-light-voyage.jpg", dark: "bg-dark-voyage.jpg" } }, ]; /** * Charge la liste des backgrounds depuis le endpoint PHP * @returns {Promise} Liste des backgrounds */ function loadBackgroundOptions() { if (isBackgroundOptionsLoaded) { return Promise.resolve(NOTE_BACKGROUND_OPTIONS); } if (backgroundOptionsLoadPromise) { return backgroundOptionsLoadPromise; } NOTE_BACKGROUND_OPTIONS = normalizeDynamicBackgroundOptions(NOTE_BACKGROUND_MANIFEST_INLINE); isBackgroundOptionsLoaded = true; backgroundOptionsLoadPromise = Promise.resolve(NOTE_BACKGROUND_OPTIONS); return backgroundOptionsLoadPromise; } function normalizeBackgroundKey(key) { if (typeof key !== "string") return ""; return key .trim() .toLowerCase() .replace(/[\s_]+/g, "-") .replace(/[^a-z0-9-]/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, ""); } function formatBackgroundLabel(rawLabel, fallbackKey) { const base = (typeof rawLabel === "string" && rawLabel.trim() !== "" ? rawLabel : fallbackKey) .replace(/[_-]+/g, " ") .replace(/([a-z0-9])([A-Z])/g, "$1 $2") .replace(/\s+/g, " ") .trim(); return base .split(" ") .filter((part) => part !== "") .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } function getCurrentThemeMode() { return document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light"; } function getColorOption(colorKey) { const normalizedKey = typeof colorKey === "string" ? colorKey.trim().toLowerCase() : ""; return NOTE_COLOR_OPTIONS.find((opt) => opt.key === normalizedKey) || NOTE_COLOR_OPTIONS.find((opt) => opt.key === "default") || null; } function getThemeColorValue(option, mode = getCurrentThemeMode()) { if (!option || typeof option !== "object") return ""; const safeMode = mode === "dark" ? "dark" : "light"; const preferred = typeof option[safeMode] === "string" ? option[safeMode].trim() : ""; const fallbackLight = typeof option.light === "string" ? option.light.trim() : ""; const fallbackDark = typeof option.dark === "string" ? option.dark.trim() : ""; return preferred || fallbackLight || fallbackDark; } function parseHexColor(colorValue) { if (typeof colorValue !== "string") return null; const safeValue = colorValue.trim(); const shortHexMatch = /^#([0-9a-f]{3})$/i.exec(safeValue); if (shortHexMatch) { return { r: parseInt(shortHexMatch[1].charAt(0).repeat(2), 16), g: parseInt(shortHexMatch[1].charAt(1).repeat(2), 16), b: parseInt(shortHexMatch[1].charAt(2).repeat(2), 16), }; } const longHexMatch = /^#([0-9a-f]{6})$/i.exec(safeValue); if (longHexMatch) { return { r: parseInt(longHexMatch[1].slice(0, 2), 16), g: parseInt(longHexMatch[1].slice(2, 4), 16), b: parseInt(longHexMatch[1].slice(4, 6), 16), }; } return null; } function getReadableForegroundForBackground(colorValue, mode = getCurrentThemeMode()) { const rgb = parseHexColor(colorValue); if (!rgb) { return mode === "dark" ? "#e8eaed" : "#202124"; } const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255; return luminance > 0.56 ? "#202124" : "#f5f7fb"; } function normalizeDynamicBackgroundOptions(rawOptions) { const rawList = Array.isArray(rawOptions) ? rawOptions : rawOptions && Array.isArray(rawOptions.backgrounds) ? rawOptions.backgrounds : []; const mergedByKey = {}; rawList.forEach((option) => { if (!option || typeof option !== "object") return; const key = normalizeBackgroundKey(typeof option.key === "string" ? option.key : option.label || ""); if (!key) return; const files = option.files && typeof option.files === "object" ? option.files : {}; const lightFile = typeof files.light === "string" ? files.light.trim() : ""; const darkFile = typeof files.dark === "string" ? files.dark.trim() : ""; if (!lightFile && !darkFile) return; if (!mergedByKey[key]) { mergedByKey[key] = { key, label: formatBackgroundLabel(option.label || "", key), paths: { light: "", dark: "", }, }; } if (lightFile) { mergedByKey[key].paths.light = `note-bg-light/${lightFile}`; } if (darkFile) { mergedByKey[key].paths.dark = `note-bg-dark/${darkFile}`; } }); return Object.values(mergedByKey).sort((a, b) => a.label.localeCompare(b.label, "fr", { sensitivity: "base" })); } function getAvailableBackgroundOptionsForMode(mode = getCurrentThemeMode()) { const safeMode = mode === "dark" ? "dark" : "light"; return NOTE_BACKGROUND_OPTIONS.filter((option) => option.paths && option.paths[safeMode]); } function getNoteBackgroundUrl(backgroundKey, mode = getCurrentThemeMode()) { const normalizedKey = normalizeBackgroundKey(backgroundKey); if (!normalizedKey) return ""; const found = NOTE_BACKGROUND_OPTIONS.find((bg) => bg.key === normalizedKey); if (!found || !found.paths) return ""; const safeMode = mode === "dark" ? "dark" : "light"; const selectedPath = found.paths[safeMode] || ""; if (!selectedPath) return ""; return `${NOTE_BACKGROUND_ASSET_ROOT}/${selectedPath}`; } function getElementVisualColor(element) { if (!element) return "default"; const colorClass = Array.from(element.classList).find((cls) => cls.startsWith("note-color-")); if (colorClass) { return colorClass.replace("note-color-", "") || "default"; } return element.dataset.color || "default"; } function getElementVisualBackground(element) { if (!element) return "none"; const normalizedBackground = normalizeBackgroundKey(element.dataset.background || ""); return normalizedBackground || "none"; } function refreshNoteBackgroundVisuals() { document.querySelectorAll(".note-card, .note-modal, .link-outer").forEach((element) => { applyNoteVisualState(element, { color: getElementVisualColor(element), background: getElementVisualBackground(element), }); }); } function refreshBackgroundPalettes() { document.querySelectorAll(".note-card").forEach((card) => { const palettePopup = card.querySelector(".note-hover-actions .palette-popup"); if (!palettePopup) return; const noteId = card.dataset.id || ""; if (!noteId) return; const editLink = card.querySelector( '.note-hover-actions a[href*="/admin/shaare/"], .note-hover-actions a[href*="do=editlink"], .note-hover-actions a[title="Edit"]', ); palettePopup.innerHTML = generatePaletteButtons({ id: noteId, editUrl: editLink && editLink.href ? editLink.href : "#", color: getElementVisualColor(card), background: getElementVisualBackground(card), }); }); document.querySelectorAll(".link-outer").forEach((card) => { const palettePopup = card.querySelector(".bookmark-palette .palette-popup"); if (!palettePopup) return; const noteId = card.dataset.id || card.getAttribute("data-id") || card.id; if (!noteId) return; const actions = card.querySelector(".link-actions"); const editLink = actions ? actions.querySelector('a[title="Modifier"], a[aria-label="Modifier ce bookmark"], a[href*="/admin/shaare/"]') : null; palettePopup.innerHTML = generateBookmarkPaletteButtons( noteId, editLink && editLink.href ? editLink.href : "#", getElementVisualColor(card), getElementVisualBackground(card), ); }); const modal = document.querySelector(".note-modal-overlay"); if (!modal || !modal.currentNote) return; const modalCard = modal.querySelector(".note-modal"); if (modalCard) { modal.currentNote.color = getElementVisualColor(modalCard); modal.currentNote.background = getElementVisualBackground(modalCard); } const modalColorPopup = modal.querySelector("#note-modal-color-popup"); if (modalColorPopup) { modalColorPopup.innerHTML = generateModalPaletteButtons(modal.currentNote); } } function parseBackgroundManifestPayload(rawPayload) { const payload = typeof rawPayload === "string" ? rawPayload.trim() : ""; if (!payload) return []; try { return JSON.parse(payload); } catch (parseError) { const firstArrayBracket = payload.indexOf("["); const lastArrayBracket = payload.lastIndexOf("]"); if (firstArrayBracket === -1 || lastArrayBracket === -1 || lastArrayBracket <= firstArrayBracket) { throw parseError; } const jsonSlice = payload.slice(firstArrayBracket, lastArrayBracket + 1); return JSON.parse(jsonSlice); } } let isThemeModeBackgroundSyncInitialized = false; function initThemeModeBackgroundSync() { if (isThemeModeBackgroundSyncInitialized) return; const root = document.documentElement; if (!root) return; let lastTheme = getCurrentThemeMode(); const observer = new MutationObserver((mutations) => { const themeChanged = mutations.some((mutation) => mutation.attributeName === "data-theme"); if (!themeChanged) return; const nextTheme = getCurrentThemeMode(); if (nextTheme === lastTheme) return; lastTheme = nextTheme; refreshNoteBackgroundVisuals(); refreshBackgroundPalettes(); }); observer.observe(root, { attributes: true, attributeFilter: ["data-theme"] }); isThemeModeBackgroundSyncInitialized = true; } function positionPalettePopup(popup) { if (!popup || !popup.classList.contains("open")) return; popup.classList.remove("open-down"); popup.style.left = ""; popup.style.right = ""; const viewportPadding = 8; const upRect = popup.getBoundingClientRect(); if (upRect.top < viewportPadding) { popup.classList.add("open-down"); const downRect = popup.getBoundingClientRect(); if (downRect.bottom > window.innerHeight - viewportPadding) { popup.classList.remove("open-down"); } } let rect = popup.getBoundingClientRect(); if (rect.right > window.innerWidth - viewportPadding) { popup.style.left = "auto"; popup.style.right = "0"; rect = popup.getBoundingClientRect(); } if (rect.left < viewportPadding) { popup.style.left = "0"; popup.style.right = "auto"; } } function applyNoteVisualState(element, note) { if (!element || !note) return; const resolvedColorOption = getColorOption(note.color || "default"); const color = resolvedColorOption ? resolvedColorOption.key : "default"; const colorValue = getThemeColorValue(resolvedColorOption); const foregroundColor = getReadableForegroundForBackground(colorValue); const normalizedBackground = normalizeBackgroundKey(note.background || ""); const background = normalizedBackground || "none"; element.classList.forEach((cls) => { if (cls.startsWith("note-color-")) element.classList.remove(cls); }); element.classList.add(`note-color-${color}`); if (colorValue) { element.style.backgroundColor = colorValue; element.style.borderColor = "transparent"; } else { element.style.removeProperty("background-color"); } if (foregroundColor) { element.style.setProperty("--note-card-fg", foregroundColor); } if (background && background !== "none") { const bgUrl = getNoteBackgroundUrl(background); if (bgUrl) { element.classList.add("note-has-bg"); element.style.setProperty("--note-bg-image", `url('${bgUrl}')`); element.dataset.background = background; } else { element.classList.remove("note-has-bg"); element.style.removeProperty("--note-bg-image"); element.dataset.background = "none"; } } else { element.classList.remove("note-has-bg"); element.style.removeProperty("--note-bg-image"); element.dataset.background = "none"; } element.dataset.color = color; } function extractNoteVisualStateFromTags(tags) { const safeTags = Array.isArray(tags) ? tags : []; let color = "default"; const foundColorTag = safeTags.find((t) => typeof t === "string" && t.startsWith("note-")); if (foundColorTag) { const candidate = foundColorTag.substring(5); if (NOTE_COLOR_OPTIONS.some((opt) => opt.key === candidate)) { color = candidate; } } let background = "none"; const foundBgTag = safeTags.find((t) => typeof t === "string" && t.startsWith(NOTE_BACKGROUND_TAG_PREFIX)); if (foundBgTag) { const candidate = normalizeBackgroundKey(foundBgTag.substring(NOTE_BACKGROUND_TAG_PREFIX.length)); if (candidate) { background = candidate; } } return { color, background }; } function initBookmarkPaletteButtons() { const linkCards = Array.from(document.querySelectorAll(".link-outer")); if (linkCards.length === 0) return; linkCards.forEach((card) => { const cardId = card.dataset.id || card.getAttribute("data-id") || card.id; if (!cardId) return; const tags = Array.from(card.querySelectorAll(".link-tag a")).map((a) => (a.textContent || "").trim()); const { color, background } = extractNoteVisualStateFromTags(tags); applyNoteVisualState(card, { color, background }); const actions = card.querySelector(".link-actions"); if (!actions) return; if (actions.querySelector(".bookmark-palette")) return; const editLink = actions.querySelector('a[title="Modifier"], a[aria-label="Modifier ce bookmark"], a[href*="/admin/shaare/"]'); if (!editLink || !editLink.href) return; const editUrl = editLink.href; const paletteBtnId = `bookmark-palette-${cardId}`; const wrapper = document.createElement("div"); wrapper.className = "bookmark-palette"; wrapper.style.position = "relative"; wrapper.innerHTML = ` `; const pinLink = Array.from(actions.querySelectorAll('a[href*="/pin"], a[aria-label*="Épingler"], a[title*="Épingler"]'))[0]; if (pinLink && pinLink.parentNode === actions) { actions.insertBefore(wrapper, pinLink); } else if (editLink && editLink.parentNode === actions) { actions.insertBefore(wrapper, editLink.nextSibling); } else { actions.appendChild(wrapper); } const paletteBtn = wrapper.querySelector(`#${paletteBtnId}`); const palettePopup = wrapper.querySelector(`#popup-${paletteBtnId}`); if (!paletteBtn || !palettePopup) return; paletteBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); document.querySelectorAll(".palette-popup.open").forEach((p) => { if (p !== palettePopup) { p.classList.remove("open"); const parentCard = p.closest(".link-outer"); if (parentCard) parentCard.classList.remove("palette-open"); } }); const nextOpenState = !palettePopup.classList.contains("open"); palettePopup.classList.toggle("open"); card.classList.toggle("palette-open", nextOpenState); positionPalettePopup(palettePopup); }); }); document.addEventListener("click", (e) => { if (e.target.closest(".bookmark-palette")) return; document.querySelectorAll(".palette-popup.open").forEach((p) => p.classList.remove("open")); document.querySelectorAll(".link-outer.palette-open").forEach((el) => el.classList.remove("palette-open")); }); } function generateBookmarkPaletteButtons(bookmarkId, editUrl, currentColor, currentBackground) { const color = currentColor || "default"; const background = normalizeBackgroundKey(currentBackground || "") || "none"; const colorButtons = [ ``, ...NOTE_COLOR_OPTIONS.filter((opt) => opt.key !== "default").map( (opt) => { const swatchColor = getThemeColorValue(opt); return ``; }, ), ].join(""); const backgroundButtons = [ ``, ...getAvailableBackgroundOptionsForMode().map((bg) => { const bgUrl = getNoteBackgroundUrl(bg.key); return ``; }), ].join(""); return `
${colorButtons}
${backgroundButtons}
`; } function syncNoteFromCardElement(note, card) { if (!note || !card) return; const colorClass = Array.from(card.classList).find((cls) => cls.startsWith("note-color-")); if (colorClass) { note.color = colorClass.replace("note-color-", "") || "default"; } const background = card.dataset.background; note.background = background && background !== "none" ? background : "none"; } /** * Initialize the Google Tasks-like view */ function initTodoView(linkList, container) { document.body.classList.add("view-todo"); // Extract task data from existing DOM const rawLinks = Array.from(linkList.querySelectorAll(".link-outer")); const tasks = rawLinks.map((link) => parseTaskFromLink(link)).filter((t) => t !== null); // Create new Layout const wrapper = document.createElement("div"); wrapper.className = "special-view-wrapper"; // 1. Sidebar const sidebar = document.createElement("div"); sidebar.className = "todo-sidebar"; // Extract unique groups for the sidebar const groups = new Set(); tasks.forEach((t) => { if (t.group) groups.add(t.group); }); const groupsList = Array.from(groups) .map((g) => `
${g}
`) .join(""); sidebar.innerHTML = `
Mes tâches ${tasks.length}
${groups.size > 0 ? `${groupsList}` : ""} `; // 2. Main Content const main = document.createElement("div"); main.className = "todo-main"; const mainHeader = document.createElement("div"); mainHeader.className = "todo-main-header"; mainHeader.innerHTML = `

Mes tâches

`; const itemsContainer = document.createElement("div"); itemsContainer.className = "todo-items-container"; // Sort Tasks: Pinned items first tasks.sort((a, b) => { const aPinned = a.tags && a.tags.includes("shaarli-pin"); const bPinned = b.tags && b.tags.includes("shaarli-pin"); return bPinned - aPinned; }); // Render Tasks tasks.forEach((task) => { itemsContainer.appendChild(renderTaskItem(task)); }); main.appendChild(mainHeader); main.appendChild(itemsContainer); wrapper.appendChild(sidebar); wrapper.appendChild(main); // Inject and Hide original linkList.style.display = "none"; // Remove pagination/toolbar if present to clean up view const toolbar = document.querySelector(".content-toolbar"); if (toolbar) toolbar.style.display = "none"; if (linkList.parentNode) { linkList.parentNode.insertBefore(wrapper, linkList); } else { container.appendChild(wrapper); } // Global filter function window.filterTasksByGroup = function (group) { const title = document.getElementById("todo-list-title"); const items = document.querySelectorAll(".todo-item"); // Update Sidebar Active State document.querySelectorAll(".todo-list-item").forEach((el) => el.classList.remove("active")); if (event && event.currentTarget) event.currentTarget.classList.add("active"); if (group === "all") { title.textContent = "Mes tâches"; items.forEach((item) => (item.style.display = "flex")); } else { title.textContent = group; items.forEach((item) => { if (item.dataset.group === group) item.style.display = "flex"; else item.style.display = "none"; }); } }; } function parseTaskFromLink(linkEl) { const id = linkEl.dataset.id; const titleEl = linkEl.querySelector(".link-title"); // "Title" normally // For Todos, the Bookmark Title is the Task Title? // Or is the title inside the description? // Mental model says: "Todo: reste un bookmark privé... LinkEntity.title" is used. const title = titleEl ? titleEl.textContent.trim() : "Task"; const descEl = linkEl.querySelector(".link-description"); const rawDesc = descEl ? descEl.innerHTML : ""; const textDesc = descEl ? descEl.textContent : ""; // Check if it's really a todo (should be if we are in ?searchtags=todo, but double check) // We assume yes. // Parse Metadata from Description text // Format: 📅 **Échéance :** // Format: 🏷️ **Groupe :** // Format: - [ ] Subtask or Main task status? // Status // If [x] is found in the first few lines, maybe completed? // User says: "Puis une checkbox markdown: - [ ] Titre" // Wait, if the Description contains the checkbox and title, then Bookmark Title is ignored? // Let's assume Bookmark Title is the master Display Title. // And "Checkbox status" can be parsed from description. let isCompleted = false; if (textDesc.includes("[x]")) isCompleted = true; // Due Date let dueDate = null; const dateMatch = textDesc.match(/Échéance\s*:\s*\*+([^*]+)\*+/); // varies by markdown regex // Text might be "📅 **Échéance :** 2023-10-10" // Regex: Échéance\s*:\s*(.*?)(\n|$) const dueMatch = textDesc.match(/Échéance\s*[:]\s*(.*?)(\n|$)/); if (dueMatch) dueDate = dueMatch[1].trim(); // Group let group = null; const groupMatch = textDesc.match(/Groupe\s*[:]\s*(.*?)(\n|$)/); if (groupMatch) group = groupMatch[1].trim(); return { id, title, isCompleted, dueDate, group, originalUrl: linkEl.querySelector(".link-url") ? linkEl.querySelector(".link-url").textContent : "", editUrl: linkEl.querySelector('a[href*="admin/shaare/"]') ? linkEl.querySelector('a[href*="admin/shaare/"]').href : "#", }; } function renderTaskItem(task) { const el = document.createElement("div"); el.className = `todo-item ${task.isCompleted ? "completed" : ""}`; el.dataset.group = task.group || ""; /* We cannot easily toggle state (AJAX) without a backend API that supports partial updates efficiently. But we can provide a link to Edit. Or simulate it. */ el.innerHTML = `
${task.isCompleted ? '' : ""}
${task.title}
${task.group ? `${task.group}` : ""} ${task.dueDate ? ` ${formatDate(task.dueDate)}` : ""}
`; // Simple click handler to open edit (since we can't sync state easily) el.querySelector(".todo-checkbox").addEventListener("click", (e) => { e.stopPropagation(); // Open edit page or toggle visually // window.location.href = task.editUrl; }); return el; } function isOverdue(dateStr) { try { return new Date(dateStr) < new Date(); } catch (e) { return false; } } function formatDate(dateStr) { try { const d = new Date(dateStr); return d.toLocaleDateString(); } catch (e) { return dateStr; } } /** * Initialize the Google Keep-like view */ /** * Initialize the Google Keep-like view */ function initNoteView(linkList, container) { document.body.classList.add("view-notes"); // 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"; // 2. Create Search/Input Area (Top) const topBar = document.createElement("div"); topBar.className = "notes-top-bar"; // Custom Input "Take a note..." const inputContainer = document.createElement("div"); inputContainer.className = "note-input-container"; inputContainer.innerHTML = `
Créer une note...
`; topBar.appendChild(inputContainer); // View Toggle and other tools const tools = document.createElement("div"); tools.className = "notes-tools"; tools.innerHTML = ` `; 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)); // Initial Render (Grid) renderNotes(contentArea, notes, "grid"); wrapper.appendChild(contentArea); // Replace original list linkList.style.display = "none"; if (linkList.parentNode) { linkList.parentNode.insertBefore(wrapper, linkList); } else { container.appendChild(wrapper); } // Modal Container const modalOverlay = document.createElement("div"); modalOverlay.className = "note-modal-overlay"; modalOverlay.innerHTML = `

`; 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, notes, "grid"); }); btnList.addEventListener("click", () => { btnList.classList.add("active"); btnGrid.classList.remove("active"); renderNotes(contentArea, notes, "list"); }); // 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(); modalColorPopup.classList.toggle("open"); positionPalettePopup(modalColorPopup); }); 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; } }); modalOverlay.querySelector("#note-modal-archive").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; addTagToNote(editUrl, "shaarli-archiver") .then(() => { if (modalOverlay.currentNote) { if (!modalOverlay.currentNote.tags.includes("shaarli-archiver")) { modalOverlay.currentNote.tags.push("shaarli-archiver"); } } const noteCard = document.querySelector(`.note-card[data-id="${noteId}"]`); if (noteCard) noteCard.remove(); const index = notes.findIndex((n) => String(n.id) === String(noteId)); if (index > -1) notes.splice(index, 1); modalOverlay.classList.remove("open"); }) .catch((err) => { console.error("Error archiving note:", err); alert("Erreur lors de l'archivage 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; } document.querySelectorAll(".note-hover-actions .palette-popup.open").forEach((p) => { p.classList.remove("open"); }); }); } function parseNoteFromLink(linkEl) { const id = linkEl.dataset.id; const titleEl = linkEl.querySelector(".link-title"); const title = titleEl ? titleEl.textContent.trim() : ""; const descEl = linkEl.querySelector(".link-description"); const descHtml = descEl ? descEl.innerHTML : ""; const descText = descEl ? descEl.textContent : ""; // Extract Image from Description (First image as cover) let coverImage = null; if (descEl) { const img = descEl.querySelector("img"); if (img) { coverImage = img.src; // Optionally remove img from body text if it's purely a cover // But usually we keep it or hide it via CSS if we construct a custom card } } const urlEl = linkEl.querySelector(".link-url"); const url = urlEl ? urlEl.textContent.trim() : ""; const tags = []; let color = "default"; let background = "none"; linkEl.querySelectorAll(".link-tag-list a").forEach((tag) => { const t = tag.textContent.trim(); // Check for color tag if (t.startsWith("note-")) { const potentialColor = t.substring(5); const knownColor = NOTE_COLOR_OPTIONS.some((opt) => opt.key === potentialColor); if (knownColor) { color = potentialColor; } else { tags.push(t); } } else if (t.startsWith(NOTE_BACKGROUND_TAG_PREFIX)) { background = normalizeBackgroundKey(t.substring(NOTE_BACKGROUND_TAG_PREFIX.length)) || "none"; } else { tags.push(t); } }); const isPinnedByTag = tags.includes("shaarli-pin"); const actionsEl = linkEl.querySelector(".link-actions"); const editUrl = actionsEl && actionsEl.querySelector('a[href*="admin/shaare"]') ? actionsEl.querySelector('a[href*="admin/shaare"]').href : "#"; const deleteUrl = actionsEl && actionsEl.querySelector('a[href*="delete"]') ? actionsEl.querySelector('a[href*="delete"]').href : "#"; const pinUrl = actionsEl && actionsEl.querySelector('a[href*="pin"]') ? actionsEl.querySelector('a[href*="pin"]').href : "#"; // User requested "availability of the tag 'shaarli-pin' as the main source" const isPinned = tags.includes("shaarli-pin"); return { id, title, descHtml, descText, coverImage, url, tags, color, background, editUrl, deleteUrl, pinUrl, isPinned }; } function renderNotes(container, notes, viewMode) { container.innerHTML = ""; container.className = viewMode === "grid" ? "notes-masonry" : "notes-list-view"; const visibleNotes = notes.filter((note) => !(note.tags || []).includes("shaarli-archiver")); // Sort: Pinned items first visibleNotes.sort((a, b) => { const aPinned = a.tags.includes("shaarli-pin"); const bPinned = b.tags.includes("shaarli-pin"); return bPinned - aPinned; }); visibleNotes.forEach((note) => { const card = document.createElement("div"); card.className = "note-card"; card.dataset.id = note.id; applyNoteVisualState(card, note); if (viewMode === "list") card.classList.add("list-mode"); // Main Click to Open Modal card.addEventListener("click", (e) => { // Prevent if clicking buttons if (e.target.closest("button") || e.target.closest("a") || e.target.closest(".note-hover-actions")) return; syncNoteFromCardElement(note, card); openNoteModal(note); }); // Cover Image if (note.coverImage && viewMode === "grid") { // Show cover mainly in grid const imgContainer = document.createElement("div"); imgContainer.className = "note-cover"; imgContainer.innerHTML = `Cover`; card.appendChild(imgContainer); } // Inner Content const inner = document.createElement("div"); inner.className = "note-inner"; // Title if (note.title) { const h3 = document.createElement("h3"); h3.className = "note-title"; h3.textContent = note.title; inner.appendChild(h3); } // Body (truncated in grid, maybe?) if (note.descHtml) { const body = document.createElement("div"); body.className = "note-body"; // Start simple: use innerHTML but maybe strip big images if we used cover? // For now, let's just dump it and style images to fit or hide if first child body.innerHTML = note.descHtml; inner.appendChild(body); } // Tags (Labels) if (note.tags.length > 0) { 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); } }); inner.appendChild(tagContainer); } // Hover Actions (Keep style: at bottom, visible on hover) const actions = document.createElement("div"); actions.className = "note-hover-actions"; // Palette Button Logic const paletteBtnId = `palette-${note.id}`; actions.innerHTML = `
`; // Palette Toggle const paletteBtn = actions.querySelector(`#${paletteBtnId}`); const palettePopup = actions.querySelector(`#popup-${paletteBtnId}`); paletteBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); // Close others? document.querySelectorAll(".palette-popup.open").forEach((p) => { if (p !== palettePopup) p.classList.remove("open"); }); palettePopup.classList.toggle("open"); positionPalettePopup(palettePopup); }); inner.appendChild(actions); card.appendChild(inner); container.appendChild(card); }); } function setModalPinButtonState(button, isPinned) { if (!button) return; button.classList.toggle("active", isPinned); button.title = isPinned ? "Désépingler" : "Épingler"; const icon = button.querySelector("i"); if (icon) { icon.className = `mdi ${isPinned ? "mdi-pin" : "mdi-pin-outline"}`; } } function renderModalTags(container, tags) { if (!container) return; container.innerHTML = ""; const visibleTags = (tags || []).filter((tag) => tag && tag !== "note"); if (visibleTags.length === 0) { container.classList.add("is-empty"); return; } container.classList.remove("is-empty"); visibleTags.forEach((tag) => { const tagEl = document.createElement("span"); tagEl.className = "note-tag"; tagEl.textContent = tag; container.appendChild(tagEl); }); } function openNoteModal(note) { const modal = document.querySelector(".note-modal-overlay"); if (!modal) return; const modalCard = modal.querySelector(".note-modal"); const title = modal.querySelector("#note-modal-title"); const content = modal.querySelector(".note-modal-content"); const tagsContainer = modal.querySelector("#note-modal-tags"); const editLink = modal.querySelector("#note-modal-edit"); const pinButton = modal.querySelector("#note-modal-pin"); const modalColorPopup = modal.querySelector("#note-modal-color-popup"); modal.currentNote = note; modalCard.className = "note-modal"; applyNoteVisualState(modalCard, note); modalCard.dataset.noteId = note.id || ""; modalCard.dataset.editUrl = note.editUrl || ""; modalCard.dataset.deleteUrl = note.deleteUrl || ""; modalCard.dataset.background = note.background || "none"; const visibleTags = (note.tags || []).filter((tag) => tag && tag !== "note"); modalCard.dataset.tags = visibleTags.join("||"); title.textContent = note.title || "Sans titre"; content.innerHTML = `
${note.descHtml || ""}
`; renderModalTags(tagsContainer, visibleTags); if (editLink) { editLink.href = note.editUrl || "#"; } if (modalColorPopup) { modalColorPopup.innerHTML = generateModalPaletteButtons(note); modalColorPopup.classList.remove("open"); } setModalPinButtonState(pinButton, !!note.isPinned); modal.classList.add("open"); } function generateModalPaletteButtons(note) { const currentColor = note.color || "default"; const currentBackground = normalizeBackgroundKey(note.background || "") || "none"; const colorButtons = [ ``, ...NOTE_COLOR_OPTIONS.filter((opt) => opt.key !== "default").map( (opt) => { const swatchColor = getThemeColorValue(opt); return ``; }, ), ].join(""); const backgroundButtons = [ ``, ...getAvailableBackgroundOptionsForMode().map((bg) => { const bgUrl = getNoteBackgroundUrl(bg.key); return ``; }), ].join(""); return `
${colorButtons}
${backgroundButtons}
`; } window.setModalNoteColor = function (color) { const modal = document.querySelector(".note-modal-overlay"); if (!modal || !modal.currentNote) return; const currentNote = modal.currentNote; setNoteColor(currentNote.id, color, currentNote.editUrl); currentNote.color = color; const modalCard = modal.querySelector(".note-modal"); if (modalCard) { applyNoteVisualState(modalCard, currentNote); } const modalColorPopup = modal.querySelector("#note-modal-color-popup"); if (modalColorPopup) { modalColorPopup.innerHTML = generateModalPaletteButtons(currentNote); modalColorPopup.classList.add("open"); positionPalettePopup(modalColorPopup); } }; window.setModalNoteBackground = function (backgroundKey) { const modal = document.querySelector(".note-modal-overlay"); if (!modal || !modal.currentNote) return; const currentNote = modal.currentNote; const normalizedBackgroundKey = backgroundKey === "none" ? "none" : normalizeBackgroundKey(backgroundKey) || "none"; setNoteBackground(currentNote.id, normalizedBackgroundKey, currentNote.editUrl); currentNote.background = normalizedBackgroundKey; const modalCard = modal.querySelector(".note-modal"); if (modalCard) { applyNoteVisualState(modalCard, currentNote); } const modalColorPopup = modal.querySelector("#note-modal-color-popup"); if (modalColorPopup) { modalColorPopup.innerHTML = generateModalPaletteButtons(currentNote); modalColorPopup.classList.add("open"); positionPalettePopup(modalColorPopup); } }; function generatePaletteButtons(note) { const currentColor = note.color || "default"; const currentBackground = normalizeBackgroundKey(note.background || "") || "none"; const colorButtons = [ ``, ...NOTE_COLOR_OPTIONS.filter((opt) => opt.key !== "default").map( (opt) => { const swatchColor = getThemeColorValue(opt); return ``; }, ), ].join(""); const backgroundButtons = [ ``, ...getAvailableBackgroundOptionsForMode().map((bg) => { const bgUrl = getNoteBackgroundUrl(bg.key); return ``; }), ].join(""); return `
${colorButtons}
${backgroundButtons}
`; } window.setNoteColor = function (noteId, color, editUrl) { // 1. Visual Update (Immediate feedback) const card = document.querySelector(`.note-card[data-id="${noteId}"]`); if (card) { const background = card.dataset.background || "none"; applyNoteVisualState(card, { color, background }); } const bookmarkCard = document.querySelector(`.link-outer[data-id="${noteId}"]`); if (bookmarkCard) { const background = bookmarkCard.dataset.background || "none"; applyNoteVisualState(bookmarkCard, { color, background }); const palettePopup = bookmarkCard.querySelector(".bookmark-palette .palette-popup"); if (palettePopup) { palettePopup.innerHTML = generateBookmarkPaletteButtons(noteId, editUrl, color, background); } } const modal = document.querySelector(".note-modal-overlay"); if (modal && modal.currentNote && String(modal.currentNote.id) === String(noteId)) { modal.currentNote.color = color; const modalCard = modal.querySelector(".note-modal"); if (modalCard) { applyNoteVisualState(modalCard, modal.currentNote); } const modalColorPopup = modal.querySelector("#note-modal-color-popup"); if (modalColorPopup) { modalColorPopup.innerHTML = generateModalPaletteButtons(modal.currentNote); } } // 2. Persistence via AJAX Form Submission 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"); // Extract all necessary fields 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); } }); // Update Tags let currentTags = formData.get("lf_tags") || ""; let tagsArray = currentTags.split(/[\s,]+/).filter((t) => t.trim() !== ""); // Remove existing color tags tagsArray = tagsArray.filter((t) => { if (!t.startsWith("note-")) return true; const colorKey = t.substring(5); return !NOTE_COLOR_OPTIONS.some((opt) => opt.key === colorKey); }); // Add new color tag (unless default) if (color !== "default") { tagsArray.push(`note-${color}`); } formData.set("lf_tags", tagsArray.join(" ")); formData.append("save_edit", "1"); // Trigger save action // POST back to Shaarli return fetch(form.action, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: formData.toString(), }); }) .then((response) => { if (response.ok) { console.log(`Color ${color} saved for note ${noteId}`); } else { throw new Error("Failed to save color"); } }) .catch((err) => { console.error("Error saving note color:", err); alert("Erreur lors de la sauvegarde de la couleur. Veuillez rafraîchir la page."); }); }; window.setNoteBackground = function (noteId, backgroundKey, editUrl) { const normalizedBackgroundKey = backgroundKey === "none" ? "none" : normalizeBackgroundKey(backgroundKey) || "none"; const card = document.querySelector(`.note-card[data-id="${noteId}"]`); if (card) { const colorClass = Array.from(card.classList).find((cls) => cls.startsWith("note-color-")); const color = colorClass ? colorClass.replace("note-color-", "") : card.dataset.color || "default"; applyNoteVisualState(card, { color, background: normalizedBackgroundKey }); } const bookmarkCard = document.querySelector(`.link-outer[data-id="${noteId}"]`); if (bookmarkCard) { const colorClass = Array.from(bookmarkCard.classList).find((cls) => cls.startsWith("note-color-")); const color = colorClass ? colorClass.replace("note-color-", "") : bookmarkCard.dataset.color || "default"; applyNoteVisualState(bookmarkCard, { color, background: normalizedBackgroundKey }); const palettePopup = bookmarkCard.querySelector(".bookmark-palette .palette-popup"); if (palettePopup) { palettePopup.innerHTML = generateBookmarkPaletteButtons(noteId, editUrl, color, normalizedBackgroundKey); } } const modal = document.querySelector(".note-modal-overlay"); if (modal && modal.currentNote && String(modal.currentNote.id) === String(noteId)) { modal.currentNote.background = normalizedBackgroundKey; const modalCard = modal.querySelector(".note-modal"); if (modalCard) { applyNoteVisualState(modalCard, modal.currentNote); } const modalColorPopup = modal.querySelector("#note-modal-color-popup"); if (modalColorPopup) { modalColorPopup.innerHTML = generateModalPaletteButtons(modal.currentNote); } } 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.startsWith(NOTE_BACKGROUND_TAG_PREFIX)); if (normalizedBackgroundKey && normalizedBackgroundKey !== "none") { tagsArray.push(`${NOTE_BACKGROUND_TAG_PREFIX}${normalizedBackgroundKey}`); } 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 save background"); } }) .catch((err) => { console.error("Error saving note background:", err); alert("Erreur lors de la sauvegarde du fond. Veuillez rafraîchir la page."); }); }; function addTagToNote(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() !== ""); if (!tagsArray.includes(tag)) tagsArray.push(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; }); } /* ========================================================== PINNED ITEMS LOGIC (Tag: shaarli-pin) ========================================================== */ function initPinnedItems() { const container = document.querySelector(".links-list, .notes-masonry, .notes-list-view"); if (!container) return; // Exit if no container found (e.g. empty page or other view) const items = Array.from(container.children); const pinnedItems = []; items.forEach((item) => { // Support both Standard Link items and Note items if (item.classList.contains("link-outer") || item.classList.contains("note-card")) { let isPinned = false; // 1. Check for Tag 'shaarli-pin' // In note-card, we might need to check dataset or re-parse tags if not visible? // But usually renderNotes puts tags in DOM or we rely on data attribute if we saved it? // Let's rely on finding the tag text in the DOM for consistency with Standard View. // For Note View, tags are in .note-tags > .note-tag // For Standard View, tags are in .link-tag-list > a const itemHtml = item.innerHTML; // Simple search in content (quick & dirty but effective for tag presence) // Better: Select text content of tags specifically to avoid false positives in description const tagElements = item.querySelectorAll(".link-tag-list a, .note-tag"); for (let t of tagElements) { if (t.textContent.trim() === "shaarli-pin") { isPinned = true; break; } } // 2. Enforce Visual State based on Tag Presence const pinBtnIcon = item.querySelector(".mdi-pin-outline, .mdi-pin"); const titleArea = item.querySelector(".link-title, .note-title"); const titleIcon = titleArea ? titleArea.querySelector("i.mdi-pin") : null; if (isPinned) { // It IS Pinned: Ensure UI reflects this pinnedItems.push(item); item.classList.add("is-pinned-tag"); // Button -> Filled Pin if (pinBtnIcon) { pinBtnIcon.classList.remove("mdi-pin-outline"); pinBtnIcon.classList.add("mdi-pin"); if (pinBtnIcon.parentElement) pinBtnIcon.parentElement.classList.add("active"); } // Title -> Add Icon if missing if (titleArea && !titleIcon) { const newIcon = document.createElement("i"); newIcon.className = "mdi mdi-pin"; newIcon.style.color = "var(--primary)"; newIcon.style.marginRight = "8px"; titleArea.prepend(newIcon); } } else { // It is NOT Pinned: Ensure UI reflects this (Clean up native sticky or mismatches) item.classList.remove("is-pinned-tag"); // Button -> Outline Pin if (pinBtnIcon) { pinBtnIcon.classList.remove("mdi-pin"); pinBtnIcon.classList.add("mdi-pin-outline"); if (pinBtnIcon.parentElement) pinBtnIcon.parentElement.classList.remove("active"); } // Title -> Remove Icon if present if (titleIcon) { titleIcon.remove(); } } } }); // 3. Move Pinned Items to Top (Only for standard list, renderNotes already sorts itself) if (container.classList.contains("links-list")) { for (let i = pinnedItems.length - 1; i >= 0; i--) { container.prepend(pinnedItems[i]); } } // 4. Click Listener for all Pin Buttons (Event Delegation) // Avoid adding multiple listeners if init called multiple times? // We'll rely on one global listener on document, but here we add it inside this function which is called once on load. // To be safe, let's remove old one if we could, but anonymous function makes it hard. // Better: Allow this to run, but ensure we don't duplicate. // Since initPinnedItems is called on DOMContentLoaded, it runs once. // Note: We already have the listener attached in previous version. // We will just keep the listener logic here in the replacement. document.addEventListener( "click", function (e) { const btn = e.target.closest('a[href*="do=pin"], .note-hover-actions a[href*="pin"], .link-actions a[href*="pin"]'); if (btn) { e.preventDefault(); e.stopPropagation(); const card = btn.closest(".link-outer, .note-card"); const id = card ? card.dataset.id : null; // Re-derive edit URL if needed let editUrl = btn.href.replace("do=pin", "do=editlink").replace("pin", "editlink"); if (card) { const editBtn = card.querySelector('a[href*="edit_link"], a[href*="admin/shaare"]'); if (editBtn) editUrl = editBtn.href; } if (id && editUrl) { togglePinTag(id, editUrl, btn); } } }, { once: false }, ); // Listener is permanent } function togglePinTag(id, editUrl, btn) { const icon = btn.querySelector("i"); let isPinning = false; if (icon) { if (icon.classList.contains("mdi-pin-outline")) { icon.classList.remove("mdi-pin-outline"); icon.classList.add("mdi-pin"); btn.classList.add("active"); isPinning = true; } else { icon.classList.remove("mdi-pin"); icon.classList.add("mdi-pin-outline"); btn.classList.remove("active"); isPinning = false; } } // Update Title Icon (The one "devant le titre") const card = btn.closest(".link-outer, .note-card"); if (card) { // Update Title Icon const titleArea = card.querySelector(".link-title, .note-title"); if (titleArea) { let titleIcon = titleArea.querySelector("i.mdi-pin"); if (isPinning) { if (!titleIcon) { const newIcon = document.createElement("i"); newIcon.className = "mdi mdi-pin"; newIcon.style.color = "var(--primary)"; newIcon.style.marginRight = "8px"; titleArea.prepend(newIcon); } } else { if (titleIcon) titleIcon.remove(); } } // Update Tag List Visualization // We need to handle both Note Cards (.note-tags) and Standard Links (.link-tag-list) let tagContainer = card.querySelector(".note-tags"); if (!tagContainer) tagContainer = card.querySelector(".link-tag-list"); if (tagContainer) { // Check if tag exists already let existingTagElement = null; // Search in children // Notes: .note-tag // Links: .label-tag > a OR just a const allCandidates = tagContainer.querySelectorAll("*"); for (let el of allCandidates) { if (el.textContent.trim() === "shaarli-pin") { // We found the text. // If note, it's the span.note-tag if (el.classList.contains("note-tag")) { existingTagElement = el; break; } // If link, we want the anchor or its wrapper if (el.tagName === "A") { // Check if wrapped in .label-tag if (el.parentElement.classList.contains("label-tag")) { existingTagElement = el.parentElement; } else { existingTagElement = el; } break; } } } if (isPinning) { if (!existingTagElement) { if (card.classList.contains("note-card")) { // Add Note Tag const span = document.createElement("span"); span.className = "note-tag"; span.textContent = "shaarli-pin"; tagContainer.appendChild(span); } else { // Add Link Tag (Standard View) // Structure: shaarli-pin const wrapper = document.createElement("span"); wrapper.className = "link-tag"; const link = document.createElement("a"); link.href = "?searchtags=shaarli-pin"; link.textContent = "shaarli-pin"; wrapper.appendChild(link); // Append space first for separation if there are other tags if (tagContainer.children.length > 0) { tagContainer.appendChild(document.createTextNode(" ")); } tagContainer.appendChild(wrapper); } } } else { if (existingTagElement) { // Remove the element const prev = existingTagElement.previousSibling; existingTagElement.remove(); // Clean up trailing space/text if it was the last one or between tags if (prev && prev.nodeType === Node.TEXT_NODE && !prev.textContent.trim()) { prev.remove(); } } } } } 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() !== ""); const pinTag = "shaarli-pin"; if (isPinning) { if (!tagsArray.includes(pinTag)) tagsArray.push(pinTag); } else { tagsArray = tagsArray.filter((t) => t !== pinTag); } 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((res) => { if (res.ok) console.log("Pin toggled successfully"); }) .catch((err) => console.error(err)); }