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" }, { key: "custom", label: "Personnalisé", light: "#custom", dark: "#custom" } ]; const NOTE_FILTER_OPTIONS = [ { key: "none", label: "Aucun", icon: "mdi-close-circle-outline" }, { key: "glass", label: "Glass", icon: "mdi-blur" }, { key: "vignette", label: "Vignette", icon: "mdi-vignette" }, { key: "lined", label: "Ligné", icon: "mdi-format-align-justify" }, { key: "grid", label: "Quadrillé", icon: "mdi-grid" }, { key: "noise", label: "Noise", icon: "mdi-grain" }, { key: "dots", label: "Points", icon: "mdi-dots-grid" }, { key: "stripes", label: "Rayures", icon: "mdi-slash-forward" }, ]; const NOTE_FONT_COLOR_OPTIONS = [ { key: "auto", label: "Auto", value: "auto" }, { key: "light", label: "Clair", value: "#f5f7fb" }, { key: "dark", label: "Sombre", value: "#202124" }, { key: "white", label: "Blanc", value: "#ffffff" }, { key: "black", label: "Noir", value: "#000000" }, { key: "custom", label: "Personnalisé", value: "custom" } ]; const NOTE_FONT_COLOR_TAG_PREFIX = "font-"; const NOTE_COLOR_TAG_PREFIX = "note-color-"; const NOTE_FILTER_TAG_PREFIX = "notefilter-"; 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: "radio", label: "Radio", files: { light: "bg-light-radio.jpg", dark: "bg-dark-radio.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"; if (element.dataset.color === "custom" && element.dataset.customColor) { return `custom:${element.dataset.customColor}`; } const colorClass = Array.from(element.classList).find((cls) => cls.startsWith("note-color-")); if (colorClass) return colorClass.replace("note-color-", ""); return element.dataset.color || "default"; } function getElementVisualBackground(element) { if (!element) return "none"; const normalizedBackground = normalizeBackgroundKey(element.dataset.background || ""); return normalizedBackground || "none"; } function normalizeFilterKey(key) { if (typeof key !== "string") return "none"; const normalized = key.trim().toLowerCase(); if (NOTE_FILTER_OPTIONS.some((opt) => opt.key === normalized)) { return normalized; } return "none"; } function getElementVisualFilter(element) { if (!element) return "none"; return normalizeFilterKey(element.dataset.filter || ""); } function getElementVisualFontColor(element) { if (!element) return "auto"; return element.dataset.fontColor || "auto"; } function refreshNoteFilterVisuals() { document.querySelectorAll(".note-card, .note-modal, .link-outer").forEach((element) => { applyNoteVisualState(element, { color: getElementVisualColor(element), filter: getElementVisualFilter(element), background: getElementVisualBackground(element), fontColor: getElementVisualFontColor(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"; } } let backgroundStudioPanelInitialized = false; function ensureBackgroundStudioPanel() { if (backgroundStudioPanelInitialized) return; if (document.getElementById("shaarli-bg-studio")) { backgroundStudioPanelInitialized = true; return; } const panel = document.createElement("div"); panel.id = "shaarli-bg-studio"; panel.className = "bg-studio-panel"; panel.setAttribute("role", "dialog"); panel.setAttribute("aria-modal", "false"); panel.setAttribute("aria-hidden", "true"); panel.style.display = "none"; document.body.appendChild(panel); document.addEventListener("click", (e) => { const p = document.getElementById("shaarli-bg-studio"); if (!p || !p.classList.contains("open")) return; if (e.target.closest("#shaarli-bg-studio")) return; const anchor = p.__anchorEl; if (anchor && (e.target === anchor || e.target.closest(`#${anchor.id}`))) return; closeBackgroundStudioPanel(); }); document.addEventListener("keydown", (e) => { if (e.key !== "Escape") return; const p = document.getElementById("shaarli-bg-studio"); if (!p || !p.classList.contains("open")) return; closeBackgroundStudioPanel(); }); panel.addEventListener("click", (e) => { const actionBtn = e.target.closest("button[data-bg-studio-action]"); if (!actionBtn) return; const panelEl = document.getElementById("shaarli-bg-studio"); if (!panelEl) return; const mode = panelEl.dataset.mode || "entity"; const entityId = panelEl.dataset.entityId || ""; const editUrl = panelEl.dataset.editUrl || ""; const action = actionBtn.dataset.bgStudioAction; if (action === "close") { closeBackgroundStudioPanel(); return; } if (action === "set-color") { const key = actionBtn.dataset.colorKey || "default"; if (mode === "modal") setModalNoteColor(key); else setNoteColor(entityId, key, editUrl); return; } if (action === "set-background") { const key = actionBtn.dataset.bgKey || "none"; if (mode === "modal") setModalNoteBackground(key); else setNoteBackground(entityId, key, editUrl); return; } if (action === "set-filter") { const filterKey = actionBtn.dataset.filter || "none"; if (mode === "modal") setModalNoteFilter(filterKey); else setNoteFilter(entityId, filterKey, editUrl); return; } if (action === "set-query") { panelEl.dataset.query = ""; renderBackgroundStudioPanel(panelEl); const input = panelEl.querySelector(".bg-studio-search-input"); if (input) input.focus(); return; } if (action === "set-defaults") { if (mode === "modal") { setModalNoteColor("default"); setModalNoteFilter("none"); } else { setNoteColor(entityId, "default", editUrl); setNoteFilter(entityId, "none", editUrl); } return; } if (action === "reset-font-color") { if (mode === "modal") { setModalNoteFontColor("auto"); } else { setNoteFontColor(entityId, "auto", editUrl); } return; } if (action === "open-color-picker") { openColorPickerPanel({ mode, entityId, editUrl, type: "background" }); return; } if (action === "open-font-color-picker") { openColorPickerPanel({ mode, entityId, editUrl, type: "font" }); return; } if (action === "set-font-color") { const fontColorKey = actionBtn.dataset.fontColorKey || "auto"; if (mode === "modal") setModalNoteFontColor(fontColorKey); else setNoteFontColor(entityId, fontColorKey, editUrl); return; } }); panel.addEventListener("keydown", (e) => { const targetBtn = e.target && e.target.closest ? e.target.closest("button") : null; if (!targetBtn) return; if (e.key === "Enter" || e.key === " ") { if (targetBtn.hasAttribute("data-bg-studio-action") || targetBtn.classList.contains("bg-studio-thumb") || targetBtn.classList.contains("bg-studio-swatch")) { e.preventDefault(); targetBtn.click(); } return; } const moveFocus = (buttons, nextIndex) => { if (!buttons || buttons.length === 0) return; const idx = Math.max(0, Math.min(buttons.length - 1, nextIndex)); const el = buttons[idx]; if (el && typeof el.focus === "function") el.focus(); }; if (targetBtn.classList.contains("bg-studio-thumb")) { const buttons = Array.from(panel.querySelectorAll(".bg-studio-gallery .bg-studio-thumb")); const currentIndex = buttons.indexOf(targetBtn); if (currentIndex === -1) return; const colSize = 2; if (e.key === "ArrowRight") { e.preventDefault(); moveFocus(buttons, currentIndex + colSize); } else if (e.key === "ArrowLeft") { e.preventDefault(); moveFocus(buttons, currentIndex - colSize); } else if (e.key === "ArrowDown") { e.preventDefault(); moveFocus(buttons, currentIndex + 1); } else if (e.key === "ArrowUp") { e.preventDefault(); moveFocus(buttons, currentIndex - 1); } return; } if (targetBtn.classList.contains("bg-studio-swatch")) { const buttons = Array.from(panel.querySelectorAll(".bg-studio-swatches .bg-studio-swatch")); const currentIndex = buttons.indexOf(targetBtn); if (currentIndex === -1) return; if (e.key === "ArrowRight") { e.preventDefault(); moveFocus(buttons, currentIndex + 1); } else if (e.key === "ArrowLeft") { e.preventDefault(); moveFocus(buttons, currentIndex - 1); } return; } }); panel.addEventListener("input", (e) => { const input = e.target && e.target.matches(".bg-studio-search-input") ? e.target : null; if (!input) return; const panelEl = document.getElementById("shaarli-bg-studio"); if (!panelEl) return; panelEl.dataset.query = input.value || ""; window.clearTimeout(panelEl.__searchTimer); panelEl.__searchTimer = window.setTimeout(() => { renderBackgroundStudioPanel(panelEl); }, 150); }); backgroundStudioPanelInitialized = true; } function closeBackgroundStudioPanel() { const panel = document.getElementById("shaarli-bg-studio"); if (!panel) return; panel.classList.remove("open"); panel.style.display = "none"; panel.setAttribute("aria-hidden", "true"); panel.__anchorEl = null; } function openBackgroundStudioPanel({ anchorEl, mode, entityId, editUrl, currentColor, currentFilter, currentBackground, currentFontColor, title, }) { ensureBackgroundStudioPanel(); const panel = document.getElementById("shaarli-bg-studio"); if (!panel) return; panel.dataset.mode = mode || "entity"; panel.dataset.entityId = entityId || ""; panel.dataset.editUrl = editUrl || ""; panel.dataset.color = currentColor || "default"; panel.dataset.filter = normalizeFilterKey(currentFilter || "") || "none"; panel.dataset.background = normalizeBackgroundKey(currentBackground || "") || "none"; panel.dataset.fontColor = currentFontColor || panel.dataset.fontColor || "auto"; panel.dataset.query = panel.dataset.query || ""; panel.dataset.title = title || "Mes images & couleurs"; panel.__anchorEl = anchorEl || null; renderBackgroundStudioPanel(panel); panel.style.display = "block"; panel.classList.add("open"); panel.setAttribute("aria-hidden", "false"); positionBackgroundStudioPanel(panel, anchorEl); window.requestAnimationFrame(() => { const input = panel.querySelector(".bg-studio-search-input"); if (input && typeof input.focus === "function") { input.focus(); input.select(); } }); } function positionBackgroundStudioPanel(panel, anchorEl) { if (!panel) return; const viewportPadding = 12; const rect = anchorEl && anchorEl.getBoundingClientRect ? anchorEl.getBoundingClientRect() : null; panel.style.left = ""; panel.style.top = ""; panel.style.right = ""; panel.style.bottom = ""; const panelRect = panel.getBoundingClientRect(); const preferredTop = rect ? rect.top - panelRect.height - 10 : viewportPadding; const preferredLeft = rect ? rect.left : viewportPadding; let top = preferredTop; if (top < viewportPadding) { top = rect ? rect.bottom + 10 : viewportPadding; } let left = preferredLeft; if (left + panelRect.width > window.innerWidth - viewportPadding) { left = window.innerWidth - viewportPadding - panelRect.width; } if (left < viewportPadding) left = viewportPadding; if (top + panelRect.height > window.innerHeight - viewportPadding) { top = window.innerHeight - viewportPadding - panelRect.height; } if (top < viewportPadding) top = viewportPadding; panel.style.left = `${Math.round(left)}px`; panel.style.top = `${Math.round(top)}px`; } function normalizeSearchText(value) { return String(value || "") .toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .trim(); } function categorizeBackgroundKey(key, label) { const hay = normalizeSearchText(`${key || ""} ${label || ""}`); if (!key || key === "none") return "solid"; if (hay.includes("degrade") || hay.includes("gradient")) return "gradient"; if (hay.includes("grid") || hay.includes("quadrill") || hay.includes("lign") || hay.includes("feuille")) return "grid"; if (hay.includes("damier") || hay.includes("checker") || hay.includes("texture")) return "texture"; return "image"; } function getBackgroundStudioItems() { const colors = NOTE_COLOR_OPTIONS.map((opt) => ({ type: "color", key: opt.key, label: opt.label, color: getThemeColorValue(opt), keywords: [opt.key, opt.label], })); const backgrounds = [ { type: "background", category: "solid", key: "none", label: "Couleur unie", keywords: ["none", "sans image", "solid", "uni"], }, ...getAvailableBackgroundOptionsForMode().map((bg) => ({ type: "background", category: categorizeBackgroundKey(bg.key, bg.label), key: bg.key, label: bg.label, url: getNoteBackgroundUrl(bg.key), keywords: [bg.key, bg.label, "image", "background"], })), ]; return { colors, backgrounds }; } function renderBackgroundStudioPanel(panel) { if (!panel) return; const activeEl = document.activeElement; const wasSearchFocused = !!( activeEl && panel.contains(activeEl) && activeEl.classList && activeEl.classList.contains("bg-studio-search-input") ); const caretStart = wasSearchFocused && typeof activeEl.selectionStart === "number" ? activeEl.selectionStart : null; const caretEnd = wasSearchFocused && typeof activeEl.selectionEnd === "number" ? activeEl.selectionEnd : null; const title = panel.dataset.title || "Mes images & couleurs"; const color = panel.dataset.color || "default"; const isCustomColor = typeof color === "string" && color.startsWith("custom:"); const currentFilter = panel.dataset.filter || "none"; const currentFontColor = panel.dataset.fontColor || "auto"; const query = normalizeSearchText(panel.dataset.query || ""); const { colors, backgrounds } = getBackgroundStudioItems(); // Filter backgrounds based on search query const filteredBackgrounds = backgrounds.filter((b) => { if (!query) return true; const hay = normalizeSearchText([b.label, b.key, "image", "background"].join(" ")); return hay.includes(query); }); // Generate gallery HTML for backgrounds const galleryHtml = filteredBackgrounds .map((item) => { const isActive = panel.dataset.background === item.key; const common = `class="bg-studio-thumb ${isActive ? "is-active" : ""}" type="button"`; const thumb = item.key === "none" ? `` : ``; return `
${thumb}
${item.label}
`; }) .join(""); // Colors row with custom color picker button const colorsRowHtml = colors .filter((c) => c.key !== "default" && c.key !== "custom") .map((c) => { const isActive = color === c.key; const hex = c.color || ""; return ``; }) .join("") + // Add custom color button (opens color picker) ``; // Font colors row const isCustomFontColor = typeof currentFontColor === "string" && currentFontColor.startsWith("custom:"); const fontColorsRowHtml = NOTE_FONT_COLOR_OPTIONS .filter((c) => c.key !== "custom") .map((c) => { const isActive = currentFontColor === c.key; const value = c.value || "auto"; const style = value === "auto" ? "background: linear-gradient(135deg, #f5f7fb 50%, #202124 50%);" : `background-color: ${value};`; return ``; }) .join("") + // Add custom font color button ``; const filterBtn = (key, icon, label) => { const active = currentFilter === key; return ``; }; const filtersHtml = NOTE_FILTER_OPTIONS.map((f) => filterBtn(f.key, f.icon, f.label)).join(""); const showClear = !!(panel.dataset.query || "").trim(); panel.innerHTML = `
${title}
Colors:
${colorsRowHtml}
Font:
${fontColorsRowHtml}
Filtres:
${filtersHtml}
`; if (wasSearchFocused) { const input = panel.querySelector(".bg-studio-search-input"); if (input && typeof input.focus === "function") { input.focus({ preventScroll: true }); if (typeof input.setSelectionRange === "function") { const len = (input.value || "").length; const start = typeof caretStart === "number" ? Math.max(0, Math.min(len, caretStart)) : len; const end = typeof caretEnd === "number" ? Math.max(0, Math.min(len, caretEnd)) : len; try { input.setSelectionRange(start, end); } catch (e) { // Ignore selection errors for non-text inputs or unsupported browsers. } } } } } function applyNoteVisualState(element, note) { if (!element || !note) return; const rawColor = note.color || "default"; const isCustomColor = typeof rawColor === "string" && rawColor.startsWith("custom:"); const customHex = isCustomColor ? rawColor.substring(7) : ""; const resolvedColorOption = isCustomColor ? null : getColorOption(rawColor); const color = isCustomColor ? "custom" : (resolvedColorOption ? resolvedColorOption.key : "default"); const colorValue = isCustomColor ? customHex : getThemeColorValue(resolvedColorOption); const foregroundColor = getReadableForegroundForBackground(colorValue); const filter = normalizeFilterKey(note.filter || "none"); const normalizedBackground = normalizeBackgroundKey(note.background || ""); const background = normalizedBackground || "none"; const fontColor = note.fontColor || "auto"; element.classList.forEach((cls) => { if (cls.startsWith("note-color-")) element.classList.remove(cls); if (cls.startsWith("note-filter-")) element.classList.remove(cls); }); element.classList.add(`note-color-${color}`); if (filter !== "none") { element.classList.add(`note-filter-${filter}`); } 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 (fontColor && fontColor !== "auto") { let fontValue = null; if (typeof fontColor === "string" && fontColor.startsWith("custom:")) { fontValue = fontColor.substring(7); } else { const opt = NOTE_FONT_COLOR_OPTIONS.find((o) => o.key === fontColor); if (opt && opt.value && opt.value !== "auto") fontValue = opt.value; } if (fontValue) { element.style.setProperty("--note-card-fg", fontValue); } } 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; if (isCustomColor && customHex) { element.dataset.customColor = customHex; } else { element.dataset.customColor = ""; } element.dataset.filter = filter; element.dataset.fontColor = fontColor; } function extractNoteVisualStateFromTags(tags) { const safeTags = Array.isArray(tags) ? tags : []; let color = "default"; const foundColorTag = safeTags.find((t) => typeof t === "string" && t.startsWith(NOTE_COLOR_TAG_PREFIX)); if (foundColorTag) { const candidate = foundColorTag.substring(NOTE_COLOR_TAG_PREFIX.length); if (/^[0-9A-Fa-f]{6}$/.test(candidate)) { color = `custom:#${candidate.toUpperCase()}`; } else if (NOTE_COLOR_OPTIONS.some((opt) => opt.key === candidate)) { color = candidate; } } else { // Backward compat: legacy note-custom- const legacyCustomTag = safeTags.find((t) => typeof t === "string" && t.startsWith("note-custom-")); if (legacyCustomTag) { const candidate = legacyCustomTag.substring("note-custom-".length); if (/^[0-9A-Fa-f]{6}$/.test(candidate)) { color = `custom:#${candidate.toUpperCase()}`; } } // Backward compat: legacy note- const legacyColorTag = safeTags.find((t) => typeof t === "string" && t.startsWith("note-")); if (legacyColorTag) { const candidate = legacyColorTag.substring(5); if (NOTE_COLOR_OPTIONS.some((opt) => opt.key === candidate)) { color = candidate; } } } let filter = "none"; const foundFilterTag = safeTags.find((t) => typeof t === "string" && t.startsWith(NOTE_FILTER_TAG_PREFIX)); if (foundFilterTag) { const candidate = normalizeFilterKey(foundFilterTag.substring(NOTE_FILTER_TAG_PREFIX.length)); if (candidate && candidate !== "none") { filter = 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; } } let fontColor = "auto"; const foundFontTag = safeTags.find((t) => typeof t === "string" && t.startsWith(NOTE_FONT_COLOR_TAG_PREFIX)); if (foundFontTag) { const raw = foundFontTag.substring(NOTE_FONT_COLOR_TAG_PREFIX.length); const hex = raw.startsWith("#") ? raw : `#${raw}`; if (/^#[0-9A-Fa-f]{6}$/.test(hex)) { fontColor = `custom:${hex.toUpperCase()}`; } } return { color, filter, background, fontColor }; } 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, filter, background, fontColor } = extractNoteVisualStateFromTags(tags); applyNoteVisualState(card, { color, filter, background, fontColor }); 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}`); if (!paletteBtn) return; paletteBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); openBackgroundStudioPanel({ anchorEl: paletteBtn, mode: "entity", entityId: cardId, editUrl, currentColor: getElementVisualColor(card), currentFilter: getElementVisualFilter(card), currentBackground: getElementVisualBackground(card), currentFontColor: getElementVisualFontColor(card), title: "Mes images & couleurs", }); }); }); document.addEventListener("click", (e) => { if (e.target.closest(".bookmark-palette")) return; if (e.target.closest("#shaarli-bg-studio")) return; closeBackgroundStudioPanel(); }); } function generateBookmarkPaletteButtons(bookmarkId, editUrl, currentColor, currentFilter) { return generateUnifiedPaletteMenu({ entityId: bookmarkId, editUrl, currentColor, currentFilter, mode: "entity", }); } function generateUnifiedPaletteMenu({ entityId, editUrl, currentColor, currentFilter, mode }) { const color = currentColor || "default"; const filter = normalizeFilterKey(currentFilter || "") || "none"; const onColorClick = mode === "modal" ? (c) => `setModalNoteColor('${c}')` : (c) => `setNoteColor('${entityId}', '${c}', '${editUrl}')`; const onFilterClick = mode === "modal" ? (k) => `setModalNoteFilter('${k}')` : (k) => `setNoteFilter('${entityId}', '${k}', '${editUrl}')`; const colorButtons = [ ``, ...NOTE_COLOR_OPTIONS.filter((opt) => opt.key !== "default").map((opt) => { const swatchColor = getThemeColorValue(opt); return ``; }), ].join(""); const filterButtons = [ ``, ...NOTE_FILTER_OPTIONS.filter((opt) => opt.key !== "none").map((opt) => { return ``; }), ].join(""); return `
Couleurs
${colorButtons}
Filtres
${filterButtons}
`; } 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 filter = card.dataset.filter; note.filter = filter && filter !== "none" ? filter : "none"; 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(); const modalCard = modalOverlay.querySelector(".note-modal"); openBackgroundStudioPanel({ anchorEl: modalColorBtn, mode: "modal", entityId: modalCard ? modalCard.dataset.noteId || "" : "", editUrl: modalCard ? modalCard.dataset.editUrl || "" : "", currentColor: modalCard ? getElementVisualColor(modalCard) : "default", currentFilter: modalCard ? getElementVisualFilter(modalCard) : "none", currentBackground: modalCard ? getElementVisualBackground(modalCard) : "none", currentFontColor: modalCard ? getElementVisualFontColor(modalCard) : "auto", title: "Mes images & couleurs", }); }); modalPinBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); const modalCard = modalOverlay.querySelector(".note-modal"); const noteId = modalCard.dataset.noteId; const editUrl = modalCard.dataset.editUrl; if (!noteId || !editUrl) return; togglePinTag(noteId, editUrl, modalPinBtn); const isPinned = modalPinBtn.classList.contains("active"); let tags = (modalCard.dataset.tags || "") .split("||") .filter((t) => t); if (isPinned) { if (!tags.includes("shaarli-pin")) tags.push("shaarli-pin"); } else { tags = tags.filter((t) => t !== "shaarli-pin"); } modalCard.dataset.tags = tags.join("||"); renderModalTags(modalOverlay.querySelector("#note-modal-tags"), tags); if (modalOverlay.currentNote) { modalOverlay.currentNote.isPinned = isPinned; modalOverlay.currentNote.tags = tags; } }); modalOverlay.querySelector("#note-modal-delete").addEventListener("click", () => { const modalCard = modalOverlay.querySelector(".note-modal"); const deleteUrl = modalCard.dataset.deleteUrl; if (deleteUrl && deleteUrl !== "#") { window.location.href = deleteUrl; } }); 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 rawTags = []; linkEl.querySelectorAll(".link-tag-list a").forEach((tag) => { const t = (tag.textContent || "").trim(); if (t) rawTags.push(t); }); const { color, filter, background, fontColor } = extractNoteVisualStateFromTags(rawTags); const isLegacyNoteColorTag = (t) => { if (typeof t !== "string") return false; if (!t.startsWith("note-")) return false; const candidate = t.substring(5); return NOTE_COLOR_OPTIONS.some((opt) => opt.key === candidate); }; const tags = rawTags.filter((t) => { if (t.startsWith(NOTE_COLOR_TAG_PREFIX)) return false; if (t.startsWith("note-custom-")) return false; if (isLegacyNoteColorTag(t)) return false; if (t.startsWith(NOTE_FILTER_TAG_PREFIX)) return false; if (t.startsWith(NOTE_BACKGROUND_TAG_PREFIX)) return false; if (t.startsWith(NOTE_FONT_COLOR_TAG_PREFIX)) return false; return true; }); 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, filter, background, fontColor, 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}`); paletteBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); openBackgroundStudioPanel({ anchorEl: paletteBtn, mode: "entity", entityId: note.id, editUrl: note.editUrl, currentColor: getElementVisualColor(card), currentFilter: getElementVisualFilter(card), currentBackground: getElementVisualBackground(card), title: "Mes images & couleurs", }); }); 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) { return generateUnifiedPaletteMenu({ entityId: note && note.id ? note.id : "", editUrl: note && note.editUrl ? note.editUrl : "", currentColor: note && note.color ? note.color : "default", currentFilter: note && note.filter ? note.filter : "none", mode: "modal", }); } 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.setModalNoteFilter = function (filterKey) { const modal = document.querySelector(".note-modal-overlay"); if (!modal || !modal.currentNote) return; const currentNote = modal.currentNote; const normalizedFilterKey = normalizeFilterKey(filterKey) || "none"; setNoteFilter(currentNote.id, normalizedFilterKey, currentNote.editUrl); currentNote.filter = normalizedFilterKey; 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) { return generateUnifiedPaletteMenu({ entityId: note && note.id ? note.id : "", editUrl: note && note.editUrl ? note.editUrl : "", currentColor: note && note.color ? note.color : "default", currentFilter: note && note.filter ? note.filter : "none", mode: "entity", }); } window.setNoteColor = function (noteId, color, editUrl) { // 1. Visual Update (Immediate feedback) setNoteColorVisual(noteId, color); const bookmarkCard = document.querySelector(`.link-outer[data-id="${noteId}"]`); if (bookmarkCard) { const palettePopup = bookmarkCard.querySelector(".bookmark-palette .palette-popup"); if (palettePopup) { const currentColor = getElementVisualColor(bookmarkCard); const currentFilter = getElementVisualFilter(bookmarkCard); palettePopup.innerHTML = generateBookmarkPaletteButtons(noteId, editUrl, currentColor, currentFilter); } } 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 (only one note-color-* allowed) tagsArray = tagsArray.filter((t) => !t.startsWith(NOTE_COLOR_TAG_PREFIX)); // Backward compat cleanup: remove legacy note-custom- tagsArray = tagsArray.filter((t) => !t.startsWith("note-custom-")); // Backward compat cleanup: remove legacy note- 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") { if (typeof color === "string" && color.startsWith("custom:")) { const cleanColor = color.substring(7).replace("#", ""); tagsArray.push(`${NOTE_COLOR_TAG_PREFIX}${cleanColor}`); } else { tagsArray.push(`${NOTE_COLOR_TAG_PREFIX}${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.setNoteFilter = function (noteId, filterKey, editUrl) { const normalizedFilterKey = normalizeFilterKey(filterKey) || "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, filter: normalizedFilterKey }); } 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, filter: normalizedFilterKey }); const palettePopup = bookmarkCard.querySelector(".bookmark-palette .palette-popup"); if (palettePopup) { palettePopup.innerHTML = generateBookmarkPaletteButtons(noteId, editUrl, color, normalizedFilterKey); } } const modal = document.querySelector(".note-modal-overlay"); if (modal && modal.currentNote && String(modal.currentNote.id) === String(noteId)) { modal.currentNote.filter = normalizedFilterKey; 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_FILTER_TAG_PREFIX)); if (normalizedFilterKey && normalizedFilterKey !== "none") { tagsArray.push(`${NOTE_FILTER_TAG_PREFIX}${normalizedFilterKey}`); } 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 filter"); } }) .catch((err) => { console.error("Error saving note filter:", err); alert("Erreur lors de la sauvegarde du filtre. 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"; const filter = card.dataset.filter || "none"; applyNoteVisualState(card, { color, filter, 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"; const filter = bookmarkCard.dataset.filter || "none"; applyNoteVisualState(bookmarkCard, { color, filter, background: 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."); }); }; 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 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)); } /* ========================================================== COLOR PICKER PANEL ========================================================== */ let colorPickerPanelInitialized = false; function ensureColorPickerPanel() { if (colorPickerPanelInitialized) return; if (document.getElementById("shaarli-color-picker")) { colorPickerPanelInitialized = true; return; } const panel = document.createElement("div"); panel.id = "shaarli-color-picker"; panel.className = "color-picker-panel"; panel.setAttribute("role", "dialog"); panel.setAttribute("aria-modal", "false"); panel.setAttribute("aria-hidden", "true"); panel.style.display = "none"; document.body.appendChild(panel); document.addEventListener("click", (e) => { const p = document.getElementById("shaarli-color-picker"); if (!p || !p.classList.contains("open")) return; if (e.target.closest("#shaarli-color-picker")) return; if (e.target.closest("#shaarli-bg-studio")) return; closeColorPickerPanel(); }); document.addEventListener("keydown", (e) => { if (e.key !== "Escape") return; const p = document.getElementById("shaarli-color-picker"); if (!p || !p.classList.contains("open")) return; closeColorPickerPanel(); }); colorPickerPanelInitialized = true; } function closeColorPickerPanel() { const panel = document.getElementById("shaarli-color-picker"); if (!panel) return; const type = panel.dataset.type || "background"; const skipRestore = panel.dataset.skipRestore === "1"; if (!skipRestore && type === "font") { const prevFontColorKey = panel.dataset.prevFontColorKey || "auto"; const mode = panel.dataset.mode || "entity"; const entityId = panel.dataset.entityId || ""; if (mode === "modal") { setModalNoteFontColorVisual(prevFontColorKey); } else { setNoteFontColorVisual(entityId, prevFontColorKey); } } if (!skipRestore && type === "background") { const prevColorKey = panel.dataset.prevColorKey || "default"; const mode = panel.dataset.mode || "entity"; const entityId = panel.dataset.entityId || ""; if (mode === "modal") { setModalNoteColorVisual(prevColorKey); } else { setNoteColorVisual(entityId, prevColorKey); } } panel.dataset.skipRestore = "0"; panel.classList.remove("open"); panel.style.display = "none"; panel.setAttribute("aria-hidden", "true"); } function openColorPickerPanel({ mode, entityId, editUrl, type }) { ensureColorPickerPanel(); const panel = document.getElementById("shaarli-color-picker"); if (!panel) return; panel.dataset.mode = mode || "entity"; panel.dataset.entityId = entityId || ""; panel.dataset.editUrl = editUrl || ""; panel.dataset.type = type || "background"; const isDark = getCurrentThemeMode() === "dark"; const defaultColor = isDark ? "#20293A" : "#ffffff"; if ((type || "background") === "font") { let prevFontColorKey = "auto"; if ((mode || "entity") === "modal") { const modal = document.querySelector(".note-modal-overlay"); const modalCard = modal ? modal.querySelector(".note-modal") : null; prevFontColorKey = (modalCard && modalCard.dataset.fontColor) ? modalCard.dataset.fontColor : "auto"; } else { const noteCard = document.querySelector(`.note-card[data-id="${entityId}"]`); const bookmarkCard = document.querySelector(`.link-outer[data-id="${entityId}"]`); prevFontColorKey = (noteCard && noteCard.dataset.fontColor) ? noteCard.dataset.fontColor : ((bookmarkCard && bookmarkCard.dataset.fontColor) ? bookmarkCard.dataset.fontColor : "auto"); } panel.dataset.prevFontColorKey = prevFontColorKey; } else { panel.dataset.prevFontColorKey = ""; } if ((type || "background") === "background") { let prevColorKey = "default"; if ((mode || "entity") === "modal") { const modal = document.querySelector(".note-modal-overlay"); const modalCard = modal ? modal.querySelector(".note-modal") : null; if (modalCard) { const isCustom = modalCard.dataset.color === "custom"; const cc = modalCard.dataset.customColor || ""; prevColorKey = isCustom && cc ? `custom:${cc}` : getElementVisualColor(modalCard); } } else { const noteCard = document.querySelector(`.note-card[data-id="${entityId}"]`); const bookmarkCard = document.querySelector(`.link-outer[data-id="${entityId}"]`); const el = noteCard || bookmarkCard; if (el) { const isCustom = el.dataset.color === "custom"; const cc = el.dataset.customColor || ""; prevColorKey = isCustom && cc ? `custom:${cc}` : getElementVisualColor(el); } } panel.dataset.prevColorKey = prevColorKey; } else { panel.dataset.prevColorKey = ""; } panel.innerHTML = `
${type === "font" ? "Couleur du texte" : "Couleur personnalisée"}
`; // Initialize color picker state let currentHue = 210; let currentSaturation = 50; let currentValue = 50; let currentHex = defaultColor; const gradient = panel.querySelector("#color-gradient"); const cursor = panel.querySelector("#color-cursor"); const hueSlider = panel.querySelector("#hue-slider"); const hexInput = panel.querySelector("#hex-input"); const preview = panel.querySelector("#color-preview"); function hsvToHex(h, s, v) { const c = v * s; const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); const m = v - c; let r, g, b; if (h < 60) { r = c; g = x; b = 0; } else if (h < 120) { r = x; g = c; b = 0; } else if (h < 180) { r = 0; g = c; b = x; } else if (h < 240) { r = 0; g = x; b = c; } else if (h < 300) { r = x; g = 0; b = c; } else { r = c; g = 0; b = x; } const toHex = (n) => Math.round((n + m) * 255).toString(16).padStart(2, "0"); return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase(); } function hexToHsv(hex) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); if (!result) return { h: 0, s: 0, v: 0 }; const r = parseInt(result[1], 16) / 255; const g = parseInt(result[2], 16) / 255; const b = parseInt(result[3], 16) / 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const d = max - min; let h = 0; if (d !== 0) { if (max === r) h = ((g - b) / d + 6) % 6 * 60; else if (max === g) h = ((b - r) / d + 2) * 60; else h = ((r - g) / d + 4) * 60; } const s = max === 0 ? 0 : d / max; const v = max; return { h, s, v }; } function updateColor() { currentHex = hsvToHex(currentHue, currentSaturation / 100, currentValue / 100); gradient.style.background = `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, hsl(${currentHue}, 100%, 50%))`; cursor.style.left = `${currentSaturation}%`; cursor.style.top = `${100 - currentValue}%`; preview.style.backgroundColor = currentHex; hexInput.value = currentHex; if ((panel.dataset.type || "background") === "font") { const previewKey = `custom:${currentHex}`; const pMode = panel.dataset.mode || "entity"; const pEntityId = panel.dataset.entityId || ""; if (pMode === "modal") { setModalNoteFontColorVisual(previewKey); } else { setNoteFontColorVisual(pEntityId, previewKey); } } if ((panel.dataset.type || "background") === "background") { const previewKey = `custom:${currentHex}`; const pMode = panel.dataset.mode || "entity"; const pEntityId = panel.dataset.entityId || ""; if (pMode === "modal") { setModalNoteColorVisual(previewKey); } else { setNoteColorVisual(pEntityId, previewKey); } } } function setFromHex(hex) { const hsv = hexToHsv(hex); currentHue = hsv.h; currentSaturation = hsv.s * 100; currentValue = hsv.v * 100; hueSlider.value = currentHue; updateColor(); } // Event listeners let isDragging = false; gradient.addEventListener("mousedown", (e) => { isDragging = true; const rect = gradient.getBoundingClientRect(); currentSaturation = Math.max(0, Math.min(100, ((e.clientX - rect.left) / rect.width) * 100)); currentValue = Math.max(0, Math.min(100, 100 - ((e.clientY - rect.top) / rect.height) * 100)); updateColor(); }); document.addEventListener("mousemove", (e) => { if (!isDragging) return; const rect = gradient.getBoundingClientRect(); currentSaturation = Math.max(0, Math.min(100, ((e.clientX - rect.left) / rect.width) * 100)); currentValue = Math.max(0, Math.min(100, 100 - ((e.clientY - rect.top) / rect.height) * 100)); updateColor(); }); document.addEventListener("mouseup", () => { isDragging = false; }); hueSlider.addEventListener("input", (e) => { currentHue = parseInt(e.target.value); updateColor(); }); hexInput.addEventListener("input", (e) => { const hex = e.target.value; if (/^#[0-9A-F]{6}$/i.test(hex)) { setFromHex(hex); } }); // Initialize if ((panel.dataset.type || "background") === "font") { const prevFontColorKey = panel.dataset.prevFontColorKey || "auto"; let initialHex = defaultColor; if (typeof prevFontColorKey === "string" && prevFontColorKey.startsWith("custom:")) { initialHex = prevFontColorKey.substring(7); } else if (prevFontColorKey !== "auto") { const opt = NOTE_FONT_COLOR_OPTIONS.find((o) => o.key === prevFontColorKey); if (opt && opt.value && opt.value !== "auto") initialHex = opt.value; } setFromHex(initialHex); } else if ((panel.dataset.type || "background") === "background") { const prevColorKey = panel.dataset.prevColorKey || "default"; let initialHex = defaultColor; if (typeof prevColorKey === "string" && prevColorKey.startsWith("custom:")) { initialHex = prevColorKey.substring(7); } setFromHex(initialHex); } else { setFromHex(defaultColor); } // Position the color picker panel positionColorPickerPanel(panel, mode); panel.style.display = "block"; panel.classList.add("open"); panel.setAttribute("aria-hidden", "false"); } function positionColorPickerPanel(panel, mode) { if (!panel) return; const bgPanel = document.getElementById("shaarli-bg-studio"); const viewportPadding = 12; // Default position if bgPanel is not available let anchorRect = null; if (bgPanel && bgPanel.classList.contains("open")) { const bgRect = bgPanel.getBoundingClientRect(); // Position to the right of the bg-studio panel let left = bgRect.right + 10; let top = bgRect.top; // Check if it fits on the right if (left + panel.offsetWidth > window.innerWidth - viewportPadding) { // Position to the left instead left = bgRect.left - panel.offsetWidth - 10; } // Ensure it stays within viewport if (left < viewportPadding) left = viewportPadding; if (top + panel.offsetHeight > window.innerHeight - viewportPadding) { top = window.innerHeight - viewportPadding - panel.offsetHeight; } if (top < viewportPadding) top = viewportPadding; panel.style.left = `${Math.round(left)}px`; panel.style.top = `${Math.round(top)}px`; panel.style.right = ""; panel.style.bottom = ""; } else { // Center in viewport if bg panel not available const panelRect = panel.getBoundingClientRect(); const left = Math.max(viewportPadding, (window.innerWidth - panelRect.width) / 2); const top = Math.max(viewportPadding, (window.innerHeight - panelRect.height) / 2); panel.style.left = `${Math.round(left)}px`; panel.style.top = `${Math.round(top)}px`; } } // Handle color picker actions document.addEventListener("click", (e) => { const actionBtn = e.target.closest("[data-color-picker-action]"); if (!actionBtn) return; const panel = document.getElementById("shaarli-color-picker"); if (!panel) return; const action = actionBtn.dataset.colorPickerAction; if (action === "close") { closeColorPickerPanel(); return; } if (action === "apply") { const mode = panel.dataset.mode || "entity"; const entityId = panel.dataset.entityId || ""; const editUrl = panel.dataset.editUrl || ""; const type = panel.dataset.type || "background"; const hexInput = panel.querySelector("#hex-input"); const color = hexInput ? hexInput.value : "#ffffff"; if (type === "font") { if (mode === "modal") { setModalCustomFontColor(color); } else { setNoteFontColor(entityId, `custom:${color}`, editUrl); } } else { if (mode === "modal") { setModalCustomNoteColor(color); } else { setNoteColor(entityId, `custom:${color}`, editUrl); } } panel.dataset.skipRestore = "1"; closeColorPickerPanel(); } }); function setNoteFontColorVisual(noteId, fontColorKey) { const card = document.querySelector(`.note-card[data-id="${noteId}"]`); const bookmarkCard = document.querySelector(`.link-outer[data-id="${noteId}"]`); let colorValue = "auto"; if (typeof fontColorKey === "string" && fontColorKey.startsWith("custom:")) { colorValue = fontColorKey.substring(7); } else if (fontColorKey !== "auto") { const option = NOTE_FONT_COLOR_OPTIONS.find((opt) => opt.key === fontColorKey); colorValue = option ? option.value : "auto"; } const applyTo = (element) => { if (!element) return; if (colorValue === "auto") element.style.removeProperty("--note-card-fg"); else element.style.setProperty("--note-card-fg", colorValue); element.dataset.fontColor = fontColorKey; }; applyTo(card); applyTo(bookmarkCard); const modal = document.querySelector(".note-modal-overlay"); if (modal && modal.currentNote && String(modal.currentNote.id) === String(noteId)) { const modalCard = modal.querySelector(".note-modal"); applyTo(modalCard); } } function setModalNoteFontColorVisual(fontColorKey) { const modal = document.querySelector(".note-modal-overlay"); if (!modal || !modal.currentNote) return; setNoteFontColorVisual(modal.currentNote.id, fontColorKey); } function setNoteColorVisual(noteId, colorKey) { const card = document.querySelector(`.note-card[data-id="${noteId}"]`); const bookmarkCard = document.querySelector(`.link-outer[data-id="${noteId}"]`); const applyTo = (element) => { if (!element) return; const currentState = { color: getElementVisualColor(element), filter: getElementVisualFilter(element), background: getElementVisualBackground(element), fontColor: getElementVisualFontColor(element), }; if (typeof colorKey === "string" && colorKey.startsWith("custom:")) { const hex = colorKey.substring(7); element.classList.forEach((cls) => { if (cls.startsWith("note-color-")) element.classList.remove(cls); }); element.classList.add("note-color-custom"); element.style.backgroundColor = hex; element.style.borderColor = "transparent"; element.dataset.color = "custom"; element.dataset.customColor = hex; const fg = getReadableForegroundForBackground(hex); if (fg) element.style.setProperty("--note-card-fg", fg); return; } element.dataset.customColor = ""; applyNoteVisualState(element, { ...currentState, color: colorKey }); }; applyTo(card); applyTo(bookmarkCard); const modal = document.querySelector(".note-modal-overlay"); if (modal && modal.currentNote && String(modal.currentNote.id) === String(noteId)) { const modalCard = modal.querySelector(".note-modal"); applyTo(modalCard); } } function setModalNoteColorVisual(colorKey) { const modal = document.querySelector(".note-modal-overlay"); if (!modal || !modal.currentNote) return; setNoteColorVisual(modal.currentNote.id, colorKey); } /* ========================================================== FONT COLOR FUNCTIONS ========================================================== */ function setNoteFontColor(noteId, fontColorKey, editUrl) { // 1. Visual Update (Immediate feedback) setNoteFontColorVisual(noteId, fontColorKey); // 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"); 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() !== ""); // Remove existing font color tags tagsArray = tagsArray.filter((t) => !t.startsWith(NOTE_FONT_COLOR_TAG_PREFIX)); // Add new font color tag (unless auto) if (fontColorKey !== "auto") { if (fontColorKey.startsWith("custom:")) { tagsArray.push(`${NOTE_FONT_COLOR_TAG_PREFIX}${fontColorKey.substring(7).replace("#", "")}`); } else { const option = NOTE_FONT_COLOR_OPTIONS.find((opt) => opt.key === fontColorKey); if (option && option.value !== "auto") { tagsArray.push(`${NOTE_FONT_COLOR_TAG_PREFIX}${option.value.substring(1)}`); } } } 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) { console.log(`Font color ${fontColorKey} saved for note ${noteId}`); } else { throw new Error("Failed to save font color"); } }) .catch((err) => { console.error("Error saving note font color:", err); }); } function setModalNoteFontColor(fontColorKey) { const modal = document.querySelector(".note-modal-overlay"); if (!modal || !modal.currentNote) return; const currentNote = modal.currentNote; setNoteFontColor(currentNote.id, fontColorKey, currentNote.editUrl); currentNote.fontColor = fontColorKey; const modalCard = modal.querySelector(".note-modal"); if (modalCard) { let colorValue = "auto"; if (fontColorKey.startsWith("custom:")) { colorValue = fontColorKey.substring(7); } else if (fontColorKey !== "auto") { const option = NOTE_FONT_COLOR_OPTIONS.find((opt) => opt.key === fontColorKey); colorValue = option ? option.value : "auto"; } if (colorValue === "auto") { modalCard.style.removeProperty("--note-card-fg"); } else { modalCard.style.setProperty("--note-card-fg", colorValue); } modalCard.dataset.fontColor = fontColorKey; } } function setModalCustomFontColor(color) { setModalNoteFontColor(`custom:${color}`); } /* ========================================================== CUSTOM NOTE COLOR FUNCTIONS ========================================================== */ function setModalCustomNoteColor(color) { const modal = document.querySelector(".note-modal-overlay"); if (!modal || !modal.currentNote) return; const currentNote = modal.currentNote; // Apply custom color visually const modalCard = modal.querySelector(".note-modal"); if (modalCard) { modalCard.style.backgroundColor = color; modalCard.style.borderColor = "transparent"; modalCard.dataset.color = "custom"; modalCard.dataset.customColor = color; } // Save via AJAX fetch(currentNote.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() !== ""); // Remove existing color tags (only one note-color-* allowed) tagsArray = tagsArray.filter((t) => !t.startsWith(NOTE_COLOR_TAG_PREFIX)); // Backward compat cleanup: remove legacy note-custom- tagsArray = tagsArray.filter((t) => !t.startsWith("note-custom-")); // Backward compat cleanup: remove legacy note- 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 custom color tag const cleanColor = color.replace("#", ""); tagsArray.push(`${NOTE_COLOR_TAG_PREFIX}${cleanColor}`); 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 custom color"); console.log(`Custom color ${color} saved for note ${currentNote.id}`); }) .catch((err) => { console.error("Error saving custom color:", err); }); }