From 1091f01f83610f5a5b5e4efefff5fc381668937e Mon Sep 17 00:00:00 2001 From: Guy Sandler Date: Sun, 16 Aug 2026 13:42:47 -0700 Subject: [PATCH] patches for firefox and other bugs --- css/content.css | 11 +- css/darkmodecss.js | 14 ++ html/popup.html | 8 +- js/background.js | 2 + js/content.js | 435 ++++++++++++++++++++++++++++++++++++++------- js/markdown.js | 3 +- js/popup.js | 42 ++--- manifest.json | 2 +- 8 files changed, 424 insertions(+), 93 deletions(-) diff --git a/css/content.css b/css/content.css index 60e35a6..5145a70 100644 --- a/css/content.css +++ b/css/content.css @@ -48,7 +48,7 @@ } .canvasrefined-add-assignment {max-height: 0; overflow:hidden;transition: .3s max-height;} .canvasrefined-custom-open {max-height: 250px} -#better-todo-add-task-menu.canvasrefined-custom-open {max-height: 420px;} +#better-todo-add-task-menu.canvasrefined-custom-open {max-height: 520px;} .canvasrefined-custom-input {box-sizing: border-box!important; width: 100%!important; height: 32px!important} #better-todo-new-task-date, #better-todo-new-task-time { @@ -58,10 +58,9 @@ font-size: 12px; padding-inline: 6px; } -#better-todo-new-task-date::-webkit-calendar-picker-indicator { - filter: invert(1) brightness(2); - opacity: .95; -} +/* The native date/time picker icons are themed in dark mode only via + color-scheme (see DARKMODE_CSS in css/darkmodecss.js); light mode keeps the + default dark glyphs on the light surface. */ .canvasrefined-custom-btn {background: #f5f5f5; border: 1px solid #c7cdd1; border-radius:6px; padding: 2px 8px;} /* Better sidebar buttons sit on the --bcsidebar surface and use --bcsidebar-text for labels/icons, so drop the light button chrome and use a subtle hover tint. */ @@ -204,7 +203,7 @@ .canvasrefined-notes-rendered ul,.canvasrefined-notes-rendered ol {margin:8px 0;padding-left:24px;} .canvasrefined-notes-rendered li {margin:3px 0;} .canvasrefined-notes-rendered li.cr-task {list-style:none;margin-left:-18px;display:flex;align-items:flex-start;gap:6px;} -.canvasrefined-notes-rendered li.cr-task input {margin-top:4px;} +.canvasrefined-notes-rendered li.cr-task input {margin-top:4px;cursor:pointer;} .canvasrefined-notes-rendered blockquote {margin:8px 0;padding:4px 12px;border-left:3px solid var(--bcborders,#c7cdd1);color:var(--bctext-1,#5b6770);background:var(--bcbackground-2,rgba(0,0,0,.03));} .canvasrefined-notes-rendered code {font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.9em;background:var(--bcbackground-2,#f1f3f5);padding:1px 5px;border-radius:3px;} .canvasrefined-notes-rendered pre {margin:10px 0;padding:10px 12px;background:var(--bcbackground-2,#f1f3f5);border:1px solid var(--bcborders,#e2e6e8);border-radius:4px;overflow:auto;} diff --git a/css/darkmodecss.js b/css/darkmodecss.js index cb0bd08..9a7baee 100644 --- a/css/darkmodecss.js +++ b/css/darkmodecss.js @@ -1193,4 +1193,18 @@ hr { .event-details strong { color: var(--bctext-0) !important; } +/* Theme the native date/time picker icons so they stay visible on the dark + sidebar surface. Applied in dark mode only — light mode keeps the default + dark glyphs (see css/content.css). + color-scheme: dark is the cross-browser way to flip the native picker icon + to a light glyph. Firefox 109+ ignores ::-webkit-calendar-picker-indicator + (the icon lives in a closed Shadow DOM), so a filter alone does nothing + there; color-scheme fixes both Firefox and modern Chrome. Do NOT also apply + an invert filter — color-scheme already makes the glyph light, and inverting + would flip it back to dark in Chrome. */ +#better-todo-new-task-date, +#better-todo-new-task-time { + color-scheme: dark !important; + color: var(--bctext-0) !important; +} `; \ No newline at end of file diff --git a/html/popup.html b/html/popup.html index 9883a90..0526500 100644 --- a/html/popup.html +++ b/html/popup.html @@ -500,7 +500,13 @@
-

Export Settings

+

+ Export Settings + + + + +

diff --git a/js/background.js b/js/background.js index 895780d..f8bebbf 100644 --- a/js/background.js +++ b/js/background.js @@ -81,6 +81,8 @@ chrome.runtime.onInstalled.addListener(function () { "tab_icons": false, "todo_hide_feedback": false, "todo_full_height": true, + "todo_progress_rings": true, + "todo_confetti": true, "device_dark": false, "cumulative_gpa": { "name": "Cumulative GPA", "hidden": false, "weight": "dnc", "credits": 999, "gr": 3.21 }, // "show_updates": false, diff --git a/js/content.js b/js/content.js index fd8fa6e..524af50 100644 --- a/js/content.js +++ b/js/content.js @@ -284,9 +284,7 @@ function createNasaInfoOverlay() { const panel = nasaInfoOverlayEl.querySelector("#nasa-info-panel"); const populatePanel = async () => { - // The NASA worker (background.js getNasaBackground) falls back up to 7 days - // when today's APOD isn't available yet, so search backward for the date - // that was actually cached and displayed instead of assuming today. + // Search backward up to 7 days for the APOD actually cached, since today's may not be ready. const date = new Date(); for (let i = 0; i < 7; i++) { const dateStr = date.toISOString().slice(0, 10); @@ -620,8 +618,7 @@ function applyOptionsChanges(changes) { loadCardAssignments(); break; case "equal_height_cards": - // No need to rebuild the assignment rows — just stretch (or reset) - // the card heights in place for a snappy toggle. + // Stretch or reset card heights in place instead of rebuilding rows. equalizeCardHeights(); break; case "custom_cards": @@ -833,7 +830,7 @@ async function applyCustomBackground() { -webkit-backdrop-filter: blur(8px) saturate(120%) !important; } #right-side-wrapper { - // backdrop-filter: blur(10px) !important; + /* backdrop-filter: blur(10px) !important; */ background-color: color-mix(in srgb, var(--bcbackground-0), transparent 35%); border-radius: 5px; } @@ -1553,6 +1550,7 @@ function buildPlannerNotePayload(form) { const courseIdRaw = form.querySelector("#better-todo-new-task-course")?.value; const dateValue = form.querySelector("#better-todo-new-task-date")?.value; const timeValue = form.querySelector("#better-todo-new-task-time")?.value; + const link = form.querySelector("#better-todo-new-task-link")?.value?.trim() || ""; if (!title) { throw new Error("Task title is required."); @@ -1570,6 +1568,7 @@ function buildPlannerNotePayload(form) { return { title, details, + link, courseId: courseIdRaw ? parseInt(courseIdRaw) : null, // Canvas accepts local timestamp strings more reliably than UTC ISO strings for planner notes. todoDate: `${dateValue}T${timeValue}:00`, @@ -1646,6 +1645,167 @@ async function createCanvasPlannerNote(payload) { throw new Error(lastError || "Canvas rejected task creation."); } +/* Custom task links: Canvas planner notes have no link field, so store a + user link in sync storage keyed by the note id. */ +function getCustomTaskLinks() { + return (options && options.custom_task_links) || {}; +} + +function getCustomTaskLinkId(item) { + return item?.plannable_id ?? item?.plannable?.id ?? null; +} + +function normalizeTaskLink(link) { + if (!link) return ""; + link = String(link).trim(); + if (!link) return ""; + if (/^https?:\/\//i.test(link)) return link; + if (link.startsWith("//")) return "https:" + link; + return domain + (link.startsWith("/") ? link : "/" + link); +} + +function customTaskHref(item) { + const id = getCustomTaskLinkId(item); + const links = getCustomTaskLinks(); + if (id != null && links[String(id)]) { + return normalizeTaskLink(links[String(id)]); + } + const courseId = item?.course_id || item?.plannable?.course_id || item?.context_id; + if (courseId) return `${domain}/courses/${courseId}`; + return `${domain}/`; +} + +function saveCustomTaskLink(noteId, link) { + if (noteId == null) return; + const links = { ...getCustomTaskLinks() }; + const key = String(noteId); + if (link && String(link).trim()) { + links[key] = String(link).trim(); + } else { + delete links[key]; + } + options = { ...options, custom_task_links: links }; + chrome.storage.sync.set({ custom_task_links: links }); +} + +function deleteCustomTaskLink(noteId) { + if (noteId == null) return; + const links = { ...getCustomTaskLinks() }; + delete links[String(noteId)]; + options = { ...options, custom_task_links: links }; + chrome.storage.sync.set({ custom_task_links: links }); +} + +async function updateCanvasPlannerNote(id, payload) { + if (!id) throw new Error("Missing task id."); + const csrfToken = CSRFtoken(); + const plannerNote = { + title: payload.title, + todo_date: payload.todoDate, + }; + if (payload.details) plannerNote.details = payload.details; + // Sending course_id as empty string disassociates the note from its course. + plannerNote.course_id = payload.courseId || ""; + + const attempts = [ + { + headers: { + "content-type": "application/json", + "accept": "application/json", + "X-CSRF-Token": csrfToken, + }, + body: JSON.stringify({ planner_note: plannerNote }), + }, + { + headers: { + "content-type": "application/x-www-form-urlencoded; charset=UTF-8", + "accept": "application/json", + "X-CSRF-Token": csrfToken, + }, + body: (() => { + const formBody = new URLSearchParams(); + formBody.set("planner_note[title]", plannerNote.title); + formBody.set("planner_note[todo_date]", plannerNote.todo_date); + if (plannerNote.details) formBody.set("planner_note[details]", plannerNote.details); + formBody.set("planner_note[course_id]", plannerNote.course_id); + return formBody.toString(); + })(), + }, + ]; + + let lastError = "Canvas rejected task update."; + for (const attempt of attempts) { + const response = await fetch(`${domain}/api/v1/planner_notes/${id}`, { + method: "PUT", + headers: attempt.headers, + body: attempt.body, + }); + + if (response.status === 200 || response.status === 201) { + return response.json(); + } + + try { + const errData = await response.json(); + if (errData?.errors?.length) { + lastError = errData.errors.join(" "); + } else if (errData?.message) { + lastError = errData.message; + } + } catch (_) { + // Keep prior error text when body is not JSON. + } + } + + throw new Error(lastError || "Canvas rejected task update."); +} + +async function deleteCanvasPlannerNote(id) { + if (!id) throw new Error("Missing task id."); + const csrfToken = CSRFtoken(); + const response = await fetch(`${domain}/api/v1/planner_notes/${id}`, { + method: "DELETE", + headers: { + "content-type": "application/json", + "accept": "application/json", + "X-CSRF-Token": csrfToken, + }, + }); + + if (response.status === 200 || response.status === 201 || response.status === 204) { + return true; + } + + let lastError = "Canvas rejected task deletion."; + try { + const errData = await response.json(); + if (errData?.errors?.length) lastError = errData.errors.join(" "); + else if (errData?.message) lastError = errData.message; + } catch (_) { /* ignore */ } + throw new Error(lastError); +} + +/* Scroll a task form field into view, leaving room below for the native date/time picker. */ +function scrollTodoIntoView(el, smooth = true) { + if (!el) return; + const sidebar = document.getElementById("right-side-wrapper"); + const style = sidebar ? getComputedStyle(sidebar) : null; + const scrollsSidebar = sidebar && + (style.overflowY === "auto" || style.overflowY === "scroll") && + sidebar.scrollHeight > sidebar.clientHeight; + const behavior = smooth ? "smooth" : "auto"; + if (scrollsSidebar) { + const rect = el.getBoundingClientRect(); + const sRect = sidebar.getBoundingClientRect(); + const elTop = rect.top - sRect.top + sidebar.scrollTop; + // Keep the element near the top so the picker below it stays on screen. + const target = elTop - (sidebar.clientHeight - rect.height) * 0.3; + sidebar.scrollTo({ top: Math.max(0, target), behavior }); + } else { + el.scrollIntoView({ block: "center", behavior }); + } +} + function fillTaskCourseOptions(courseSelect) { const cards = options.custom_cards || {}; const courseColors = options.custom_cards_3 || {}; @@ -1705,7 +1865,8 @@ function ensureTodoTaskMenu(location, feedbackElement) { }); menu.innerHTML = ` -
+
+ @@ -1713,9 +1874,13 @@ function ensureTodoTaskMenu(location, feedbackElement) {
+
- - + +
+ + +
`; @@ -1729,32 +1894,90 @@ function ensureTodoTaskMenu(location, feedbackElement) { courseSelect.addEventListener("change", () => updateTaskCourseSelectColor(courseSelect)); addTaskButton.addEventListener("click", () => { + const willOpen = !menu.classList.contains("canvasrefined-custom-open"); menu.classList.toggle("canvasrefined-custom-open"); + if (willOpen) { + resetTaskFormToCreate(menu); + // Scroll the form up so the picker stays on screen. + scrollTodoIntoView(menu, true); + } }); - menu.querySelector("#better-todo-add-task-submit").addEventListener("click", async () => { + const submitTask = async () => { const status = menu.querySelector("#better-todo-add-task-status"); const submitButton = menu.querySelector("#better-todo-add-task-submit"); + const deleteButton = menu.querySelector("#better-todo-add-task-delete"); status.textContent = ""; submitButton.disabled = true; + if (deleteButton) deleteButton.disabled = true; try { const payload = buildPlannerNotePayload(menu); - await createCanvasPlannerNote(payload); - status.textContent = "Task created."; + const editingId = menu.dataset.editingId || null; + if (editingId) { + await updateCanvasPlannerNote(editingId, payload); + saveCustomTaskLink(editingId, payload.link); + status.textContent = "Task updated."; + } else { + const created = await createCanvasPlannerNote(payload); + const newId = created?.id; + if (newId != null) saveCustomTaskLink(newId, payload.link); + status.textContent = "Task created."; + } status.style.color = "#198754"; - menu.querySelector("#better-todo-new-task-title").value = ""; - menu.querySelector("#better-todo-new-task-details").value = ""; + resetTaskFormToCreate(menu); menu.classList.remove("canvasrefined-custom-open"); getAssignments(); clearTodoList(); createTodoSections(location); } catch (e) { - status.textContent = e?.message || "Could not create task."; + status.textContent = e?.message || "Could not save task."; status.style.color = "#db3754"; } finally { submitButton.disabled = false; + if (deleteButton) deleteButton.disabled = false; + } + }; + + menu.querySelector("#better-todo-add-task-submit").addEventListener("click", submitTask); + + // Close (×) button: dismiss the form without creating/editing a task. + menu.querySelector("#better-todo-add-task-close")?.addEventListener("click", () => { + resetTaskFormToCreate(menu); + menu.classList.remove("canvasrefined-custom-open"); + }); + + // Reposition the field on focus so the picker opens on screen. + ["#better-todo-new-task-date", "#better-todo-new-task-time"].forEach((sel) => { + const input = menu.querySelector(sel); + input?.addEventListener("focus", () => scrollTodoIntoView(input, false)); + }); + + menu.querySelector("#better-todo-add-task-delete").addEventListener("click", async () => { + const editingId = menu.dataset.editingId || null; + if (!editingId) return; + if (!confirm("Delete this custom task? This cannot be undone.")) return; + const status = menu.querySelector("#better-todo-add-task-status"); + const submitButton = menu.querySelector("#better-todo-add-task-submit"); + const deleteButton = menu.querySelector("#better-todo-add-task-delete"); + status.textContent = ""; + submitButton.disabled = true; + deleteButton.disabled = true; + try { + await deleteCanvasPlannerNote(editingId); + deleteCustomTaskLink(editingId); + resetTaskFormToCreate(menu); + menu.classList.remove("canvasrefined-custom-open"); + getAssignments(); + clearTodoList(); + createTodoSections(location); + } catch (e) { + status.textContent = e?.message || "Could not delete task."; + status.style.color = "#db3754"; + } finally { + submitButton.disabled = false; + deleteButton.disabled = false; } }); } @@ -1768,6 +1991,64 @@ function ensureTodoTaskMenu(location, feedbackElement) { } } +// Reset the shared add/edit task form back to a blank "create" state. +function resetTaskFormToCreate(menu) { + if (!menu) return; + menu.querySelector("#better-todo-new-task-title").value = ""; + menu.querySelector("#better-todo-new-task-details").value = ""; + menu.querySelector("#better-todo-new-task-link").value = ""; + const courseSelect = menu.querySelector("#better-todo-new-task-course"); + if (courseSelect) { + fillTaskCourseOptions(courseSelect); + updateTaskCourseSelectColor(courseSelect); + } + const now = new Date(); + menu.querySelector("#better-todo-new-task-date").value = formatDateForInput(now); + menu.querySelector("#better-todo-new-task-time").value = formatTimeForInput(now); + const status = menu.querySelector("#better-todo-add-task-status"); + if (status) { status.textContent = ""; status.style.color = ""; } + const del = menu.querySelector("#better-todo-add-task-delete"); + if (del) del.style.display = "none"; + const submit = menu.querySelector("#better-todo-add-task-submit"); + if (submit) submit.textContent = "Create"; + delete menu.dataset.editingId; +} + +// Open the shared form pre-filled with a custom task for editing or deletion. +function openTaskForEdit(item) { + const location = document.getElementById("canvasrefined-todo-list"); + if (!location) return; + const feedbackElement = location.querySelector(".recent_feedback"); + ensureTodoTaskMenu(location, feedbackElement); + const menu = document.getElementById("better-todo-add-task-menu"); + if (!menu) return; + + const noteId = getCustomTaskLinkId(item); + menu.querySelector("#better-todo-new-task-title").value = item?.plannable?.title || ""; + menu.querySelector("#better-todo-new-task-details").value = item?.plannable?.details || ""; + const courseSelect = menu.querySelector("#better-todo-new-task-course"); + const cid = item?.course_id || item?.plannable?.course_id || ""; + if (courseSelect) courseSelect.value = cid ? String(cid) : ""; + updateTaskCourseSelectColor(courseSelect); + + const dateObj = new Date(item?.plannable_date || item?.plannable?.todo_date || Date.now()); + if (!Number.isNaN(dateObj.getTime())) { + menu.querySelector("#better-todo-new-task-date").value = formatDateForInput(dateObj); + menu.querySelector("#better-todo-new-task-time").value = formatTimeForInput(dateObj); + } + const links = getCustomTaskLinks(); + menu.querySelector("#better-todo-new-task-link").value = (noteId != null && links[String(noteId)]) || ""; + + menu.dataset.editingId = noteId != null ? String(noteId) : ""; + const del = menu.querySelector("#better-todo-add-task-delete"); + if (del) del.style.display = ""; + menu.querySelector("#better-todo-add-task-submit").textContent = "Save"; + const status = menu.querySelector("#better-todo-add-task-status"); + if (status) { status.textContent = ""; status.style.color = ""; } + menu.classList.add("canvasrefined-custom-open"); + scrollTodoIntoView(menu, true); +} + async function createTodoSections(location) { if (!location.querySelector("#better-todo-header")) { let header = makeElement("div", location, { id: "better-todo-header" }); @@ -2071,6 +2352,10 @@ function populateAssignments(iscompleted = false) { "#cccccc"; const isCustomTask = item.plannable_type == "planner_note" || item.planner_override?.custom === true; + const taskHref = isCustomTask ? customTaskHref(item) : (domain + item.html_url); + const editButtonSvg = isCustomTask + ? `` + : ""; const iconSize = isCustomTask ? 26 : 20; const iconLeftOffset = isCustomTask ? 2 : 5; const taskIcon = isCustomTask @@ -2095,9 +2380,10 @@ function populateAssignments(iscompleted = false) {
${item.context_name} - ${item.plannable.title} + ${item.plannable.title} ${convertToDueDate(item.plannable_date)}
+ ${editButtonSvg} @@ -2112,6 +2398,14 @@ function populateAssignments(iscompleted = false) { console.log("marking ", item.plannable.title, " as complete"); markAs(item, assignment.firstElementChild); }); + const editBtn = assignment.querySelector(".better-todo-assignment-edit"); + if (editBtn) { + editBtn.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + openTaskForEdit(item); + }); + } }); if (document.getElementById("better-todo-see-more")) { @@ -3056,11 +3350,7 @@ async function changeColorPreset(colors) { Dark mode */ -// Light-mode fallback values for the --bc* theme variables. These are always -// emitted (even when dark mode is OFF) so extension UI referencing -// var(--bctext-0) / var(--bcborders) / var(--bcbackground-*) still renders -// correctly in light mode (e.g. better sidebar/todo icons stay dark instead -// of falling back to the SVG initial values). Dark mode overrides these below. +// Light-mode fallbacks for the --bc* variables, always emitted so extension UI renders in light mode; dark mode overrides below. const BC_LIGHT_DEFAULTS = { "background-0": "#ffffff", "background-1": "#c7c7c7", @@ -3264,12 +3554,8 @@ function createCardAssignment(assignment) { let cardAssignments; -/* -Equal Height Cards — when enabled, every dashboard card's assignment area is -stretched to match the tallest one so cards with fewer assignments don't end -up shorter than the rest. Uses min-height (never a fixed height) so cards can -still grow if their content exceeds the tallest card. -*/ +/* Equal Height Cards: stretch each card's assignment area to match the tallest + one, using min-height so cards can still grow. */ let equalHeightResizeTimer = null; function equalizeCardHeights() { @@ -3278,8 +3564,7 @@ function equalizeCardHeights() { const enabled = options.equal_height_cards === true && options.assignments_due === true; - // Always clear any previously applied min-height first so we can either - // measure natural heights (when enabling) or fully reset (when disabling). + // Clear prior min-height so we can measure fresh or fully reset. cards.forEach(card => { const area = card.querySelector(".canvasrefined-card-assignment"); if (area) area.style.removeProperty("min-height"); @@ -3287,9 +3572,7 @@ function equalizeCardHeights() { if (!enabled) return; - // Measure the natural height of each card's assignment area, then stretch - // every area to the tallest one. Card headers are uniform, so equalizing - // the assignment area makes all cards the same total height. + // Stretch each assignment area to the tallest one. let maxHeight = 0; cards.forEach(card => { const area = card.querySelector(".canvasrefined-card-assignment"); @@ -3781,13 +4064,7 @@ function delayDashboardNotesStorage(text) { }, 250); } -/* -Built-in fallback Markdown renderer. Used only if js/markdown.js failed to load -(window.renderMarkdown missing) so the notes still render formatted output instead -of showing raw markdown text. Covers the common subset: headings, bold/italic/strike, -inline + fenced code, links, images, lists, task lists, blockquotes, hr, paragraphs. -All user text is HTML-escaped before formatting. -*/ +/* Fallback Markdown renderer used only if js/markdown.js failed to load. */ function crRenderMarkdownFallback(src) { if (src == null) return ""; const escapeHtml = (s) => String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); @@ -3804,6 +4081,7 @@ function crRenderMarkdownFallback(src) { const out = []; const lines = text.split("\n"); let i = 0; + let taskSeq = 0; // ordinal of rendered task items, stable across code-block stashing const inline = (t) => { let h = escapeHtml(t); h = h.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (m, alt, url, title) => @@ -3827,7 +4105,7 @@ function crRenderMarkdownFallback(src) { out.push(`
${q.join("
")}
`); continue; } if (/^\s*[-*+]\s+/.test(line)) { - const items = []; while (i < lines.length) { const m = lines[i].match(/^\s*[-*+]\s+(.*)$/); if (!m) break; const tk = m[1].match(/^\[([ xX])\]\s+(.*)$/); if (tk) { items.push(`
  • ${inline(tk[2])}
  • `); } else { items.push(`
  • ${inline(m[1])}
  • `); } i++; } out.push(`
      ${items.join("")}
    `); continue; + const items = []; while (i < lines.length) { const m = lines[i].match(/^\s*[-*+]\s+(.*)$/); if (!m) break; const tk = m[1].match(/^\[([ xX])\]\s+(.*)$/); if (tk) { items.push(`
  • ${inline(tk[2])}
  • `); } else { items.push(`
  • ${inline(m[1])}
  • `); } i++; } out.push(`
      ${items.join("")}
    `); continue; } if (/^\s*\d+\.\s+/.test(line)) { const items = []; while (i < lines.length) { const m = lines[i].match(/^\s*\d+\.\s+(.*)$/); if (!m) break; items.push(`
  • ${inline(m[1])}
  • `); i++; } out.push(`
      ${items.join("")}
    `); continue; @@ -3840,20 +4118,14 @@ function crRenderMarkdownFallback(src) { function renderDashboardNotesPreview(preview, text) { if (!preview) return; - // Skip identical re-renders: writing innerHTML is a childList mutation that the - // dashboard MutationObserver picks up, which re-calls loadDashboardNotes, which - // re-renders... Without this guard the notes box drives a tight self-sustaining - // loop (setTimeout 0) that starves the main thread so the render never paints. + // Skip identical re-renders to avoid a self-sustaining observer loop. if (preview._crLastText === text) return; preview._crLastText = text; const renderer = (typeof window.renderMarkdown === "function") ? window.renderMarkdown : crRenderMarkdownFallback; preview.innerHTML = renderer(text); } -/* -Insert/wrap Markdown formatting in the notes editor at the current selection. -Dispatches a synthetic `input` event so the live preview + storage handler runs. -*/ +/* Insert/wrap Markdown formatting at the selection and fire an input event. */ function notesApplyFormat(editor, action) { if (!editor) return; const start = editor.selectionStart; @@ -3947,6 +4219,34 @@ function notesApplyFormat(editor, action) { } } +/* Toggle the Nth rendered Markdown task checkbox and re-render, using a task + ordinal that skips fenced code blocks. */ +function toggleDashboardNoteTask(editor, taskIndex) { + if (!editor) return; + const value = editor.value || ""; + const lines = value.split("\n"); + let inFence = false; + let count = 0; + for (let idx = 0; idx < lines.length; idx++) { + const line = lines[idx]; + if (/^\s*```/.test(line)) { inFence = !inFence; continue; } + if (inFence) continue; + const m = line.match(/^(\s*[-*+]\s+)\[([ xX])\](\s+)/); + if (!m) continue; + if (count === taskIndex) { + const newMark = m[2] === " " ? "x" : " "; + lines[idx] = m[1] + "[" + newMark + "]" + m[3] + line.slice(m[0].length); + editor.value = lines.join("\n"); + editor.dispatchEvent(new Event("input", { bubbles: true })); + const notes = editor.closest(".canvasrefined-dashboard-notes"); + const rendered = notes ? notes.querySelector(".canvasrefined-notes-rendered") : null; + if (rendered) renderDashboardNotesPreview(rendered, editor.value); + return; + } + count++; + } +} + const DASHBOARD_NOTES_HTML = `