From b01d1fc87e760d0f9d330b5079c6a8f660fcf814 Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Wed, 10 Sep 2025 10:07:19 +0500 Subject: [PATCH 1/4] editor: improve checklist styling Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> --- .../check-list-item/check-list-item.ts | 40 ++++++++------ packages/editor/styles/styles.css | 55 +++++++++++++++---- 2 files changed, 66 insertions(+), 29 deletions(-) diff --git a/packages/editor/src/extensions/check-list-item/check-list-item.ts b/packages/editor/src/extensions/check-list-item/check-list-item.ts index 6f9b97a24..41fd001b7 100644 --- a/packages/editor/src/extensions/check-list-item/check-list-item.ts +++ b/packages/editor/src/extensions/check-list-item/check-list-item.ts @@ -98,32 +98,36 @@ export const CheckListItem = Node.create({ addNodeView() { return ({ node, getPos, editor }) => { const listItem = document.createElement("li"); - const checkboxWrapper = document.createElement("label"); - const checkboxStyler = document.createElement("span"); - const checkbox = document.createElement("input"); + const checkboxWrapper = document.createElement("div"); const content = document.createElement("div"); checkboxWrapper.contentEditable = "false"; - checkbox.type = "checkbox"; + checkboxWrapper.className = "checkbox-wrapper"; + checkboxWrapper.innerHTML = ` + + + + `; - checkbox.addEventListener("mousedown", (event) => { + content.className = "checklist-item-content"; + + checkboxWrapper.addEventListener("mousedown", (event) => { if (globalThis.keyboardShown) { event.preventDefault(); } }); - checkbox.addEventListener("change", (event) => { + checkboxWrapper.addEventListener("click", (event) => { event.preventDefault(); - // if the editor isn’t editable and we don't have a handler for + + const isChecked = checkboxWrapper.classList.contains("checked"); + + // if the editor isn't editable and we don't have a handler for // readonly checks we have to undo the latest change if (!editor.isEditable && !this.options.onReadOnlyChecked) { - checkbox.checked = !checkbox.checked; - return; } - const { checked } = event.target as any; - if (editor.isEditable && typeof getPos === "function") { editor .chain() @@ -133,7 +137,7 @@ export const CheckListItem = Node.create({ tr.setNodeMarkup(position, undefined, { ...currentNode?.attrs, - checked + checked: !isChecked }); return true; @@ -142,17 +146,17 @@ export const CheckListItem = Node.create({ } if (!editor.isEditable && this.options.onReadOnlyChecked) { // Reset state if onReadOnlyChecked returns false - if (!this.options.onReadOnlyChecked(node, checked)) { - checkbox.checked = !checkbox.checked; + if (!this.options.onReadOnlyChecked(node, !isChecked)) { + return; } } }); if (node.attrs.checked) { - checkbox.setAttribute("checked", "checked"); + checkboxWrapper.classList.add("checked"); + listItem.dataset.checked = node.attrs.checked; } - checkboxWrapper.append(checkbox, checkboxStyler); listItem.append(checkboxWrapper, content); return { @@ -165,9 +169,9 @@ export const CheckListItem = Node.create({ listItem.dataset.checked = updatedNode.attrs.checked; if (updatedNode.attrs.checked) { - checkbox.setAttribute("checked", "checked"); + checkboxWrapper.classList.add("checked"); } else { - checkbox.removeAttribute("checked"); + checkboxWrapper.classList.remove("checked"); } return true; diff --git a/packages/editor/styles/styles.css b/packages/editor/styles/styles.css index 3ff2ec936..f71f27118 100644 --- a/packages/editor/styles/styles.css +++ b/packages/editor/styles/styles.css @@ -643,24 +643,57 @@ p > *::selection { /* Check list */ .ProseMirror ul.simple-checklist { list-style: none; - padding: 0; + margin-left: -25px; } .ProseMirror ul.simple-checklist > li { display: flex; + align-items: center; } -.ProseMirror ul.simple-checklist > li input { - flex: 0 0 auto; - margin-right: 0.5rem; - user-select: none; - height: 1rem; - width: 1rem; - accent-color: var(--accent); -} - -.ProseMirror ul.simple-checklist > li > div { +.ProseMirror ul.simple-checklist > li > div.checklist-item-content { flex: 1 1 auto; + display: flex; + justify-content: center; + flex-direction: column; +} + +.ProseMirror ul.simple-checklist > li[data-checked="true"] > div.checklist-item-content { + opacity: 0.6; +} + +.ProseMirror ul.simple-checklist > li > div.checkbox-wrapper { + border: 2px solid var(--icon); + width: 0.9rem; + height: 0.9rem; + border-radius: 4px; + margin-right: 7px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 250ms cubic-bezier(1, 0, .37, .91); + background-color: transparent; +} + +.ProseMirror ul.simple-checklist > li > div.checkbox-wrapper:hover { + border-color: var(--accent); +} + +.ProseMirror ul.simple-checklist > li > div.checkbox-wrapper.checked { + border-color: var(--accent); +} + +.ProseMirror ul.simple-checklist > li > div.checkbox-wrapper svg { + width: 1rem; + height: 1rem; + fill: var(--accent); + opacity: 0; + transition: opacity 250ms cubic-bezier(1, 0, .37, .91); +} + +.ProseMirror ul.simple-checklist > li > div.checkbox-wrapper.checked svg { + opacity: 1; } @media screen and (max-width: 480px) { From 0ba25419f320cfc5dd8334e697bb3033c6190f87 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Thu, 16 Oct 2025 14:43:51 +0500 Subject: [PATCH 2/4] editor: use single li element for simple checklists --- .../check-list-item/check-list-item.ts | 128 ++++++++---------- packages/editor/styles/styles.css | 91 ++++++------- 2 files changed, 98 insertions(+), 121 deletions(-) diff --git a/packages/editor/src/extensions/check-list-item/check-list-item.ts b/packages/editor/src/extensions/check-list-item/check-list-item.ts index 41fd001b7..ac7aed0a2 100644 --- a/packages/editor/src/extensions/check-list-item/check-list-item.ts +++ b/packages/editor/src/extensions/check-list-item/check-list-item.ts @@ -17,8 +17,14 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ import { keybindings } from "@notesnook/common"; -import { KeyboardShortcutCommand, mergeAttributes, Node } from "@tiptap/core"; +import { + findParentNodeClosestToPos, + KeyboardShortcutCommand, + mergeAttributes, + Node +} from "@tiptap/core"; import { Node as ProseMirrorNode } from "@tiptap/pm/model"; +import { CheckList } from "../check-list/check-list"; export interface CheckListItemOptions { onReadOnlyChecked?: (node: ProseMirrorNode, checked: boolean) => boolean; @@ -97,98 +103,78 @@ export const CheckListItem = Node.create({ addNodeView() { return ({ node, getPos, editor }) => { - const listItem = document.createElement("li"); - const checkboxWrapper = document.createElement("div"); - const content = document.createElement("div"); + const isNested = node.lastChild?.type.name === CheckList.name; - checkboxWrapper.contentEditable = "false"; - checkboxWrapper.className = "checkbox-wrapper"; - checkboxWrapper.innerHTML = ` - - - - `; + const li = document.createElement("li"); + if (node.attrs.checked) li.classList.add("checked"); + else li.classList.remove("checked"); - content.className = "checklist-item-content"; + function onClick(e: MouseEvent | TouchEvent) { + if (e instanceof MouseEvent && e.button !== 0) return; + if (!(e.target instanceof HTMLElement)) return; - checkboxWrapper.addEventListener("mousedown", (event) => { - if (globalThis.keyboardShown) { - event.preventDefault(); - } - }); + const pos = typeof getPos === "function" ? getPos() : 0; + if (typeof pos !== "number") return; + const resolvedPos = editor.state.doc.resolve(pos); - checkboxWrapper.addEventListener("click", (event) => { - event.preventDefault(); + const { x, y, right } = li.getBoundingClientRect(); - const isChecked = checkboxWrapper.classList.contains("checked"); + const clientX = + e instanceof MouseEvent ? e.clientX : e.touches[0].clientX; - // if the editor isn't editable and we don't have a handler for - // readonly checks we have to undo the latest change - if (!editor.isEditable && !this.options.onReadOnlyChecked) { - return; + const clientY = + e instanceof MouseEvent ? e.clientY : e.touches[0].clientY; + + const hitArea = { width: 40, height: 40 }; + + const isRtl = + e.target.dir === "rtl" || + findParentNodeClosestToPos( + resolvedPos, + (node) => !!node.attrs.textDirection + )?.node.attrs.textDirection === "rtl"; + + let xStart = clientX >= x - hitArea.width; + let xEnd = clientX <= x; + const yStart = clientY >= y; + const yEnd = clientY <= y + hitArea.height; + + if (isRtl) { + xEnd = clientX <= right + hitArea.width; + xStart = clientX >= right; } - if (editor.isEditable && typeof getPos === "function") { - editor - .chain() - .command(({ tr }) => { - const position = getPos(); - const currentNode = tr.doc.nodeAt(position); - - tr.setNodeMarkup(position, undefined, { - ...currentNode?.attrs, - checked: !isChecked - }); - - return true; - }) - .run(); + if (xStart && xEnd && yStart && yEnd) { + e.preventDefault(); + editor.commands.command(({ tr }) => { + tr.setNodeAttribute( + pos, + "checked", + !li.classList.contains("checked") + ); + return true; + }); } - if (!editor.isEditable && this.options.onReadOnlyChecked) { - // Reset state if onReadOnlyChecked returns false - if (!this.options.onReadOnlyChecked(node, !isChecked)) { - return; - } - } - }); - - if (node.attrs.checked) { - checkboxWrapper.classList.add("checked"); - listItem.dataset.checked = node.attrs.checked; } - listItem.append(checkboxWrapper, content); + li.onmousedown = onClick; + li.ontouchstart = onClick; return { - dom: listItem, - contentDOM: content, + dom: li, + contentDOM: li, update: (updatedNode) => { if (updatedNode.type !== this.type) { return false; } + const isNested = updatedNode.lastChild?.type.name === CheckList.name; - listItem.dataset.checked = updatedNode.attrs.checked; - if (updatedNode.attrs.checked) { - checkboxWrapper.classList.add("checked"); - } else { - checkboxWrapper.classList.remove("checked"); - } + if (updatedNode.attrs.checked) li.classList.add("checked"); + else li.classList.remove("checked"); return true; } }; }; } - - // addInputRules() { - // return [ - // wrappingInputRule({ - // find: inputRegex, - // type: this.type, - // getAttributes: (match) => ({ - // checked: match[match.length - 1] === "x" - // }) - // }) - // ]; - // } }); diff --git a/packages/editor/styles/styles.css b/packages/editor/styles/styles.css index f71f27118..92e74d5da 100644 --- a/packages/editor/styles/styles.css +++ b/packages/editor/styles/styles.css @@ -76,6 +76,14 @@ margin-bottom: 5px; } +.ProseMirror li:last-of-type { + margin-bottom: 0px; +} + +.ProseMirror li:first-of-type { + margin-top: 5px; +} + .ProseMirror ul.tasklist-content-wrapper { padding-left: 0px; } @@ -643,68 +651,51 @@ p > *::selection { /* Check list */ .ProseMirror ul.simple-checklist { list-style: none; - margin-left: -25px; + margin-block: 0px !important; + padding-inline: 0px !important; + padding-inline-start: 24px !important; } -.ProseMirror ul.simple-checklist > li { - display: flex; - align-items: center; +.ProseMirror li.nested > ul.simple-checklist { + padding-inline-start: 15px !important; } -.ProseMirror ul.simple-checklist > li > div.checklist-item-content { - flex: 1 1 auto; - display: flex; - justify-content: center; - flex-direction: column; +.simple-checklist li { + position: relative; } -.ProseMirror ul.simple-checklist > li[data-checked="true"] > div.checklist-item-content { - opacity: 0.6; -} - -.ProseMirror ul.simple-checklist > li > div.checkbox-wrapper { - border: 2px solid var(--icon); - width: 0.9rem; - height: 0.9rem; - border-radius: 4px; - margin-right: 7px; - display: inline-flex; - align-items: center; - justify-content: center; +.simple-checklist > li::after { + position: absolute; + top: 0px; cursor: pointer; - transition: all 250ms cubic-bezier(1, 0, .37, .91); - background-color: transparent; + content: ""; + background-size: 18px; + width: 14px; + height: 14px; + + border: 2px solid var(--icon); + border-radius: 5px; + left: -24px; } -.ProseMirror ul.simple-checklist > li > div.checkbox-wrapper:hover { - border-color: var(--accent); +.simple-checklist > li.checked::after { + border: 2px solid var(--accent); } -.ProseMirror ul.simple-checklist > li > div.checkbox-wrapper.checked { - border-color: var(--accent); -} +.simple-checklist > li.checked::before { + position: absolute; + top: 2px; + cursor: pointer; + content: ""; + background-size: 18px; + width: 14px; + height: 14px; + left: -22px; -.ProseMirror ul.simple-checklist > li > div.checkbox-wrapper svg { - width: 1rem; - height: 1rem; - fill: var(--accent); - opacity: 0; - transition: opacity 250ms cubic-bezier(1, 0, .37, .91); -} - -.ProseMirror ul.simple-checklist > li > div.checkbox-wrapper.checked svg { - opacity: 1; -} - -@media screen and (max-width: 480px) { - .ProseMirror ul.simple-checklist > li > input { - height: 21px; - width: 21px; - } - - .ProseMirror ul.simple-checklist > li > div { - margin-top: 2px; - } + background-color: var(--accent); + mask: url(data:image/svg+xml;base64,ICA8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICAgIDxwYXRoIGQ9Ik0yMSw3TDksMTlMMy41LDEzLjVMNC45MSwxMi4wOUw5LDE2LjE3TDE5LjU5LDUuNTlMMjEsN1oiLz4KICA8L3N2Zz4K) + no-repeat 50% 50%; + mask-size: cover; } /* Callout */ From cc9b2dd5b0b2073b792f51a0ed006296210e8784 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Thu, 16 Oct 2025 14:45:40 +0500 Subject: [PATCH 3/4] editor: add back line through for checked items --- packages/editor/styles/styles.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/editor/styles/styles.css b/packages/editor/styles/styles.css index 92e74d5da..1ebbd8d06 100644 --- a/packages/editor/styles/styles.css +++ b/packages/editor/styles/styles.css @@ -698,6 +698,11 @@ p > *::selection { mask-size: cover; } +.simple-checklist > li.checked > p { + opacity: 0.8; + text-decoration-line: line-through; +} + /* Callout */ .ProseMirror div.callout { padding: 15px; From 436e09c2557021c61d2a2179e7095901c66d3dcd Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Thu, 16 Oct 2025 14:50:13 +0500 Subject: [PATCH 4/4] editor: fix text direction not working in check lists --- .../src/extensions/check-list-item/check-list-item.ts | 2 -- .../src/extensions/text-direction/text-direction.ts | 1 + packages/editor/styles/styles.css | 10 ++++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/extensions/check-list-item/check-list-item.ts b/packages/editor/src/extensions/check-list-item/check-list-item.ts index ac7aed0a2..e7e277d58 100644 --- a/packages/editor/src/extensions/check-list-item/check-list-item.ts +++ b/packages/editor/src/extensions/check-list-item/check-list-item.ts @@ -103,8 +103,6 @@ export const CheckListItem = Node.create({ addNodeView() { return ({ node, getPos, editor }) => { - const isNested = node.lastChild?.type.name === CheckList.name; - const li = document.createElement("li"); if (node.attrs.checked) li.classList.add("checked"); else li.classList.remove("checked"); diff --git a/packages/editor/src/extensions/text-direction/text-direction.ts b/packages/editor/src/extensions/text-direction/text-direction.ts index 9547eb22b..bbc67a1eb 100644 --- a/packages/editor/src/extensions/text-direction/text-direction.ts +++ b/packages/editor/src/extensions/text-direction/text-direction.ts @@ -30,6 +30,7 @@ const TEXT_DIRECTION_TYPES = [ "orderedList", "bulletList", "outlineList", + "checkList", "taskList", "table", "blockquote", diff --git a/packages/editor/styles/styles.css b/packages/editor/styles/styles.css index 1ebbd8d06..bd9bd05f1 100644 --- a/packages/editor/styles/styles.css +++ b/packages/editor/styles/styles.css @@ -646,6 +646,16 @@ p > *::selection { transform: rotate(90deg); } +.simple-checklist[dir="rtl"] li::after { + left: unset; + right: -24px; +} + +.simple-checklist[dir="rtl"] li.checked::before { + left: unset; + right: -22px; +} + [dir="rtl"] .taskItemTools { right: unset; left: 0 } /* Check list */