From 8b62046fa79bc79654cf354138f8908d48661612 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Sun, 22 Feb 2026 09:28:37 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20refactorer=20interface=20cr=C3=A9ation/?= =?UTF-8?q?=C3=A9dition=20notes=20avec=20input=20Google=20Keep-like=20(exp?= =?UTF-8?q?and=20on=20focus,=20formatting=20toolbar=20avec=20boutons=20B/I?= =?UTF-8?q?/U/H1-H3/liste/lien,=20textarea=20auto-resize),=20modal=20?= =?UTF-8?q?=C3=A9dition=20unifi=C3=A9e=20avec=20textarea=20source=20=C3=A9?= =?UTF-8?q?ditable,=20styles=20dark=20mode=20optimis=C3=A9s=20(backgrounds?= =?UTF-8?q?=20#202124,=20shadows=20rgba),=20hover=20actions=20repositionn?= =?UTF-8?q?=C3=A9s=20(gap=202px,=20margin-left=20-6px,=20opacity=200.7?= =?UTF-8?q?=E2=86=921),=20note=20cards=20avec=20transitions=20cubic-bezier?= =?UTF-8?q?=20et=20border-color=20transparent=20au=20hover,=20et=20am?= =?UTF-8?q?=C3=A9lioration=20g=C3=A9n=C3=A9rale=20UX=20avec=20animations?= =?UTF-8?q?=20fade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shaarli-pro/css/custom_views.css | 347 ++++++++++--- shaarli-pro/includes.html | 10 +- shaarli-pro/js/custom_views.js | 853 +++++++++++++++++++++++++++++-- 3 files changed, 1091 insertions(+), 119 deletions(-) diff --git a/shaarli-pro/css/custom_views.css b/shaarli-pro/css/custom_views.css index d19391c..c50c168 100644 --- a/shaarli-pro/css/custom_views.css +++ b/shaarli-pro/css/custom_views.css @@ -229,8 +229,15 @@ body.view-notes .content-container { align-items: flex-start; margin-bottom: 2rem; position: relative; - padding-right: 60px; - /* Space for toggle */ + padding-right: 0; +} + +.notes-top-bar-inner { + width: 600px; + max-width: 100%; + display: flex; + align-items: flex-start; + gap: 12px; } .note-input-container { @@ -243,10 +250,14 @@ body.view-notes .content-container { overflow: hidden; } +.note-input-container.is-editing { + max-height: 72vh; +} + [data-theme="dark"] .note-input-container { - background-color: var(--bg-sidebar); - border: 1px solid var(--border); - box-shadow: none; + background-color: #202124; + border: none; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.6), 0 2px 6px 2px rgba(0, 0, 0, 0.4); } .note-input-collapsed { @@ -259,6 +270,188 @@ body.view-notes .content-container { font-size: 1rem; } +.note-input-container.is-editing { + cursor: default; +} + +.note-input-expanded { + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px 16px 10px; + max-height: 72vh; +} + +.note-input-title { + width: 100%; + border: 0; + outline: none; + background: transparent; + color: inherit; + font-size: 1.05rem; + font-weight: 500; + padding: 0; +} + +.note-input-description-source { + width: 100%; + border: none; + background: transparent; + color: inherit; + padding: 0; + resize: none; + min-height: 120px; + max-height: 44vh; + overflow: auto; + outline: none; + display: block; + font-size: 0.95rem; + line-height: 1.5; +} + +.note-input-description-source:focus { + outline: none; +} + +[data-theme="dark"] .note-input-description-source { + border: none; +} + +.note-input-container.is-enhanced .note-input-description-source { + display: none; +} + +.note-formatting-bar { + display: none; + align-items: center; + gap: 4px; + padding: 6px 12px; + border-radius: 24px; + background: rgba(0, 0, 0, 0.04); + border: 1px solid rgba(0, 0, 0, 0.08); + width: fit-content; + max-width: 100%; + overflow-x: auto; + margin: 4px 0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); +} + +[data-theme="dark"] .note-formatting-bar { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.12); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); +} + +.note-formatting-bar.open { + display: inline-flex; + animation: fadeIn 0.15s ease; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-4px); } + to { opacity: 1; transform: translateY(0); } +} + +.note-format-btn { + appearance: none; + background: transparent; + border: none; + color: inherit; + height: 32px; + min-width: 32px; + padding: 0 8px; + border-radius: 16px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 0.85rem; + opacity: 0.85; + flex: 0 0 auto; + transition: all 0.15s ease; +} + +.note-format-btn:hover { + opacity: 1; + background: rgba(0, 0, 0, 0.08); + transform: scale(1.05); +} + +[data-theme="dark"] .note-format-btn:hover { + background: rgba(255, 255, 255, 0.14); +} + +.note-format-btn:active { + transform: scale(0.95); +} + +.note-format-sep { + width: 1px; + height: 16px; + background: rgba(0, 0, 0, 0.12); + margin: 0 4px; + flex: 0 0 auto; +} + +[data-theme="dark"] .note-format-sep { + background: rgba(255, 255, 255, 0.15); +} + +.note-input-actions-left { + display: flex; + align-items: center; + gap: 2px; + flex-wrap: wrap; +} + +.note-input-actions-left > button { + background: transparent; + border: none; + color: inherit; + opacity: 0.9; + width: 32px; + height: 32px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + padding: 0; +} + +.note-input-actions-left > button:disabled { + cursor: default; + opacity: 0.35; +} + +.note-input-actions-left > button:hover:not(:disabled) { + opacity: 1; + background: rgba(0, 0, 0, 0.08); +} + +[data-theme="dark"] .note-input-actions-left > button:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.14); +} + +.note-input-close-btn { + background: transparent; + border: none; + color: inherit; + font-weight: 600; + border-radius: 8px; + padding: 6px 10px; + cursor: pointer; +} + +.note-input-close-btn:hover { + background: rgba(136, 136, 136, 0.1); +} + +[data-theme="dark"] .note-input-close-btn:hover { + background: rgba(255, 255, 255, 0.14); +} + .note-input-placeholder { flex: 1; } @@ -329,7 +522,6 @@ body.view-notes .content-container { color: #e8eaed; } - /* --- LOGIC: Masonry vs List --- */ /* Masonry Grid */ @@ -391,17 +583,23 @@ body.view-notes .content-container { width: min(720px, 92vw); max-height: 86vh; overflow: hidden; - border-radius: 12px; + border-radius: 8px; background: var(--background-secondary, #ffffff); border: 1px solid rgba(0, 0, 0, 0.12); + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); color: inherit; display: flex; flex-direction: column; } +body.note-modal-open { + overflow: hidden; +} + [data-theme="dark"] .note-modal { background: #202124; - border-color: rgba(255, 255, 255, 0.12); + border-color: transparent; + box-shadow: 0 14px 28px rgba(0,0,0,0.5), 0 10px 10px rgba(0,0,0,0.4); } .note-modal-header { @@ -421,22 +619,47 @@ body.view-notes .content-container { font-weight: 500; } +.note-modal-title-input { + width: 100%; + border: 0; + outline: none; + background: transparent; + color: inherit; + padding: 0; +} + .note-modal-content { flex: 1; overflow-y: auto; - padding: 0 20px 18px; + padding: 0; } -.note-modal .note-body { - font-size: 1rem; - line-height: 1.75; - display: block; - -webkit-line-clamp: initial; - line-clamp: initial; - -webkit-box-orient: initial; - max-height: none; - overflow: visible; +.note-modal-description-source { + width: 100%; + border: none; + background: transparent; color: inherit; + padding: 0; + resize: none; + min-height: 200px; + max-height: 58vh; + overflow: auto; + outline: none; + display: block; + font-size: 0.95rem; + line-height: 1.5; +} + +.note-modal-description-source:focus { + outline: none; +} + +[data-theme="dark"] .note-modal-description-source { + border: none; +} + +.note-modal-overlay.is-enhanced .note-modal-description-source { + display: none; } .note-modal .note-body * { @@ -447,15 +670,10 @@ body.view-notes .content-container { display: flex; flex-wrap: wrap; gap: 8px; - padding: 10px 20px 12px; - border-top: 1px solid rgba(0, 0, 0, 0.12); + padding: 0 20px 12px; flex-shrink: 0; } -[data-theme="dark"] .note-modal-tags { - border-top-color: rgba(255, 255, 255, 0.12); -} - .note-modal-tags.is-empty { display: none; } @@ -501,16 +719,14 @@ body.view-notes .content-container { align-items: center; justify-content: space-between; gap: 6px; - padding: 8px 12px; - border-top: 1px solid rgba(0, 0, 0, 0.12); - background: rgba(0, 0, 0, 0.03); + padding: 8px 16px 16px; + background: transparent; flex-shrink: 0; flex-wrap: wrap; } [data-theme="dark"] .note-modal-actions { - border-top-color: rgba(255, 255, 255, 0.12); - background: rgba(255, 255, 255, 0.06); + background: transparent; } .note-modal-actions-left { @@ -597,28 +813,31 @@ body.view-notes .content-container { /* --- CARD STYLING --- */ .note-card { background-color: var(--background-secondary, #ffffff); - border: 1px solid #e0e0e0; + border: 1px solid rgba(0, 0, 0, 0.12); border-radius: 8px; margin-bottom: 16px; break-inside: avoid; position: relative; - transition: box-shadow 0.2s, transform 0.2s, background-color 0.2s; + transition: box-shadow 0.2s cubic-bezier(0.4, 0.0, 0.2, 1), transform 0.2s cubic-bezier(0.4, 0.0, 0.2, 1), border-color 0.2s cubic-bezier(0.4, 0.0, 0.2, 1); overflow: visible; color: var(--note-card-fg, #202124); } [data-theme="dark"] .note-card { background-color: #202124; - border: 1px solid #5f6368; + border-color: #5f6368; color: #e8eaed; } .note-card:hover { box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15); + border-color: transparent; } [data-theme="dark"] .note-card:hover { background-color: #202124; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.6), 0 1px 3px 1px rgba(0, 0, 0, 0.4); + border-color: transparent; } /* Cover Image */ @@ -687,17 +906,17 @@ body.view-notes .content-container { display: inline-flex; align-items: center; gap: 0.25rem; - background: var(--tag-bg); + background: var(--tag-bg, #f1f3f4); padding: 0.25rem 0.5rem; border-radius: 999px; font-size: 0.75rem; font-weight: 500; - color: var(--tag-text); + color: var(--tag-text, #3c4043); } [data-theme="dark"] .note-tag { - background: var(--tag-bg); - color: var(--tag-text); + background: var(--tag-bg, #3c4043); + color: var(--tag-text, #e8eaed); } .note-tag-text { @@ -738,18 +957,15 @@ body.view-notes .content-container { .note-hover-actions { display: flex; align-items: center; - gap: 0px; - /* evenly spaced */ - margin-top: 8px; - margin-left: -8px; - /* Alignment fix */ + gap: 2px; + margin-top: 12px; + margin-left: -6px; color: var(--text-light, #5f6368); opacity: 0; - /* Hidden by default */ - transition: opacity 0.2s; + transition: opacity 0.2s cubic-bezier(0.4, 0.0, 0.2, 1); position: relative; - /* For palette popup */ z-index: 2; + min-height: 34px; } [data-theme="dark"] .note-hover-actions { @@ -771,27 +987,35 @@ body.view-notes .content-container { .note-hover-actions > button, .note-hover-actions > a, .note-hover-actions > div > button { - background: none; + background: transparent; border: none; - width: 34px; - /* Touch target */ - height: 34px; + width: 32px; + height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: inherit; cursor: pointer; - font-size: 1.1rem; - transition: background-color 0.2s, color 0.2s; + font-size: 1.15rem; + transition: background-color 0.15s, color 0.15s, opacity 0.15s; text-decoration: none; + opacity: 0.7; } .note-hover-actions > button:hover, .note-hover-actions > a:hover, .note-hover-actions > div > button:hover { - background-color: rgba(136, 136, 136, 0.2); - color: var(--text-color); + background-color: rgba(0, 0, 0, 0.08); + color: var(--text-color, #202124); + opacity: 1; +} + +[data-theme="dark"] .note-hover-actions > button:hover, +[data-theme="dark"] .note-hover-actions > a:hover, +[data-theme="dark"] .note-hover-actions > div > button:hover { + background-color: rgba(255, 255, 255, 0.14); + color: #e8eaed; } .bookmark-palette { @@ -1480,22 +1704,25 @@ body.view-notes .content-container { /* Grey */ .note-card.note-color-grey { - background-color: #e8eaed; - border-color: transparent; - --note-card-fg: #2a2d31; +background-color: #e8eaed; +border-color: transparent; +--note-card-fg: #2a2d31; } .note-card.note-has-bg, .note-modal.note-has-bg, +.note-input-container.note-has-bg, .link-outer.note-has-bg { - background-image: linear-gradient(rgba(0, 0, 0, 0.16), rgba(0, 0, 0, 0.16)), var(--note-bg-image); - background-size: cover; - background-position: center bottom; +background-image: linear-gradient(rgba(0, 0, 0, 0.16), rgba(0, 0, 0, 0.16)), var(--note-bg-image); +background-size: cover; +background-position: center bottom; } .note-card.todo-card.note-has-bg[data-font-color="auto"] { - --note-card-fg: rgba(255, 255, 255, 0.92); - color: var(--note-card-fg); +--note-card-fg: rgba(255, 255, 255, 0.92); +color: var(--note-card-fg); +background-image: linear-gradient(rgba(0, 0, 0, 0.42), rgba(0, 0, 0, 0.42)), var(--note-bg-image); +text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55); background-image: linear-gradient(rgba(0, 0, 0, 0.42), rgba(0, 0, 0, 0.42)), var(--note-bg-image); text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55); } diff --git a/shaarli-pro/includes.html b/shaarli-pro/includes.html index 7ee90a8..3d6e235 100644 --- a/shaarli-pro/includes.html +++ b/shaarli-pro/includes.html @@ -24,13 +24,13 @@ - + {if="$pageName=='editlink' || $pageName=='addlink' || $pageName=='editlinkbatch'"} {/if} -{if="$pageName=='editlink' || $pageName=='editlinkbatch'"} +{if="$pageName=='editlink' || $pageName=='editlinkbatch' || ($pageName=='linklist' && isset($search_tags) && preg_match('/(^|[\s,])note([\s,]|$)/i', (string) $search_tags))"} @@ -61,14 +61,14 @@ var shaarli = { basePath: '{$base_path}', rootPath: '{$root_path}', assetPath: '{$base_path}{$asset_path}', -isAuth: {if="$is_logged_in"}true{else}false{/if}, +isAuth: (function(){/*{if="$is_logged_in"}*/return true;/*{else}*/return false;/*{/if}*/})(), pageName: '{$pageName}', visibility: '{$visibility}', -untaggedonly: {if="$untaggedonly"}true{else}false{/if} +untaggedonly: (function(){/*{if="$untaggedonly"}*/return true;/*{else}*/return false;/*{/if}*/})() }; - + {if="file_exists('tpl/shaarli-pro/extra.html')"} {include="extra"} diff --git a/shaarli-pro/js/custom_views.js b/shaarli-pro/js/custom_views.js index 1a04f64..5e56696 100644 --- a/shaarli-pro/js/custom_views.js +++ b/shaarli-pro/js/custom_views.js @@ -1,11 +1,38 @@ document.addEventListener("DOMContentLoaded", function () { // Check URL parameters for custom views const urlParams = new URLSearchParams(window.location.search); - const searchTags = urlParams.get("searchtags"); + const searchTagsRaw = urlParams.get("searchtags") || urlParams.get("searchTags") || ""; + + // Also check URL path for tag format (e.g., /tag/note) + const urlPath = window.location.pathname; + const pathMatch = urlPath.match(/\/tag\/(.+)$/); + const pathTagRaw = pathMatch ? decodeURIComponent(pathMatch[1]) : ""; + + // Parse all active tags to safely detect the view + const activeTags = (searchTagsRaw + " " + pathTagRaw).toLowerCase().split(/[\s,]+/).filter(t => t); + + // Foolproof detection using sidebar active state and DOM rendered tags + const hasNoteActiveMenu = !!document.querySelector('.sidebar-link[aria-label="Notes"].active, .header-nav-link[aria-label="Notes"].active, .sidebar-link[href*="searchtags=note"].active'); + const hasTodoActiveMenu = !!document.querySelector('.sidebar-link[aria-label="Mes tâches"].active, .header-nav-link[aria-label="Mes tâches"].active, .sidebar-link[href*="searchtags=shaarli-todo"].active'); + const hasArchiveActiveMenu = !!document.querySelector('.sidebar-link[aria-label="Archive"].active, .header-nav-link[aria-label="Archive"].active, .sidebar-link[href*="searchtags=shaarli-archive"].active'); + + const domChipTags = Array.from(document.querySelectorAll('.search-tag-chip')).map(el => (el.textContent || "").trim().toLowerCase()); + + const isNoteView = activeTags.includes("note") || hasNoteActiveMenu || domChipTags.includes("note"); + const isTodoView = activeTags.includes("shaarli-todo") || hasTodoActiveMenu || domChipTags.includes("shaarli-todo"); + const isArchiveView = activeTags.includes("shaarli-archive") || hasArchiveActiveMenu || domChipTags.includes("shaarli-archive"); const linkList = document.getElementById("links-list"); const container = document.querySelector(".content-container"); + const showErrorBanner = (msg, err) => { + const banner = document.createElement("div"); + banner.style.cssText = "background: #ffebee; color: #c62828; padding: 16px; margin: 16px; border-radius: 8px; border: 1px solid #ef9a9a; z-index: 9999; position: relative;"; + banner.innerHTML = `Erreur Custom Views: ${msg}
${err ? err.stack || err : ''}
`; + if (container) container.prepend(banner); + else document.body.prepend(banner); + }; + const restoreDefaultListView = () => { try { document.body.classList.remove("view-todo", "view-notes", "view-archive"); @@ -22,6 +49,102 @@ document.addEventListener("DOMContentLoaded", function () { document.querySelectorAll(".notes-wrapper.todo-wrapper").forEach((el) => el.remove()); }; + // Fonction de rendu Markdown basique pour l'affichage des notes + function renderMarkdown(markdown) { + if (!markdown) return ""; + let html = markdown + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/^### (.*$)/gim, "

$1

") + .replace(/^## (.*$)/gim, "

$1

") + .replace(/^# (.*$)/gim, "

$1

") + .replace(/\*\*(.*?)\*\*/gim, "$1") + .replace(/\*(.*?)\*/gim, "$1") + .replace(/(.*?)<\/u>/gim, "$1") + .replace(/\n/g, "
"); + return html; + } + + function applyKeepNoteFormatting(textarea, format) { + if (!textarea || !format) return; + const start = textarea.selectionStart || 0; + const end = textarea.selectionEnd || 0; + const value = String(textarea.value || ""); + const selected = value.slice(start, end); + + const replaceRange = (from, to, replacement) => { + const next = value.slice(0, from) + replacement + value.slice(to); + textarea.value = next; + const cursor = from + replacement.length; + textarea.focus({ preventScroll: true }); + textarea.setSelectionRange(cursor, cursor); + }; + + const wrapSelection = (prefix, suffix) => { + const body = selected || ""; + const replacement = `${prefix}${body}${suffix}`; + const next = value.slice(0, start) + replacement + value.slice(end); + textarea.value = next; + const selStart = start + prefix.length; + const selEnd = selStart + body.length; + textarea.focus({ preventScroll: true }); + textarea.setSelectionRange(selStart, selEnd); + }; + + const lineRangeForSelection = () => { + const before = value.slice(0, start); + const after = value.slice(end); + const lineStart = before.lastIndexOf("\n") + 1; + const nextNl = after.indexOf("\n"); + const lineEnd = nextNl === -1 ? value.length : end + nextNl; + return { lineStart, lineEnd }; + }; + + if (format === "bold") return wrapSelection("**", "**"); + if (format === "italic") return wrapSelection("*", "*"); + if (format === "underline") return wrapSelection("", ""); + + if (format === "h1" || format === "h2" || format === "p") { + const { lineStart, lineEnd } = lineRangeForSelection(); + const block = value.slice(lineStart, lineEnd); + const lines = block.split(/\r?\n/); + const prefix = format === "h1" ? "# " : format === "h2" ? "## " : ""; + const nextLines = lines.map((ln) => { + const stripped = ln.replace(/^\s*(#{1,6}\s+)/, ""); + return prefix ? prefix + stripped : stripped; + }); + const replacement = nextLines.join("\n"); + const next = value.slice(0, lineStart) + replacement + value.slice(lineEnd); + textarea.value = next; + textarea.focus({ preventScroll: true }); + textarea.setSelectionRange(lineStart, lineStart + replacement.length); + return; + } + + if (format === "clear") { + if (!selected) { + const { lineStart, lineEnd } = lineRangeForSelection(); + const block = value.slice(lineStart, lineEnd); + const cleaned = block + .replace(/^\s*(#{1,6}\s+)/gm, "") + .replace(/\*\*(.*?)\*\*/g, "$1") + .replace(/\*(.*?)\*/g, "$1") + .replace(/(.*?)<\/u>/g, "$1"); + const next = value.slice(0, lineStart) + cleaned + value.slice(lineEnd); + textarea.value = next; + textarea.focus({ preventScroll: true }); + textarea.setSelectionRange(lineStart, lineStart + cleaned.length); + return; + } + const cleaned = selected + .replace(/\*\*(.*?)\*\*/g, "$1") + .replace(/\*(.*?)\*/g, "$1") + .replace(/(.*?)<\/u>/g, "$1"); + replaceRange(start, end, cleaned); + } + } + const startViewInitialization = function () { if (typeof initBookmarkPaletteButtons === "function") { initBookmarkPaletteButtons(); @@ -39,27 +162,40 @@ document.addEventListener("DOMContentLoaded", function () { if (!linkList || !container) return; - if (searchTags === "shaarli-todo") { + if (isTodoView) { try { + console.log("[custom_views] Initializing Todo view"); initTodoView(linkList, container); } catch (err) { - console.error("Erreur lors de l'initialisation de la vue Todo:", err); + console.error("[custom_views] Erreur lors de l'initialisation de la vue Todo:", err); restoreDefaultListView(); } - } else if (searchTags === "note") { - // Pour la vue notes, parser les notes AVANT de supprimer les tags techniques - // afin que les propriétés visuelles (couleur, fond, etc.) soient correctement extraites - initNoteView(linkList, container); - // Puis supprimer les tags techniques de l'affichage - if (typeof initTagDisplayAndRemoval === "function") { - initTagDisplayAndRemoval(); + } else if (isNoteView) { + try { + console.log("[custom_views] Initializing Note view"); + // Pour la vue notes, parser les notes AVANT de supprimer les tags techniques + // afin que les propriétés visuelles (couleur, fond, etc.) soient correctement extraites + initNoteView(linkList, container); + // Puis supprimer les tags techniques de l'affichage + if (typeof initTagDisplayAndRemoval === "function") { + initTagDisplayAndRemoval(); + } + } catch (err) { + console.error("[custom_views] Erreur lors de l'initialisation de la vue Note:", err); + restoreDefaultListView(); } - } else if (searchTags === "shaarli-archive") { - // Vue Archive - similaire à Notes mais pour les notes archivées - initArchiveView(linkList, container); - // Puis supprimer les tags techniques de l'affichage - if (typeof initTagDisplayAndRemoval === "function") { - initTagDisplayAndRemoval(); + } else if (isArchiveView) { + try { + console.log("[custom_views] Initializing Archive view"); + // Vue Archive - similaire à Notes mais pour les notes archivées + initArchiveView(linkList, container); + // Puis supprimer les tags techniques de l'affichage + if (typeof initTagDisplayAndRemoval === "function") { + initTagDisplayAndRemoval(); + } + } catch (err) { + console.error("[custom_views] Erreur lors de l'initialisation de la vue Archive:", err); + restoreDefaultListView(); } } else { // Vue standard : supprimer les tags techniques @@ -912,6 +1048,12 @@ function ensureBackgroundStudioPanel() { const entityId = panelEl.dataset.entityId || ""; const editUrl = panelEl.dataset.editUrl || ""; + const applyDraft = (next) => { + if (typeof panelEl.__draftApply === "function") { + panelEl.__draftApply(next); + } + }; + const action = actionBtn.dataset.bgStudioAction; if (action === "close") { closeBackgroundStudioPanel(); @@ -920,22 +1062,44 @@ function ensureBackgroundStudioPanel() { if (action === "set-color") { const key = actionBtn.dataset.colorKey || "default"; - if (mode === "modal") setModalNoteColor(key); - else setNoteColor(entityId, key, editUrl); + if (mode === "draft") { + panelEl.dataset.color = key; + applyDraft({ color: key }); + renderBackgroundStudioPanel(panelEl); + } else 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); + if (mode === "draft") { + panelEl.dataset.background = key; + applyDraft({ background: key }); + renderBackgroundStudioPanel(panelEl); + } else 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); + if (mode === "draft") { + const normalized = normalizeFilterKey(filterKey || "") || "none"; + panelEl.dataset.filter = normalized; + applyDraft({ filter: normalized }); + renderBackgroundStudioPanel(panelEl); + } else if (mode === "modal") { + setModalNoteFilter(filterKey); + } else { + setNoteFilter(entityId, filterKey, editUrl); + } return; } @@ -948,7 +1112,14 @@ function ensureBackgroundStudioPanel() { } if (action === "set-defaults") { - if (mode === "modal") { + if (mode === "draft") { + panelEl.dataset.color = "default"; + panelEl.dataset.filter = "none"; + panelEl.dataset.background = "none"; + panelEl.dataset.fontColor = "auto"; + applyDraft({ color: "default", filter: "none", background: "none", fontColor: "auto" }); + renderBackgroundStudioPanel(panelEl); + } else if (mode === "modal") { setModalNoteColor("default"); setModalNoteFilter("none"); } else { @@ -959,7 +1130,11 @@ function ensureBackgroundStudioPanel() { } if (action === "reset-font-color") { - if (mode === "modal") { + if (mode === "draft") { + panelEl.dataset.fontColor = "auto"; + applyDraft({ fontColor: "auto" }); + renderBackgroundStudioPanel(panelEl); + } else if (mode === "modal") { setModalNoteFontColor("auto"); } else { setNoteFontColor(entityId, "auto", editUrl); @@ -979,8 +1154,15 @@ function ensureBackgroundStudioPanel() { if (action === "set-font-color") { const fontColorKey = actionBtn.dataset.fontColorKey || "auto"; - if (mode === "modal") setModalNoteFontColor(fontColorKey); - else setNoteFontColor(entityId, fontColorKey, editUrl); + if (mode === "draft") { + panelEl.dataset.fontColor = fontColorKey; + applyDraft({ fontColor: fontColorKey }); + renderBackgroundStudioPanel(panelEl); + } else if (mode === "modal") { + setModalNoteFontColor(fontColorKey); + } else { + setNoteFontColor(entityId, fontColorKey, editUrl); + } return; } }); @@ -1347,7 +1529,7 @@ function applyNoteVisualState(element, note) { const background = normalizedBackground || "none"; const fontColor = note.fontColor || "auto"; - element.classList.forEach((cls) => { + Array.from(element.classList).forEach((cls) => { if (cls.startsWith("note-color-")) element.classList.remove(cls); if (cls.startsWith("note-filter-")) element.classList.remove(cls); }); @@ -1879,6 +2061,161 @@ function getFormFieldValue(input) { return input.value; } +async function fetchShaareFormBaseData(url) { + const response = await fetch(url, { credentials: "same-origin" }); + const html = await response.text(); + 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 baseData = new URLSearchParams(); + const inputs = form.querySelectorAll("input, textarea"); + inputs.forEach((input) => { + if (input.type === "checkbox") { + if (input.checked) baseData.append(input.name, input.value || "on"); + } else if (input.name) { + baseData.append(input.name, getFormFieldValue(input)); + } + }); + + return { action: form.action, baseData }; +} + +async function createNewNoteViaForm({ title, markdown, visual = null }) { + const basePath = typeof shaarli !== "undefined" && shaarli.basePath ? shaarli.basePath : ""; + const formUrl = `${basePath}/admin/shaare?post=&tags=note`; + const { action, baseData } = await fetchShaareFormBaseData(formUrl); + + const formData = new URLSearchParams(baseData.toString()); + formData.set("lf_url", ""); + formData.set("lf_title", title || ""); + formData.set("lf_description", markdown || ""); + + const currentTags = (formData.get("lf_tags") || "").trim(); + let tagsArray = currentTags.split(/[\s,]+/).filter((t) => t.trim() !== ""); + + // Ensure note tag exists + if (!tagsArray.includes("note")) tagsArray.push("note"); + + // Apply optional visual tags (note-color-*, notefilter-*, notebg-*, font-*) + if (visual) { + const colorKey = visual.color || "default"; + const filterKey = normalizeFilterKey(visual.filter || "") || "none"; + const bgKey = normalizeBackgroundKey(visual.background || "") || "none"; + const fontColorKey = visual.fontColor || "auto"; + + tagsArray = tagsArray.filter((t) => !t.startsWith(NOTE_COLOR_TAG_PREFIX)); + tagsArray = tagsArray.filter((t) => !t.startsWith(NOTE_FILTER_TAG_PREFIX)); + tagsArray = tagsArray.filter((t) => !t.startsWith(NOTE_BACKGROUND_TAG_PREFIX)); + tagsArray = tagsArray.filter((t) => !t.startsWith(NOTE_FONT_COLOR_TAG_PREFIX)); + + if (colorKey && colorKey !== "default") { + if (typeof colorKey === "string" && colorKey.startsWith("custom:")) { + const clean = colorKey.substring(7).replace("#", ""); + if (clean) tagsArray.push(`${NOTE_COLOR_TAG_PREFIX}${clean}`); + } else { + tagsArray.push(`${NOTE_COLOR_TAG_PREFIX}${colorKey}`); + } + } + + if (filterKey && filterKey !== "none") { + tagsArray.push(`${NOTE_FILTER_TAG_PREFIX}${filterKey}`); + } + + if (bgKey && bgKey !== "none") { + tagsArray.push(`${NOTE_BACKGROUND_TAG_PREFIX}${bgKey}`); + } + + if (fontColorKey && fontColorKey !== "auto") { + if (typeof fontColorKey === "string" && fontColorKey.startsWith("custom:")) { + const clean = fontColorKey.substring(7).replace("#", ""); + if (clean) tagsArray.push(`${NOTE_FONT_COLOR_TAG_PREFIX}${clean}`); + } else { + const option = NOTE_FONT_COLOR_OPTIONS.find((opt) => opt.key === fontColorKey); + if (option && option.value && option.value !== "auto") { + tagsArray.push(`${NOTE_FONT_COLOR_TAG_PREFIX}${option.value.substring(1)}`); + } + } + } + } + + formData.set("lf_tags", tagsArray.join(" ")); + + if (!formData.get("returnurl")) { + formData.set("returnurl", window.location.href); + } + formData.append("save_edit", "1"); + + const response = await fetch(action, { + method: "POST", + credentials: "same-origin", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: formData.toString(), + }); + + if (!response.ok) throw new Error(`Failed to create note (status ${response.status})`); +} + +async function hydrateNoteFromEditForm(note) { + if (!note || !note.editUrl || note.editUrl === "#") return; + if (note._noteHydrated) return; + if (note._noteHydratePromise) return note._noteHydratePromise; + + note._noteHydratePromise = (async () => { + try { + const { action, baseData } = await fetchShaareFormBaseData(note.editUrl); + note._noteEditAction = action; + note._noteEditBaseData = baseData; + note._noteMarkdown = baseData.get("lf_description") || ""; + note._noteTitle = baseData.get("lf_title") || ""; + note._noteHydrated = true; + } catch (e) { + console.error("Error hydrating note edit form:", e); + } finally { + note._noteHydratePromise = null; + } + })(); + + return note._noteHydratePromise; +} + +async function persistNoteChanges(note, { title, markdown }, retryCount = 0) { + if (!note || !note.editUrl || note.editUrl === "#") return; + + const refreshEditForm = async () => { + const { action, baseData } = await fetchShaareFormBaseData(note.editUrl); + note._noteEditAction = action; + note._noteEditBaseData = baseData; + }; + + if (!note._noteEditAction || !note._noteEditBaseData || !(note._noteEditBaseData.get && note._noteEditBaseData.get("token"))) { + await refreshEditForm(); + } + + const formData = new URLSearchParams(note._noteEditBaseData.toString()); + formData.set("lf_title", title || ""); + formData.set("lf_description", markdown || ""); + formData.append("save_edit", "1"); + + const response = await fetch(note._noteEditAction, { + method: "POST", + credentials: "same-origin", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: formData.toString(), + }); + + if (!response.ok) { + if (response.status === 403 && retryCount < 1) { + note._noteEditAction = ""; + note._noteEditBaseData = null; + await refreshEditForm(); + return persistNoteChanges(note, { title, markdown }, retryCount + 1); + } + throw new Error("Failed to save note"); + } +} + async function hydrateTodoFromEditForm(todo) { if (!todo || !todo.editUrl || todo.editUrl === "#") return; if (todo._todoHydrated) return; @@ -2471,6 +2808,8 @@ async function persistTodoChanges(todo, retryCount = 0) { function initNoteView(linkList, container) { document.body.classList.add("view-notes"); + const basePath = typeof shaarli !== "undefined" && shaarli.basePath ? shaarli.basePath : ""; + // Hide standard toolbar const toolbar = document.querySelector(".content-toolbar"); if (toolbar) toolbar.style.display = "none"; @@ -2483,18 +2822,215 @@ function initNoteView(linkList, container) { const topBar = document.createElement("div"); topBar.className = "notes-top-bar"; + const topBarInner = document.createElement("div"); + topBarInner.className = "notes-top-bar-inner"; + // Custom Input "Take a note..." const inputContainer = document.createElement("div"); inputContainer.className = "note-input-container"; - inputContainer.innerHTML = ` -
+ const renderNoteInputCollapsed = () => { + inputContainer.classList.remove("is-editing", "is-enhanced"); + inputContainer.innerHTML = ` +
Créer une note...
- +
`; - topBar.appendChild(inputContainer); + + const collapsed = inputContainer.querySelector(".note-input-collapsed"); + const btn = inputContainer.querySelector(".note-input-actions button"); + const openEditor = () => { + inputContainer.classList.add("is-editing"); + inputContainer.innerHTML = ` +
+ + + +
+
+ + + + + + + +
+ +
+
+ `; + + const titleInput = inputContainer.querySelector(".note-input-title"); + const source = inputContainer.querySelector(".note-input-description-source"); + const closeBtn = inputContainer.querySelector(".note-input-close-btn"); + const paletteBtn = inputContainer.querySelector(".note-input-palette-btn"); + const formatToggleBtn = inputContainer.querySelector(".note-input-format-btn"); + const formattingBar = inputContainer.querySelector(".note-formatting-bar"); + + let hasChanges = false; + const initialTitle = ""; + const initialMarkdown = ""; + let isSaving = false; + + let draftVisual = { color: "default", filter: "none", background: "none", fontColor: "auto" }; + applyNoteVisualState(inputContainer, draftVisual); + + const setFormattingVisible = (visible) => { + if (!formattingBar) return; + if (visible) { + formattingBar.classList.add("open"); + formattingBar.setAttribute("aria-hidden", "false"); + inputContainer.classList.add("show-formatting"); + } else { + formattingBar.classList.remove("open"); + formattingBar.setAttribute("aria-hidden", "true"); + inputContainer.classList.remove("show-formatting"); + } + }; + + formatToggleBtn?.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + const isOpen = formattingBar && formattingBar.classList.contains("open"); + setFormattingVisible(!isOpen); + }); + + inputContainer.querySelectorAll(".note-format-btn").forEach((btnEl) => { + btnEl.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + if (!source) return; + applyKeepNoteFormatting(source, btnEl.dataset.noteFormat || ""); + hasChanges = true; + }); + }); + + source?.addEventListener("input", () => { + hasChanges = true; + }); + + const getValues = () => { + const t = titleInput && typeof titleInput.value === "string" ? titleInput.value.trim() : ""; + const md = source && typeof source.value === "string" ? source.value.trim() : ""; + return { title: t, markdown: md }; + }; + + const saveIfNeededAndClose = async () => { + if (isSaving) return; + const { title, markdown } = getValues(); + if (!title && !markdown) { + document.removeEventListener("pointerdown", onOutsidePointerDown, true); + renderNoteInputCollapsed(); + return; + } + + const changed = hasChanges || title !== initialTitle || markdown !== initialMarkdown; + if (!changed) { + document.removeEventListener("pointerdown", onOutsidePointerDown, true); + renderNoteInputCollapsed(); + return; + } + isSaving = true; + try { + await createNewNoteViaForm({ title, markdown, visual: draftVisual }); + window.location.reload(); + } catch (e) { + console.error("Error creating note:", e); + alert("Erreur lors de la création de la note."); + isSaving = false; + } + }; + + // Attacher les listeners de formatage immédiatement + formatToggleBtn?.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + const isOpen = formattingBar && formattingBar.classList.contains("open"); + setFormattingVisible(!isOpen); + }); + + inputContainer.querySelectorAll(".note-format-btn").forEach((btnEl) => { + btnEl.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + if (!source) return; + applyKeepNoteFormatting(source, btnEl.dataset.noteFormat || ""); + hasChanges = true; + }); + }); + + titleInput?.addEventListener("input", () => { + hasChanges = true; + }); + + closeBtn?.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + saveIfNeededAndClose(); + }); + + const onOutsidePointerDown = (e) => { + if (e.target && e.target.closest && e.target.closest("#shaarli-bg-studio")) return; + if (e.target && e.target.closest && e.target.closest(".bg-studio-panel")) return; + if (e.target && e.target.closest && e.target.closest(".note-formatting-bar")) return; + if (inputContainer.contains(e.target)) return; + saveIfNeededAndClose(); + }; + document.addEventListener("pointerdown", onOutsidePointerDown, true); + + paletteBtn?.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + openBackgroundStudioPanel({ + anchorEl: paletteBtn, + mode: "draft", + entityId: "draft-new-note", + editUrl: "", + currentColor: draftVisual.color || "default", + currentFilter: draftVisual.filter || "none", + currentBackground: draftVisual.background || "none", + currentFontColor: draftVisual.fontColor || "auto", + title: "Mes images & couleurs", + }); + + const panel = document.getElementById("shaarli-bg-studio"); + if (panel) { + panel.__draftApply = (next) => { + draftVisual = { ...draftVisual, ...(next || {}) }; + applyNoteVisualState(inputContainer, draftVisual); + }; + } + }); + + titleInput?.focus({ preventScroll: true }); + }; + + collapsed?.addEventListener("click", (e) => { + if (e.target.closest("button")) return; + openEditor(); + }); + btn?.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + openEditor(); + }); + }; + + renderNoteInputCollapsed(); + topBarInner.appendChild(inputContainer); + topBar.appendChild(topBarInner); // View Toggle and other tools const tools = document.createElement("div"); @@ -2546,14 +3082,26 @@ function initNoteView(linkList, container) { const modalOverlay = document.createElement("div"); modalOverlay.className = "note-modal-overlay"; modalOverlay.innerHTML = ` -
+