From 393bbd9a64d1b2248ce3e17dbde6f3485140f777 Mon Sep 17 00:00:00 2001
From: Cédric <42071178+BOTkirial@users.noreply.github.com>
Date: Sun, 2 Nov 2025 22:48:38 +0100
Subject: feat: Support inline toggling for todos. fixes #1931 (#1933)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* [1931] Can now chain the creation of todos from the quick add form

* [1931] Can now toggle todos from the masonry view + added a custom renderer for inputs of type checkbox (required to remove the readonly default attribute)

* handle nested lists and case

---------

Co-authored-by: Cédric <cedric.marinot@elosi.com>
Co-authored-by: Mohamed Bassem <me@mbassem.com>
---
 .../components/ui/markdown/markdown-readonly.tsx   | 46 ++++++++++++++++++++++
 1 file changed, 46 insertions(+)

(limited to 'apps/web/components/ui/markdown/markdown-readonly.tsx')

diff --git a/apps/web/components/ui/markdown/markdown-readonly.tsx b/apps/web/components/ui/markdown/markdown-readonly.tsx
index 3c6daf31..5436e961 100644
--- a/apps/web/components/ui/markdown/markdown-readonly.tsx
+++ b/apps/web/components/ui/markdown/markdown-readonly.tsx
@@ -25,15 +25,61 @@ function PreWithCopyBtn({ className, ...props }: React.ComponentProps<"pre">) {
 export function MarkdownReadonly({
   children: markdown,
   className,
+  onSave,
 }: {
   children: string;
   className?: string;
+  onSave?: (markdown: string) => void;
 }) {
+  /**
+   * This method is triggered when a checkbox is toggled from the masonry view
+   * It finds the index of the clicked checkbox inside of the note
+   * It then finds the corresponding markdown and changes it accordingly
+   */
+  const handleTodoClick = (e: React.ChangeEvent<HTMLInputElement>) => {
+    e.preventDefault();
+    const parent = e.target.closest(".prose");
+    if (!parent) return;
+    const allCheckboxes = parent.querySelectorAll(".todo-checkbox");
+    let checkboxIndex = 0;
+    allCheckboxes.forEach((cb, i) => {
+      if (cb === e.target) checkboxIndex = i;
+    });
+    let i = 0;
+    const todoPattern = /^(\s*[-*+]\s*\[)( |x|X)(\])/gm;
+    const newMarkdown = markdown.replace(
+      todoPattern,
+      (match, prefix: string, state: string, suffix: string) => {
+        const currentIndex = i++;
+        if (currentIndex !== checkboxIndex) {
+          return match;
+        }
+        const isDone = state.toLowerCase() === "x";
+        const nextState = isDone ? " " : "x";
+        return `${prefix}${nextState}${suffix}`;
+      },
+    );
+    if (onSave) {
+      onSave(newMarkdown);
+    }
+  };
+
   return (
     <Markdown
       remarkPlugins={[remarkGfm, remarkBreaks]}
       className={cn("prose dark:prose-invert", className)}
       components={{
+        input: (props) =>
+          props.type === "checkbox" ? (
+            <input
+              checked={props.checked}
+              onChange={handleTodoClick}
+              type="checkbox"
+              className="todo-checkbox"
+            />
+          ) : (
+            <input {...props} readOnly />
+          ),
         pre({ ...props }) {
           return <PreWithCopyBtn {...props} />;
         },
-- 
cgit v1.2.3-70-g09d2

