aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCédric <42071178+BOTkirial@users.noreply.github.com>2025-11-02 22:48:38 +0100
committerGitHub <noreply@github.com>2025-11-02 21:48:38 +0000
commit393bbd9a64d1b2248ce3e17dbde6f3485140f777 (patch)
tree53ad2c08bf227d2de125c7b838a17e9903754b60
parent085c832c9ab6ada92e99a6987dcdf8bd67c3f317 (diff)
downloadkarakeep-393bbd9a64d1b2248ce3e17dbde6f3485140f777.tar.zst
feat: Support inline toggling for todos. fixes #1931 (#1933)
* [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>
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkMarkdownComponent.tsx5
-rw-r--r--apps/web/components/dashboard/bookmarks/EditorCard.tsx35
-rw-r--r--apps/web/components/ui/markdown/markdown-readonly.tsx46
3 files changed, 85 insertions, 1 deletions
diff --git a/apps/web/components/dashboard/bookmarks/BookmarkMarkdownComponent.tsx b/apps/web/components/dashboard/bookmarks/BookmarkMarkdownComponent.tsx
index 82e483a9..e7fea2c3 100644
--- a/apps/web/components/dashboard/bookmarks/BookmarkMarkdownComponent.tsx
+++ b/apps/web/components/dashboard/bookmarks/BookmarkMarkdownComponent.tsx
@@ -33,10 +33,13 @@ export function BookmarkMarkdownComponent({
text,
});
};
+
return (
<div className="h-full">
{readOnly ? (
- <MarkdownReadonly>{bookmark.content.text}</MarkdownReadonly>
+ <MarkdownReadonly onSave={onSave}>
+ {bookmark.content.text}
+ </MarkdownReadonly>
) : (
<MarkdownEditor onSave={onSave} isSaving={isPending}>
{bookmark.content.text}
diff --git a/apps/web/components/dashboard/bookmarks/EditorCard.tsx b/apps/web/components/dashboard/bookmarks/EditorCard.tsx
index 7ac1cade..b80cd889 100644
--- a/apps/web/components/dashboard/bookmarks/EditorCard.tsx
+++ b/apps/web/components/dashboard/bookmarks/EditorCard.tsx
@@ -172,6 +172,35 @@ export default function EditorCard({ className }: { className?: string }) {
}
};
+ /**
+ * Methods that triggers when "enter" is pressed (without ctrl)
+ * It checks if the current line is a todo
+ * if it is it automatically appends a todo a the start of the new line
+ */
+ const handleNewTodo = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
+ const todoMarkup = "- [ ] ";
+ const textarea = inputRef.current;
+ if (!textarea) return;
+ const start = textarea.selectionStart;
+ const end = textarea.selectionEnd;
+ const textBefore = textarea.value.slice(0, start);
+ const lines = textBefore.split("\n");
+ const currentLine = lines[lines.length - 1];
+ const currentLineIsTodo = currentLine.startsWith(todoMarkup);
+ if (!currentLineIsTodo) return;
+ e.preventDefault();
+ const newValue =
+ textarea.value.slice(0, start) +
+ "\n" +
+ todoMarkup +
+ textarea.value.slice(end);
+ form.setValue("text", newValue, { shouldDirty: true, shouldTouch: true });
+ textarea.value = newValue;
+ textarea.selectionStart = start + todoMarkup.length + 1;
+ textarea.selectionEnd = start + todoMarkup.length + 1;
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
+ };
+
const OS = getOS();
return (
@@ -205,6 +234,12 @@ export default function EditorCard({ className }: { className?: string }) {
if (demoMode) {
return;
}
+ if (
+ e.key === "Enter" &&
+ !(e.metaKey || e.ctrlKey || e.shiftKey)
+ ) {
+ handleNewTodo(e);
+ }
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
form.handleSubmit(onSubmit, onError)();
}
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} />;
},