From 0ba25419f320cfc5dd8334e697bb3033c6190f87 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Thu, 16 Oct 2025 14:43:51 +0500 Subject: [PATCH] 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 */