diff --git a/shaarli-pro/css/custom_views.css b/shaarli-pro/css/custom_views.css index 4068ef6..d19391c 100644 --- a/shaarli-pro/css/custom_views.css +++ b/shaarli-pro/css/custom_views.css @@ -13,14 +13,18 @@ /* --- TODO VIEW --- */ body.view-todo .content-container { - max-width: 100%; - padding: 0; - margin: 0; + padding: 2rem; + background-color: var(--bg-body); + min-height: 100vh; +} + +[data-theme="dark"] body.view-todo .content-container { + background-color: var(--bg-body); } body.view-todo #linklist { - display: none; - /* Hide default list when wrapper is active */ + display: block; + /* Show default list when wrapper is not active */ } /* Sidebar */ @@ -636,6 +640,8 @@ body.view-notes .content-container { display: flex; flex-direction: column; gap: 8px; + position: relative; + z-index: 2; } /* Title */ @@ -1487,6 +1493,13 @@ body.view-notes .content-container { 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); + 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); +} + .note-card .note-title, .note-card .note-body, .note-card .note-tag, @@ -2179,4 +2192,226 @@ body.view-archive .content-container { right: auto; top: auto; transform: none; +} + +body.view-todo .note-card.todo-card .note-body { + padding-top: 0.25rem; +} + +body.view-todo .note-card.todo-card .todo-checklist-preview-wrap { + display: block; + -webkit-line-clamp: initial; + line-clamp: initial; + -webkit-box-orient: initial; + overflow: visible; + max-height: none; +} + +.todo-checklist-preview { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.todo-checklist-preview-item { + display: flex; + align-items: center; + gap: 0.6rem; + font-size: 0.95rem; + line-height: 1.35; +} + +.todo-checklist-preview-box { + width: 16px; + height: 16px; + border-radius: 4px; + border: 2px solid currentColor; + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + opacity: 0.7; +} + +.todo-checklist-preview-box i { + font-size: 14px; + opacity: 0; +} + +.todo-checklist-preview-item.is-checked { + opacity: 0.7; +} + +.todo-checklist-preview-item.is-checked .todo-checklist-preview-box i { + opacity: 1; +} + +.todo-checklist-preview-item.is-checked .todo-checklist-preview-text { + text-decoration: line-through; +} + +.todo-checklist-preview-more { + opacity: 0.7; +} + +.todo-modal .note-modal-content { + padding-top: 6px; +} + +.todo-checklist { + display: flex; + flex-direction: column; + gap: 6px; + padding-bottom: 6px; +} + +.todo-checklist-row { + display: grid; + grid-template-columns: 26px 26px minmax(0, 1fr) 34px; + align-items: center; + gap: 8px; + padding: 6px 2px; + border-radius: 8px; +} + +.todo-checklist-row.is-checked { + opacity: 0.75; +} + +.todo-checklist-row.is-dragging { + opacity: 0.6; +} + +.todo-drag-handle { + width: 26px; + height: 26px; + border: none; + background: none; + color: inherit; + opacity: 0.7; + cursor: grab; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; +} + +.todo-drag-handle:active { + cursor: grabbing; +} + +.todo-drag-handle i { + font-size: 18px; +} + +.todo-checklist-box { + width: 26px; + height: 26px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.todo-checklist-box input { + position: absolute; + opacity: 0; + pointer-events: none; +} + +.todo-checklist-box-ui { + width: 16px; + height: 16px; + border-radius: 4px; + border: 2px solid currentColor; + display: inline-block; + opacity: 0.75; +} + +.todo-item-checkbox:checked + .todo-checklist-box-ui { + background: currentColor; + box-shadow: inset 0 0 0 3px rgba(0, 0, 0, 0.22); +} + +[data-theme="dark"] .todo-item-checkbox:checked + .todo-checklist-box-ui { + box-shadow: inset 0 0 0 3px rgba(0, 0, 0, 0.35); +} + +.todo-item-text { + width: 100%; + border: none; + background: transparent; + color: inherit; + font-size: 1rem; + line-height: 1.4; + padding: 6px 6px; + border-radius: 6px; + outline: none; + min-width: 0; +} + +.todo-checklist-row.is-checked .todo-item-text { + text-decoration: line-through; +} + +.todo-item-text:focus { + background: rgba(0, 0, 0, 0.06); +} + +[data-theme="dark"] .todo-item-text:focus { + background: rgba(255, 255, 255, 0.08); +} + +.todo-item-delete { + width: 34px; + height: 34px; + border: none; + background: none; + color: inherit; + opacity: 0.7; + cursor: pointer; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; +} + +.todo-item-delete:hover { + opacity: 1; + background: rgba(0, 0, 0, 0.08); +} + +[data-theme="dark"] .todo-item-delete:hover { + background: rgba(255, 255, 255, 0.12); +} + +.todo-item-delete i { + font-size: 20px; +} + +.todo-add-item-btn { + border: none; + background: none; + color: inherit; + opacity: 0.8; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 10px; + padding: 8px 0; + font-size: 1rem; + justify-content: flex-start; + width: 100%; +} + +.todo-add-item-btn i { + font-size: 18px; +} + +.todo-add-item-btn:hover { + opacity: 1; } \ No newline at end of file diff --git a/shaarli-pro/js/custom_views.js b/shaarli-pro/js/custom_views.js index 75c2f03..1a04f64 100644 --- a/shaarli-pro/js/custom_views.js +++ b/shaarli-pro/js/custom_views.js @@ -6,6 +6,22 @@ document.addEventListener("DOMContentLoaded", function () { const linkList = document.getElementById("links-list"); const container = document.querySelector(".content-container"); + const restoreDefaultListView = () => { + try { + document.body.classList.remove("view-todo", "view-notes", "view-archive"); + } catch (e) { + // noop + } + + const toolbar = document.querySelector(".content-toolbar"); + if (toolbar) toolbar.style.display = ""; + + const list = document.getElementById("links-list"); + if (list) list.style.display = ""; + + document.querySelectorAll(".notes-wrapper.todo-wrapper").forEach((el) => el.remove()); + }; + const startViewInitialization = function () { if (typeof initBookmarkPaletteButtons === "function") { initBookmarkPaletteButtons(); @@ -24,7 +40,12 @@ document.addEventListener("DOMContentLoaded", function () { if (!linkList || !container) return; if (searchTags === "shaarli-todo") { - initTodoView(linkList, container); + try { + initTodoView(linkList, container); + } catch (err) { + console.error("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 @@ -179,6 +200,7 @@ function isTechnicalTag(tag) { if (!t) return false; if (t === "note") return true; + if (t === "shaarli-todo") return true; if (t === "shaarli-pin") return true; if (t.startsWith(NOTE_FONT_COLOR_TAG_PREFIX)) return true; if (t.startsWith(NOTE_COLOR_TAG_PREFIX)) return true; @@ -237,7 +259,7 @@ function removeTagFromEntity(editUrl, tag) { if (input.type === "checkbox") { if (input.checked) formData.append(input.name, input.value || "on"); } else if (input.name) { - formData.append(input.name, input.value); + formData.append(input.name, getFormFieldValue(input)); } }); @@ -317,11 +339,12 @@ function initTagDisplayAndRemoval() { tags = tags.filter((t) => t !== tag); card.dataset.tags = tags.join("||"); - const modal = document.querySelector(".note-modal-overlay"); - if (modal && modal.currentNote && String(modal.currentNote.id) === String(card.dataset.id)) { - modal.currentNote.tags = (modal.currentNote.tags || []).filter((t) => t !== tag); - const tagsContainer = modal.querySelector("#note-modal-tags"); - renderModalTags(tagsContainer, modal.currentNote.tags); + const modal = getOpenModalOverlay(); + const entity = getModalCurrentEntity(modal); + if (modal && entity && String(entity.id) === String(card.dataset.id)) { + entity.tags = (entity.tags || []).filter((t) => t !== tag); + const tagsContainer = modal.querySelector(".note-modal-tags"); + renderModalTags(tagsContainer, entity.tags); } } @@ -330,22 +353,23 @@ function initTagDisplayAndRemoval() { tags = tags.filter((t) => t !== tag); card.dataset.tags = tags.join("||"); - const modal = document.querySelector(".note-modal-overlay"); - if (modal && modal.currentNote) { - modal.currentNote.tags = (modal.currentNote.tags || []).filter((t) => t !== tag); - const tagsContainer = modal.querySelector("#note-modal-tags"); - renderModalTags(tagsContainer, modal.currentNote.tags); + const modal = getOpenModalOverlay(); + const entity = getModalCurrentEntity(modal); + if (modal && entity) { + entity.tags = (entity.tags || []).filter((t) => t !== tag); + const tagsContainer = modal.querySelector(".note-modal-tags"); + renderModalTags(tagsContainer, entity.tags); } // Also update corresponding card if visible - const noteId = card.dataset.noteId; - const noteCard = noteId ? document.querySelector(`.note-card[data-id="${noteId}"]`) : null; - if (noteCard) { - const pill = noteCard.querySelector(`.note-tag[data-tag="${CSS.escape(tag)}"]`); + const entityId = card.dataset.noteId || card.dataset.todoId || (entity && entity.id ? String(entity.id) : ""); + const entityCard = entityId ? document.querySelector(`.note-card[data-id="${entityId}"]`) : null; + if (entityCard) { + const pill = entityCard.querySelector(`.note-tag[data-tag="${CSS.escape(tag)}"]`); if (pill) pill.remove(); - let noteTags = (noteCard.dataset.tags || "").split("||").filter((t) => t); - noteTags = noteTags.filter((t) => t !== tag); - noteCard.dataset.tags = noteTags.join("||"); + let entityTags = (entityCard.dataset.tags || "").split("||").filter((t) => t); + entityTags = entityTags.filter((t) => t !== tag); + entityCard.dataset.tags = entityTags.join("||"); } } }) @@ -358,6 +382,15 @@ function initTagDisplayAndRemoval() { tagDisplayRemovalInitialized = true; } +function getOpenModalOverlay() { + return document.querySelector(".note-modal-overlay.open"); +} + +function getModalCurrentEntity(modal) { + if (!modal) return null; + return modal.currentTodo || modal.currentNote || null; +} + function resolveThemeAssetBasePath() { const cssLink = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).find( (link) => link.href && link.href.includes("/custom_views.css"), @@ -743,18 +776,19 @@ function refreshBackgroundPalettes() { ); }); - const modal = document.querySelector(".note-modal-overlay"); - if (!modal || !modal.currentNote) return; + const modal = getOpenModalOverlay(); + const entity = getModalCurrentEntity(modal); + if (!modal || !entity) return; const modalCard = modal.querySelector(".note-modal"); if (modalCard) { - modal.currentNote.color = getElementVisualColor(modalCard); - modal.currentNote.background = getElementVisualBackground(modalCard); + entity.color = getElementVisualColor(modalCard); + entity.background = getElementVisualBackground(modalCard); } - const modalColorPopup = modal.querySelector("#note-modal-color-popup"); + const modalColorPopup = modal.querySelector(".note-modal-palette"); if (modalColorPopup) { - modalColorPopup.innerHTML = generateModalPaletteButtons(modal.currentNote); + modalColorPopup.innerHTML = generateModalPaletteButtons(entity); } } @@ -1572,211 +1606,858 @@ function syncNoteFromCardElement(note, card) { * Initialize the Google Tasks-like view */ function initTodoView(linkList, container) { - document.body.classList.add("view-todo"); + document.body.classList.add("view-todo", "view-notes"); - // 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); + const basePath = typeof shaarli !== "undefined" && shaarli.basePath ? shaarli.basePath : ""; - // 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"; + const wrapper = document.createElement("div"); + wrapper.className = "notes-wrapper todo-wrapper"; + + const topBar = document.createElement("div"); + topBar.className = "notes-top-bar"; + + const inputContainer = document.createElement("div"); + inputContainer.className = "note-input-container"; + inputContainer.innerHTML = ` +
+ Créer une tâche... +
+ +
+
+ `; + topBar.appendChild(inputContainer); + + const tools = document.createElement("div"); + tools.className = "notes-tools"; + tools.innerHTML = ` + + + `; + topBar.appendChild(tools); + wrapper.appendChild(topBar); + + const contentArea = document.createElement("div"); + contentArea.className = "notes-content-area"; + + const links = Array.from(linkList.querySelectorAll(".link-outer")); + const todos = links.map((link) => parseTodoFromLink(link)).filter((t) => t); + + wrapper.appendChild(contentArea); + + linkList.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"); + renderTodos(contentArea, todos, "grid"); - // Update Sidebar Active State - document.querySelectorAll(".todo-list-item").forEach((el) => el.classList.remove("active")); - if (event && event.currentTarget) event.currentTarget.classList.add("active"); + const modalOverlay = document.createElement("div"); + modalOverlay.className = "note-modal-overlay todo-modal-overlay"; + modalOverlay.innerHTML = ` +
+
+

+ +
+
+
+ +
+
+
+
+
+ +
+
+ + +
+ +
+
+ `; + document.body.appendChild(modalOverlay); - if (group === "all") { - title.textContent = "Mes tâches"; - items.forEach((item) => (item.style.display = "flex")); + 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"); + renderTodos(contentArea, todos, "grid"); + }); + + btnList.addEventListener("click", () => { + btnList.classList.add("active"); + btnGrid.classList.remove("active"); + renderTodos(contentArea, todos, "list"); + }); + + modalOverlay.querySelector("#todo-modal-close").addEventListener("click", () => { + modalOverlay.classList.remove("open"); + }); + modalOverlay.addEventListener("click", (e) => { + if (e.target === modalOverlay) modalOverlay.classList.remove("open"); + }); + + const modalPinBtn = modalOverlay.querySelector("#todo-modal-pin"); + const modalColorBtn = modalOverlay.querySelector("#todo-modal-color-btn"); + const modalColorPopup = modalOverlay.querySelector("#todo-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.todoId || "" : "", + 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 todoId = modalCard.dataset.todoId; + const editUrl = modalCard.dataset.editUrl; + if (!todoId || !editUrl) return; + + togglePinTag(todoId, 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 { - title.textContent = group; - items.forEach((item) => { - if (item.dataset.group === group) item.style.display = "flex"; - else item.style.display = "none"; - }); + tags = tags.filter((t) => t !== "shaarli-pin"); } - }; + + modalCard.dataset.tags = tags.join("||"); + renderModalTags(modalOverlay.querySelector("#todo-modal-tags"), tags); + + if (modalOverlay.currentTodo) { + modalOverlay.currentTodo.isPinned = isPinned; + modalOverlay.currentTodo.tags = tags; + } + }); + + modalOverlay.querySelector("#todo-modal-delete").addEventListener("click", () => { + const modalCard = modalOverlay.querySelector(".note-modal"); + const deleteUrl = modalCard.dataset.deleteUrl; + if (deleteUrl && deleteUrl !== "#") { + window.location.href = deleteUrl; + } + }); + + modalOverlay.addEventListener("click", (e) => { + if (!e.target.closest(".note-modal-color-picker")) { + modalColorPopup.classList.remove("open"); + } + }); + + modalOverlay.querySelector("#todo-modal-add-item").addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + if (!modalOverlay.currentTodo) return; + addTodoItem(modalOverlay, modalOverlay.currentTodo); + }); + + if (typeof initTagDisplayAndRemoval === "function") { + initTagDisplayAndRemoval(); + } } -function parseTaskFromLink(linkEl) { +function parseTodoFromLink(linkEl) { + if (!linkEl) return null; + 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 titleEl = linkEl.querySelector(".link-title"); + const title = titleEl ? titleEl.textContent.trim() : ""; - const title = titleEl ? titleEl.textContent.trim() : "Task"; const descEl = linkEl.querySelector(".link-description"); - const rawDesc = descEl ? descEl.innerHTML : ""; - const textDesc = descEl ? descEl.textContent : ""; + const descHtml = descEl ? descEl.innerHTML : ""; + const descText = descEl ? descEl.textContent : ""; - // Check if it's really a todo (should be if we are in ?searchtags=todo, but double check) - // We assume yes. + const rawTags = []; + linkEl.querySelectorAll(".link-tag-list a").forEach((tag) => { + const t = (tag.textContent || "").trim(); + if (t) rawTags.push(t); + }); - // Parse Metadata from Description text - // Format: 📅 **Échéance :** - // Format: 🏷️ **Groupe :** - // Format: - [ ] Subtask or Main task status? + const { color, filter, background, fontColor } = extractNoteVisualStateFromTags(rawTags); - // 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. + const tags = rawTags.filter((t) => { + if (t.startsWith(NOTE_COLOR_TAG_PREFIX)) return false; + if (t.startsWith("note-custom-")) 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; + }); - let isCompleted = false; - if (textDesc.includes("[x]")) isCompleted = true; + 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 : "#"; + const isPinned = tags.includes("shaarli-pin"); - // 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(); + const items = extractChecklistItemsFromDescription(descEl, descText); + const parsed = parseTodoMarkdown(descText); 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 : "#", + descHtml, + descText, + items, + _todoHeaderLines: parsed.headerLines, + _todoFooterLines: parsed.footerLines, + tags, + color, + filter, + background, + fontColor, + editUrl, + deleteUrl, + pinUrl, + isPinned, }; } -function renderTaskItem(task) { - const el = document.createElement("div"); - el.className = `todo-item ${task.isCompleted ? "completed" : ""}`; - el.dataset.group = task.group || ""; +function extractChecklistItemsFromDescription(descEl, descText) { + const items = []; - /* - 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. - */ + if (descEl) { + const checkboxEls = Array.from(descEl.querySelectorAll('input[type="checkbox"]')); + if (checkboxEls.length > 0) { + checkboxEls.forEach((cb) => { + const li = cb.closest("li") || cb.parentElement; + let text = ""; + if (li) { + const clone = li.cloneNode(true); + clone.querySelectorAll('input[type="checkbox"]').forEach((i) => i.remove()); + text = (clone.textContent || "").trim(); + } + items.push({ checked: !!cb.checked, text }); + }); + return items; + } + } - 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; + const lines = String(descText || "").split(/\r?\n/); + const fallbackSource = descEl ? descEl.innerHTML : lines.join("\n"); + const parsed = parseTodoMarkdown(fallbackSource); + return parsed.items; } -function isOverdue(dateStr) { +function getFormFieldValue(input) { + if (!input) return ""; + const tag = (input.tagName || "").toUpperCase(); + if (tag === "TEXTAREA") { + const v = typeof input.value === "string" ? input.value : ""; + if (v && v.trim() !== "") return v; + const t = typeof input.textContent === "string" ? input.textContent : ""; + return t; + } + return input.value; +} + +async function hydrateTodoFromEditForm(todo) { + if (!todo || !todo.editUrl || todo.editUrl === "#") return; + if (todo._todoHydrated) return; + if (todo._todoHydratePromise) return todo._todoHydratePromise; + + todo._todoHydratePromise = (async () => { + try { + const response = await fetch(todo.editUrl, { credentials: "same-origin" }); + if (!response.ok) { + console.error("Todo hydration failed (edit form fetch not ok).", { id: todo && todo.id, url: todo && todo.editUrl, status: response && response.status }); + return; + } + 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) { + console.error("Todo hydration failed (edit form not found).", { id: todo && todo.id, url: todo && todo.editUrl, status: response && response.status }); + return; + } + + 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, getFormFieldValue(input)); + } + }); + + todo._todoEditAction = form.action; + todo._todoEditBaseData = formData; + + const rawMarkdown = formData.get("lf_description") || ""; + const parsed = parseTodoMarkdown(rawMarkdown); + todo._todoHeaderLines = parsed.headerLines; + todo._todoFooterLines = parsed.footerLines; + todo.items = parsed.items; + + if (!todo._todoDebugLogged && rawMarkdown && String(rawMarkdown).trim() && (!parsed.items || parsed.items.length === 0)) { + todo._todoDebugLogged = true; + console.warn("Todo parsed 0 items from lf_description; sample follows.", { + id: todo && todo.id, + url: todo && todo.editUrl, + sample: String(rawMarkdown).slice(0, 240), + }); + } + + updateTodoCardPreview(todo); + todo._todoHydrated = true; + } catch (e) { + console.error("Error hydrating todo markdown:", e); + } finally { + todo._todoHydratePromise = null; + } + })(); + + return todo._todoHydratePromise; +} + +function renderTodos(container, todos, viewMode) { + if (!container) return; + container.innerHTML = ""; + container.className = viewMode === "grid" ? "notes-masonry" : "notes-list-view"; + + const visibleTodos = (todos || []).slice(); + visibleTodos.sort((a, b) => { + const aPinned = (a.tags || []).includes("shaarli-pin"); + const bPinned = (b.tags || []).includes("shaarli-pin"); + return bPinned - aPinned; + }); + + visibleTodos.forEach((todo) => { + const card = document.createElement("div"); + card.className = "note-card todo-card"; + card.dataset.id = todo.id; + card.dataset.editUrl = todo.editUrl || ""; + card.dataset.tags = (todo.tags || []).filter((t) => t).join("||"); + todo._todoCardEl = card; + applyNoteVisualState(card, todo); + if (viewMode === "list") card.classList.add("list-mode"); + + card.addEventListener("click", (e) => { + if (e.target.closest("button") || e.target.closest("a") || e.target.closest(".note-hover-actions")) return; + syncNoteFromCardElement(todo, card); + openTodoModal(todo); + }); + + const inner = document.createElement("div"); + inner.className = "note-inner"; + + if (todo.title) { + const h3 = document.createElement("h3"); + h3.className = "note-title"; + h3.textContent = todo.title; + inner.appendChild(h3); + } + + const body = document.createElement("div"); + body.className = "note-body todo-checklist-preview-wrap"; + body.innerHTML = buildTodoPreviewHtml(todo.items || []); + inner.appendChild(body); + + if ((todo.tags || []).length > 0) { + const tagContainer = document.createElement("div"); + tagContainer.className = "note-tags"; + todo.tags.forEach((t) => { + if (isTechnicalTag(t)) return; + const pill = createTagPill({ tag: t, onRemoveClass: "note-tag-remove-btn", tagClass: "note-tag", canRemove: !!todo.editUrl && todo.editUrl !== "#" }); + tagContainer.appendChild(pill); + }); + inner.appendChild(tagContainer); + } + + const actions = document.createElement("div"); + actions.className = "note-hover-actions"; + + const paletteBtnId = `palette-${todo.id}`; + + actions.innerHTML = ` +
+ +
+
+ + + + `; + + const paletteBtn = actions.querySelector(`#${paletteBtnId}`); + paletteBtn.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + openBackgroundStudioPanel({ + anchorEl: paletteBtn, + mode: "entity", + entityId: todo.id, + editUrl: todo.editUrl, + currentColor: getElementVisualColor(card), + currentFilter: getElementVisualFilter(card), + currentBackground: getElementVisualBackground(card), + title: "Mes images & couleurs", + }); + }); + + inner.appendChild(actions); + card.appendChild(inner); + container.appendChild(card); + + hydrateTodoFromEditForm(todo); + }); +} + +function buildTodoPreviewHtml(items) { + const safeItems = Array.isArray(items) ? items : []; + const maxItems = 8; + const visible = safeItems.slice(0, maxItems); + const hasMore = safeItems.length > maxItems; + + const rows = visible + .map((it) => { + const checked = !!it.checked; + const text = escapeHtml(String(it.text || "")); + return `
  • ${text}
  • `; + }) + .join(""); + + return ``; +} + +function escapeHtml(str) { + return String(str) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/\"/g, """) + .replace(/'/g, "'"); +} + +function parseTodoMarkdown(markdown) { + const raw = String(markdown || ""); + const normalized = raw + .replace(//gi, "\n") + .replace(/<\/(p|div|li|h\d)\s*>/gi, "\n"); + + let text = normalized; try { - return new Date(dateStr) < new Date(); + const tmp = document.createElement("div"); + tmp.innerHTML = normalized; + const checkboxEls = Array.from(tmp.querySelectorAll('input[type="checkbox"]')); + if (checkboxEls.length > 0) { + const items = []; + checkboxEls.forEach((cb) => { + const li = cb.closest("li") || cb.parentElement; + let itemText = ""; + if (li) { + const clone = li.cloneNode(true); + clone.querySelectorAll('input[type="checkbox"]').forEach((i) => i.remove()); + itemText = (clone.textContent || "").trim(); + } + items.push({ checked: !!cb.checked, text: itemText }); + }); + return { headerLines: [], footerLines: [], items }; + } + + const listItemEls = Array.from(tmp.querySelectorAll("li")); + if (listItemEls.length > 0) { + const items = listItemEls + .map((li) => ({ checked: false, text: (li.textContent || "").trim() })) + .filter((it) => it.text); + if (items.length > 0) { + return { headerLines: [], footerLines: [], items }; + } + } + text = tmp.textContent || normalized; } catch (e) { - return false; + text = normalized; + } + + const lines = text.split(/\r?\n/); + const taskRegexes = [ + /^\s*[-*+]\s*\[([ xX])\]\s*(.*)$/, + /^\s*\[([ xX])\]\s*(.*)$/, + /^\s*☐\s*(.*)$/, + /^\s*☑\s*(.*)$/, + ]; + + let firstTaskIndex = -1; + let lastTaskIndex = -1; + const items = []; + + lines.forEach((line, idx) => { + let matched = false; + + for (let i = 0; i < taskRegexes.length; i++) { + const rx = taskRegexes[i]; + const m = line.match(rx); + if (!m) continue; + + matched = true; + if (firstTaskIndex === -1) firstTaskIndex = idx; + lastTaskIndex = idx; + + if (i === 2) { + items.push({ checked: false, text: (m[1] || "").trim() }); + } else if (i === 3) { + items.push({ checked: true, text: (m[1] || "").trim() }); + } else { + items.push({ checked: String(m[1] || "").toLowerCase() === "x", text: (m[2] || "").trim() }); + } + break; + } + + if (!matched) return; + }); + + const headerLines = firstTaskIndex === -1 ? lines.slice() : lines.slice(0, firstTaskIndex); + const footerLines = lastTaskIndex === -1 ? [] : lines.slice(lastTaskIndex + 1); + + return { headerLines, footerLines, items }; +} + +function buildTodoMarkdown(todo) { + const headerLines = Array.isArray(todo._todoHeaderLines) ? todo._todoHeaderLines : []; + const footerLines = Array.isArray(todo._todoFooterLines) ? todo._todoFooterLines : []; + const items = Array.isArray(todo.items) ? todo.items : []; + const taskLines = items.map((it) => `- [${it.checked ? "x" : " "}] ${String(it.text || "").trim()}`); + + const out = []; + headerLines.forEach((l) => out.push(l)); + taskLines.forEach((l) => out.push(l)); + footerLines.forEach((l) => out.push(l)); + + return out.join("\n").replace(/\s+$/g, ""); +} + +function renderTodoModalChecklist(modal, todo) { + const list = modal.querySelector("#todo-modal-checklist"); + if (!list) return; + + list.innerHTML = ""; + const items = Array.isArray(todo.items) ? todo.items : []; + + items.forEach((item, idx) => { + const row = document.createElement("div"); + row.className = "todo-checklist-row"; + row.dataset.index = String(idx); + row.setAttribute("draggable", "true"); + row.classList.toggle("is-checked", !!item.checked); + row.innerHTML = ` + + + + + `; + + const cb = row.querySelector(".todo-item-checkbox"); + const textInput = row.querySelector(".todo-item-text"); + const delBtn = row.querySelector(".todo-item-delete"); + + cb.addEventListener("change", () => { + todo.items[idx].checked = cb.checked; + scheduleTodoSave(modal, todo); + row.classList.toggle("is-checked", cb.checked); + }); + + textInput.addEventListener("input", () => { + todo.items[idx].text = textInput.value; + scheduleTodoSave(modal, todo); + }); + + delBtn.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + todo.items.splice(idx, 1); + renderTodoModalChecklist(modal, todo); + scheduleTodoSave(modal, todo); + }); + + row.addEventListener("dragstart", (e) => { + row.classList.add("is-dragging"); + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("text/plain", String(idx)); + }); + row.addEventListener("dragend", () => { + row.classList.remove("is-dragging"); + }); + + list.appendChild(row); + }); + + if (!list.dataset.dndInit) { + list.addEventListener( + "dragover", + (e) => { + e.preventDefault(); + const dragging = list.querySelector(".todo-checklist-row.is-dragging"); + if (!dragging) return; + + const afterElement = getDragAfterElement(list, e.clientY); + if (afterElement == null) { + list.appendChild(dragging); + } else { + list.insertBefore(dragging, afterElement); + } + }, + { passive: false }, + ); + + list.addEventListener("drop", (e) => { + e.preventDefault(); + const current = modal && modal.currentTodo ? modal.currentTodo : null; + if (!current) return; + const rows = Array.from(list.querySelectorAll(".todo-checklist-row")); + const newItems = []; + rows.forEach((r) => { + const oldIndex = Number(r.dataset.index); + if (!Number.isNaN(oldIndex) && current.items && current.items[oldIndex]) { + newItems.push(current.items[oldIndex]); + } + }); + current.items = newItems; + renderTodoModalChecklist(modal, current); + scheduleTodoSave(modal, current); + }); + + list.dataset.dndInit = "1"; } } -function formatDate(dateStr) { - try { - const d = new Date(dateStr); - return d.toLocaleDateString(); - } catch (e) { - return dateStr; +function getDragAfterElement(container, y) { + const draggableElements = [...container.querySelectorAll(".todo-checklist-row:not(.is-dragging)")]; + + return draggableElements.reduce( + (closest, child) => { + const box = child.getBoundingClientRect(); + const offset = y - box.top - box.height / 2; + if (offset < 0 && offset > closest.offset) { + return { offset, element: child }; + } + return closest; + }, + { offset: Number.NEGATIVE_INFINITY, element: null }, + ).element; +} + +async function openTodoModal(todo) { + const modal = document.querySelector(".todo-modal-overlay"); + if (!modal) return; + + const modalCard = modal.querySelector(".note-modal"); + const title = modal.querySelector("#todo-modal-title"); + const tagsContainer = modal.querySelector("#todo-modal-tags"); + const editLink = modal.querySelector("#todo-modal-edit"); + const pinButton = modal.querySelector("#todo-modal-pin"); + const modalColorPopup = modal.querySelector("#todo-modal-color-popup"); + + modal.currentTodo = todo; + + modalCard.className = "note-modal todo-modal"; + applyNoteVisualState(modalCard, todo); + modalCard.dataset.todoId = todo.id || ""; + modalCard.dataset.editUrl = todo.editUrl || ""; + modalCard.dataset.deleteUrl = todo.deleteUrl || ""; + modalCard.dataset.background = todo.background || "none"; + + const visibleTags = (todo.tags || []).filter((tag) => tag && !isTechnicalTag(tag)); + modalCard.dataset.tags = visibleTags.join("||"); + + title.textContent = todo.title || "Sans titre"; + renderModalTags(tagsContainer, visibleTags); + + if (editLink) { + editLink.href = todo.editUrl || "#"; + } + + if (modalColorPopup) { + modalColorPopup.innerHTML = generateUnifiedPaletteMenu({ + entityId: todo && todo.id ? todo.id : "", + editUrl: todo && todo.editUrl ? todo.editUrl : "", + currentColor: todo && todo.color ? todo.color : "default", + currentFilter: todo && todo.filter ? todo.filter : "none", + mode: "modal", + }); + modalColorPopup.classList.remove("open"); + } + + setModalPinButtonState(pinButton, !!todo.isPinned); + + modal.classList.add("open"); + renderTodoModalChecklist(modal, todo); + + const loadKey = `${todo.id}-${Date.now()}`; + modal._todoLoadKey = loadKey; + + if (todo.editUrl && todo.editUrl !== "#") { + try { + const response = await fetch(todo.editUrl, { credentials: "same-origin" }); + if (!response.ok) { + console.error("Todo modal load failed (edit form fetch not ok).", { id: todo && todo.id, url: todo && todo.editUrl, status: response && response.status }); + return; + } + const html = await response.text(); + if (modal._todoLoadKey !== loadKey) return; + + const parser = new DOMParser(); + const doc = parser.parseFromString(html, "text/html"); + const form = doc.querySelector('form[name="linkform"]'); + if (!form) { + console.error("Todo modal load failed (edit form not found).", { id: todo && todo.id, url: todo && todo.editUrl, status: response && response.status }); + return; + } + + 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, getFormFieldValue(input)); + } + }); + + todo._todoEditAction = form.action; + todo._todoEditBaseData = formData; + + const rawMarkdown = formData.get("lf_description") || ""; + const parsed = parseTodoMarkdown(rawMarkdown); + todo._todoHeaderLines = parsed.headerLines; + todo._todoFooterLines = parsed.footerLines; + todo.items = parsed.items; + + renderTodoModalChecklist(modal, todo); + } catch (e) { + console.error("Error loading todo markdown:", e); + } + } +} + +function addTodoItem(modal, todo) { + if (!todo.items) todo.items = []; + todo.items.push({ checked: false, text: "" }); + renderTodoModalChecklist(modal, todo); + const inputs = modal.querySelectorAll(".todo-item-text"); + const last = inputs && inputs.length ? inputs[inputs.length - 1] : null; + if (last) last.focus(); + scheduleTodoSave(modal, todo); +} + +function scheduleTodoSave(modal, todo) { + if (!modal || !todo) return; + if (modal._todoSaveTimer) { + clearTimeout(modal._todoSaveTimer); + } + + updateTodoCardPreview(todo); + + modal._todoSaveTimer = setTimeout(() => { + persistTodoChanges(todo).catch((err) => { + console.error("Error saving todo:", err); + alert("Erreur lors de la sauvegarde de la tâche. Veuillez rafraîchir la page."); + }); + }, 600); +} + +function updateTodoCardPreview(todo) { + if (!todo || !todo.id) return; + let card = todo._todoCardEl && todo._todoCardEl.isConnected ? todo._todoCardEl : null; + if (!card) { + card = document.querySelector(`.note-card.todo-card[data-id="${String(todo.id)}"]`); + } + if (!card) { + const all = Array.from(document.querySelectorAll(".note-card.todo-card")); + card = all.find((c) => String(c.dataset.id || "") === String(todo.id)) || null; + } + if (!card) return; + const wrap = card.querySelector(".todo-checklist-preview-wrap"); + if (!wrap) return; + wrap.innerHTML = buildTodoPreviewHtml(todo.items || []); +} + +async function persistTodoChanges(todo, retryCount = 0) { + if (!todo || !todo.editUrl || todo.editUrl === "#") return; + + const refreshEditForm = async () => { + const response = await fetch(todo.editUrl, { 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)); + } + }); + + todo._todoEditAction = form.action; + todo._todoEditBaseData = baseData; + + const rawMarkdown = baseData.get("lf_description") || ""; + const parsed = parseTodoMarkdown(rawMarkdown); + todo._todoHeaderLines = parsed.headerLines; + todo._todoFooterLines = parsed.footerLines; + }; + + if (!todo._todoEditAction || !todo._todoEditBaseData || !(todo._todoEditBaseData.get && todo._todoEditBaseData.get("token"))) { + await refreshEditForm(); + } + + const description = buildTodoMarkdown(todo); + const formData = new URLSearchParams(todo._todoEditBaseData.toString()); + formData.set("lf_description", description); + formData.append("save_edit", "1"); + + const response = await fetch(todo._todoEditAction, { + 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) { + todo._todoEditAction = ""; + todo._todoEditBaseData = null; + await refreshEditForm(); + return persistTodoChanges(todo, retryCount + 1); + } + throw new Error("Failed to save todo"); } } @@ -2476,8 +3157,9 @@ function renderModalTags(container, tags) { container.innerHTML = ""; const visibleTags = (tags || []).filter((tag) => tag && !isTechnicalTag(tag)); - const modal = document.querySelector(".note-modal-overlay"); - const canRemove = !!(modal && modal.currentNote && modal.currentNote.editUrl && modal.currentNote.editUrl !== "#"); + const modal = container.closest(".note-modal-overlay") || getOpenModalOverlay(); + const entity = getModalCurrentEntity(modal); + const canRemove = !!(entity && entity.editUrl && entity.editUrl !== "#"); if (visibleTags.length === 0) { container.classList.add("is-empty"); @@ -2544,43 +3226,64 @@ function generateModalPaletteButtons(note) { } window.setModalNoteColor = function (color) { - const modal = document.querySelector(".note-modal-overlay"); - if (!modal || !modal.currentNote) return; + const modal = getOpenModalOverlay(); + const entity = getModalCurrentEntity(modal); + if (!modal || !entity) return; - const currentNote = modal.currentNote; - setNoteColor(currentNote.id, color, currentNote.editUrl); + setNoteColor(entity.id, color, entity.editUrl); - currentNote.color = color; + entity.color = color; const modalCard = modal.querySelector(".note-modal"); if (modalCard) { - applyNoteVisualState(modalCard, currentNote); + applyNoteVisualState(modalCard, entity); } - const modalColorPopup = modal.querySelector("#note-modal-color-popup"); + const modalColorPopup = modal.querySelector(".note-modal-palette"); if (modalColorPopup) { - modalColorPopup.innerHTML = generateModalPaletteButtons(currentNote); + modalColorPopup.innerHTML = generateModalPaletteButtons(entity); + modalColorPopup.classList.add("open"); + positionPalettePopup(modalColorPopup); + } +}; + +window.setModalNoteFontColor = function (fontColorKey) { + const modal = getOpenModalOverlay(); + const entity = getModalCurrentEntity(modal); + if (!modal || !entity) return; + + setNoteFontColor(entity.id, fontColorKey, entity.editUrl); + + entity.fontColor = fontColorKey; + const modalCard = modal.querySelector(".note-modal"); + if (modalCard) { + applyNoteVisualState(modalCard, entity); + } + + const modalColorPopup = modal.querySelector(".note-modal-palette"); + if (modalColorPopup) { + modalColorPopup.innerHTML = generateModalPaletteButtons(entity); modalColorPopup.classList.add("open"); positionPalettePopup(modalColorPopup); } }; window.setModalNoteFilter = function (filterKey) { - const modal = document.querySelector(".note-modal-overlay"); - if (!modal || !modal.currentNote) return; + const modal = getOpenModalOverlay(); + const entity = getModalCurrentEntity(modal); + if (!modal || !entity) return; - const currentNote = modal.currentNote; const normalizedFilterKey = normalizeFilterKey(filterKey) || "none"; - setNoteFilter(currentNote.id, normalizedFilterKey, currentNote.editUrl); + setNoteFilter(entity.id, normalizedFilterKey, entity.editUrl); - currentNote.filter = normalizedFilterKey; + entity.filter = normalizedFilterKey; const modalCard = modal.querySelector(".note-modal"); if (modalCard) { - applyNoteVisualState(modalCard, currentNote); + applyNoteVisualState(modalCard, entity); } - const modalColorPopup = modal.querySelector("#note-modal-color-popup"); + const modalColorPopup = modal.querySelector(".note-modal-palette"); if (modalColorPopup) { - modalColorPopup.innerHTML = generateModalPaletteButtons(currentNote); + modalColorPopup.innerHTML = generateModalPaletteButtons(entity); modalColorPopup.classList.add("open"); positionPalettePopup(modalColorPopup); } @@ -2610,16 +3313,17 @@ window.setNoteColor = function (noteId, color, editUrl) { } } - const modal = document.querySelector(".note-modal-overlay"); - if (modal && modal.currentNote && String(modal.currentNote.id) === String(noteId)) { - modal.currentNote.color = color; + const modal = getOpenModalOverlay(); + const entity = getModalCurrentEntity(modal); + if (modal && entity && String(entity.id) === String(noteId)) { + entity.color = color; const modalCard = modal.querySelector(".note-modal"); if (modalCard) { - applyNoteVisualState(modalCard, modal.currentNote); + applyNoteVisualState(modalCard, entity); } - const modalColorPopup = modal.querySelector("#note-modal-color-popup"); + const modalColorPopup = modal.querySelector(".note-modal-palette"); if (modalColorPopup) { - modalColorPopup.innerHTML = generateModalPaletteButtons(modal.currentNote); + modalColorPopup.innerHTML = generateModalPaletteButtons(entity); } } @@ -2641,7 +3345,7 @@ window.setNoteColor = function (noteId, color, editUrl) { if (input.type === "checkbox") { if (input.checked) formData.append(input.name, input.value || "on"); } else if (input.name) { - formData.append(input.name, input.value); + formData.append(input.name, getFormFieldValue(input)); } }); @@ -2718,16 +3422,17 @@ window.setNoteFilter = function (noteId, filterKey, editUrl) { } } - const modal = document.querySelector(".note-modal-overlay"); - if (modal && modal.currentNote && String(modal.currentNote.id) === String(noteId)) { - modal.currentNote.filter = normalizedFilterKey; + const modal = getOpenModalOverlay(); + const entity = getModalCurrentEntity(modal); + if (modal && entity && String(entity.id) === String(noteId)) { + entity.filter = normalizedFilterKey; const modalCard = modal.querySelector(".note-modal"); if (modalCard) { - applyNoteVisualState(modalCard, modal.currentNote); + applyNoteVisualState(modalCard, entity); } - const modalColorPopup = modal.querySelector("#note-modal-color-popup"); + const modalColorPopup = modal.querySelector(".note-modal-palette"); if (modalColorPopup) { - modalColorPopup.innerHTML = generateModalPaletteButtons(modal.currentNote); + modalColorPopup.innerHTML = generateModalPaletteButtons(entity); } } @@ -2747,7 +3452,7 @@ window.setNoteFilter = function (noteId, filterKey, editUrl) { if (input.type === "checkbox") { if (input.checked) formData.append(input.name, input.value || "on"); } else if (input.name) { - formData.append(input.name, input.value); + formData.append(input.name, getFormFieldValue(input)); } }); @@ -2800,16 +3505,17 @@ window.setNoteBackground = function (noteId, backgroundKey, editUrl) { 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 modal = getOpenModalOverlay(); + const entity = getModalCurrentEntity(modal); + if (modal && entity && String(entity.id) === String(noteId)) { + entity.background = normalizedBackgroundKey; const modalCard = modal.querySelector(".note-modal"); if (modalCard) { - applyNoteVisualState(modalCard, modal.currentNote); + applyNoteVisualState(modalCard, entity); } - const modalColorPopup = modal.querySelector("#note-modal-color-popup"); + const modalColorPopup = modal.querySelector(".note-modal-palette"); if (modalColorPopup) { - modalColorPopup.innerHTML = generateModalPaletteButtons(modal.currentNote); + modalColorPopup.innerHTML = generateModalPaletteButtons(entity); } } @@ -2829,7 +3535,7 @@ window.setNoteBackground = function (noteId, backgroundKey, editUrl) { if (input.type === "checkbox") { if (input.checked) formData.append(input.name, input.value || "on"); } else if (input.name) { - formData.append(input.name, input.value); + formData.append(input.name, getFormFieldValue(input)); } }); @@ -2864,22 +3570,22 @@ window.setNoteBackground = function (noteId, backgroundKey, editUrl) { }; window.setModalNoteBackground = function (backgroundKey) { - const modal = document.querySelector(".note-modal-overlay"); - if (!modal || !modal.currentNote) return; + const modal = getOpenModalOverlay(); + const entity = getModalCurrentEntity(modal); + if (!modal || !entity) return; - const currentNote = modal.currentNote; const normalizedBackgroundKey = backgroundKey === "none" ? "none" : normalizeBackgroundKey(backgroundKey) || "none"; - setNoteBackground(currentNote.id, normalizedBackgroundKey, currentNote.editUrl); + setNoteBackground(entity.id, normalizedBackgroundKey, entity.editUrl); - currentNote.background = normalizedBackgroundKey; + entity.background = normalizedBackgroundKey; const modalCard = modal.querySelector(".note-modal"); if (modalCard) { - applyNoteVisualState(modalCard, currentNote); + applyNoteVisualState(modalCard, entity); } - const modalColorPopup = modal.querySelector("#note-modal-color-popup"); + const modalColorPopup = modal.querySelector(".note-modal-palette"); if (modalColorPopup) { - modalColorPopup.innerHTML = generateModalPaletteButtons(currentNote); + modalColorPopup.innerHTML = generateModalPaletteButtons(entity); modalColorPopup.classList.add("open"); positionPalettePopup(modalColorPopup); } @@ -2901,7 +3607,7 @@ function addTagToNote(editUrl, tag) { if (input.type === "checkbox") { if (input.checked) formData.append(input.name, input.value || "on"); } else if (input.name) { - formData.append(input.name, input.value); + formData.append(input.name, getFormFieldValue(input)); } }); @@ -3179,7 +3885,7 @@ function togglePinTag(id, editUrl, btn) { if (input.type === "checkbox") { if (input.checked) formData.append(input.name, input.value || "on"); } else if (input.name) { - formData.append(input.name, input.value); + formData.append(input.name, getFormFieldValue(input)); } }); @@ -3302,7 +4008,7 @@ function openColorPickerPanel({ mode, entityId, editUrl, type }) { if ((type || "background") === "font") { let prevFontColorKey = "auto"; if ((mode || "entity") === "modal") { - const modal = document.querySelector(".note-modal-overlay"); + const modal = getOpenModalOverlay(); const modalCard = modal ? modal.querySelector(".note-modal") : null; prevFontColorKey = (modalCard && modalCard.dataset.fontColor) ? modalCard.dataset.fontColor : "auto"; } else { @@ -3318,7 +4024,7 @@ function openColorPickerPanel({ mode, entityId, editUrl, type }) { if ((type || "background") === "background") { let prevColorKey = "default"; if ((mode || "entity") === "modal") { - const modal = document.querySelector(".note-modal-overlay"); + const modal = getOpenModalOverlay(); const modalCard = modal ? modal.querySelector(".note-modal") : null; if (modalCard) { const isCustom = modalCard.dataset.color === "custom"; @@ -3635,17 +4341,19 @@ function setNoteFontColorVisual(noteId, fontColorKey) { applyTo(card); applyTo(bookmarkCard); - const modal = document.querySelector(".note-modal-overlay"); - if (modal && modal.currentNote && String(modal.currentNote.id) === String(noteId)) { + const modal = getOpenModalOverlay(); + const entity = getModalCurrentEntity(modal); + if (modal && entity && String(entity.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); + const modal = getOpenModalOverlay(); + const entity = getModalCurrentEntity(modal); + if (!modal || !entity) return; + setNoteFontColorVisual(entity.id, fontColorKey); } function setNoteColorVisual(noteId, colorKey) { @@ -3683,19 +4391,20 @@ function setNoteColorVisual(noteId, colorKey) { applyTo(card); applyTo(bookmarkCard); - const modal = document.querySelector(".note-modal-overlay"); - if (modal && modal.currentNote && String(modal.currentNote.id) === String(noteId)) { + const modal = getOpenModalOverlay(); + const entity = getModalCurrentEntity(modal); + if (modal && entity && String(entity.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); + const modal = getOpenModalOverlay(); + const entity = getModalCurrentEntity(modal); + if (!modal || !entity) return; + setNoteColorVisual(entity.id, colorKey); } - /* ========================================================== FONT COLOR FUNCTIONS ========================================================== */ @@ -3720,7 +4429,7 @@ function setNoteFontColor(noteId, fontColorKey, editUrl) { if (input.type === "checkbox") { if (input.checked) formData.append(input.name, input.value || "on"); } else if (input.name) { - formData.append(input.name, input.value); + formData.append(input.name, getFormFieldValue(input)); } }); @@ -3764,18 +4473,18 @@ function setNoteFontColor(noteId, fontColorKey, editUrl) { } function setModalNoteFontColor(fontColorKey) { - const modal = document.querySelector(".note-modal-overlay"); - if (!modal || !modal.currentNote) return; + const modal = getOpenModalOverlay(); + const entity = getModalCurrentEntity(modal); + if (!modal || !entity) return; - const currentNote = modal.currentNote; - setNoteFontColor(currentNote.id, fontColorKey, currentNote.editUrl); + setNoteFontColor(entity.id, fontColorKey, entity.editUrl); - currentNote.fontColor = fontColorKey; + entity.fontColor = fontColorKey; const modalCard = modal.querySelector(".note-modal"); if (modalCard) { let colorValue = "auto"; - if (fontColorKey.startsWith("custom:")) { - colorValue = fontColorKey.substring(7); + if (String(fontColorKey || "").startsWith("custom:")) { + colorValue = String(fontColorKey).substring(7); } else if (fontColorKey !== "auto") { const option = NOTE_FONT_COLOR_OPTIONS.find((opt) => opt.key === fontColorKey); colorValue = option ? option.value : "auto"; @@ -3798,10 +4507,9 @@ function setModalCustomFontColor(color) { CUSTOM NOTE COLOR FUNCTIONS ========================================================== */ function setModalCustomNoteColor(color) { - const modal = document.querySelector(".note-modal-overlay"); - if (!modal || !modal.currentNote) return; - - const currentNote = modal.currentNote; + const modal = getOpenModalOverlay(); + const entity = getModalCurrentEntity(modal); + if (!modal || !entity || !entity.editUrl) return; // Apply custom color visually const modalCard = modal.querySelector(".note-modal"); @@ -3813,7 +4521,7 @@ function setModalCustomNoteColor(color) { } // Save via AJAX - fetch(currentNote.editUrl) + fetch(entity.editUrl) .then((response) => response.text()) .then((html) => { const parser = new DOMParser(); @@ -3829,7 +4537,7 @@ function setModalCustomNoteColor(color) { if (input.type === "checkbox") { if (input.checked) formData.append(input.name, input.value || "on"); } else if (input.name) { - formData.append(input.name, input.value); + formData.append(input.name, getFormFieldValue(input)); } }); @@ -3864,7 +4572,7 @@ function setModalCustomNoteColor(color) { }) .then((response) => { if (!response.ok) throw new Error("Failed to save custom color"); - console.log(`Custom color ${color} saved for note ${currentNote.id}`); + console.log(`Custom color ${color} saved for note ${entity.id}`); }) .catch((err) => { console.error("Error saving custom color:", err);