From 4ea4bbd782210abbdf7bba29fcecea4ef776aec4 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Thu, 21 Dec 2023 11:08:54 +0500 Subject: [PATCH] editor: add support for simple checklists --- packages/core/package-lock.json | 2 + packages/core/src/content-types/tiptap.ts | 5 +- .../check-list-item/check-list-item.ts | 47 +++++-------------- .../src/extensions/check-list/check-list.ts | 4 +- .../src/extensions/task-item/task-item.ts | 11 +---- .../src/extensions/task-list/task-list.ts | 8 +--- packages/editor/src/index.ts | 4 +- .../editor/src/toolbar/tool-definitions.ts | 4 +- packages/editor/src/utils/prosemirror.ts | 5 ++ packages/editor/styles/styles.css | 39 ++++++--------- 10 files changed, 44 insertions(+), 85 deletions(-) diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json index c837c688c..c0887c9d5 100644 --- a/packages/core/package-lock.json +++ b/packages/core/package-lock.json @@ -2214,6 +2214,8 @@ }, "node_modules/@streetwriters/showdown": { "version": "3.0.5-alpha", + "resolved": "https://registry.npmjs.org/@streetwriters/showdown/-/showdown-3.0.5-alpha.tgz", + "integrity": "sha512-jD9JFhxLDx6XeyZOLVB0zWtwGduwNiFpxn5rxu6ThyKyWGnu1O+L1w04WLC1L56pyEhypr3Tsk24dzo2Se/50g==", "license": "MIT", "bin": { "showdown": "bin/showdown.js" diff --git a/packages/core/src/content-types/tiptap.ts b/packages/core/src/content-types/tiptap.ts index 758ffcec9..a81251b53 100644 --- a/packages/core/src/content-types/tiptap.ts +++ b/packages/core/src/content-types/tiptap.ts @@ -59,11 +59,12 @@ export class Tiptap { preserveNewlines: true, selectors: [ { selector: "table", format: "dataTable" }, - { selector: "ul.checklist", format: "taskList" }, + { selector: "ul.checklist", format: "checkList" }, + { selector: "ul.simple-checklist", format: "checkList" }, { selector: "p", format: "paragraph" } ], formatters: { - taskList: (elem, walk, builder, formatOptions) => { + checkList: (elem, walk, builder, formatOptions) => { return formatList(elem, walk, builder, formatOptions, (elem) => { return elem.attribs.class && elem.attribs.class.includes("checked") ? " ✅ " 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 5e78c2f76..8873a3143 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 @@ -28,7 +28,6 @@ export interface CheckListItemOptions { onReadOnlyChecked?: (node: ProseMirrorNode, checked: boolean) => boolean; nested: boolean; HTMLAttributes: Record; - checkListTypeName: string; } export const inputRegex = /^\s*(\[([( |x])?\])\s$/; @@ -39,8 +38,7 @@ export const CheckListItem = Node.create({ addOptions() { return { nested: false, - HTMLAttributes: {}, - checkListTypeName: "checkList" + HTMLAttributes: {} }; }, @@ -55,9 +53,9 @@ export const CheckListItem = Node.create({ checked: { default: false, keepOnSplit: false, - parseHTML: (element) => element.getAttribute("data-checked") === "true", + parseHTML: (element) => element.classList.contains("checked"), renderHTML: (attributes) => ({ - "data-checked": attributes.checked + class: attributes.checked ? "checked" : "" }) } }; @@ -66,30 +64,19 @@ export const CheckListItem = Node.create({ parseHTML() { return [ { - tag: `li[data-type="${this.name}"]`, + tag: `li.simple-checklist--item`, priority: 51 } ]; }, - renderHTML({ node, HTMLAttributes }) { + renderHTML({ HTMLAttributes }) { return [ "li", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { - "data-type": this.name + class: "simple-checklist--item" }), - [ - "label", - [ - "input", - { - type: "checkbox", - checked: node.attrs.checked ? "checked" : null - } - ], - ["span"] - ], - ["div", 0] + 0 ]; }, @@ -112,16 +99,15 @@ export const CheckListItem = Node.create({ }, addNodeView() { - return ({ node, HTMLAttributes, getPos, editor }) => { + 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 content = document.createElement("div"); - checkboxWrapper.contentEditable = "false"; + checkbox.contentEditable = "false"; checkbox.type = "checkbox"; checkbox.addEventListener("change", (event) => { + event.preventDefault(); // 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) { @@ -135,7 +121,6 @@ export const CheckListItem = Node.create({ if (editor.isEditable && typeof getPos === "function") { editor .chain() - .focus(undefined, { scrollIntoView: false }) .command(({ tr }) => { const position = getPos(); const currentNode = tr.doc.nodeAt(position); @@ -157,21 +142,11 @@ export const CheckListItem = Node.create({ } }); - Object.entries(this.options.HTMLAttributes).forEach(([key, value]) => { - listItem.setAttribute(key, value); - }); - - listItem.dataset.checked = node.attrs.checked; if (node.attrs.checked) { checkbox.setAttribute("checked", "checked"); } - checkboxWrapper.append(checkbox, checkboxStyler); - listItem.append(checkboxWrapper, content); - - Object.entries(HTMLAttributes).forEach(([key, value]) => { - listItem.setAttribute(key, value); - }); + listItem.append(checkbox, content); return { dom: listItem, diff --git a/packages/editor/src/extensions/check-list/check-list.ts b/packages/editor/src/extensions/check-list/check-list.ts index 7338f4439..d7a904358 100644 --- a/packages/editor/src/extensions/check-list/check-list.ts +++ b/packages/editor/src/extensions/check-list/check-list.ts @@ -53,7 +53,7 @@ export const CheckList = Node.create({ parseHTML() { return [ { - tag: `ul[data-type="${this.name}"]`, + tag: `ul.simple-checklist`, priority: 51 } ]; @@ -63,7 +63,7 @@ export const CheckList = Node.create({ return [ "ul", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { - "data-type": this.name + class: "simple-checklist" }), 0 ]; diff --git a/packages/editor/src/extensions/task-item/task-item.ts b/packages/editor/src/extensions/task-item/task-item.ts index 16f5a8846..2de01a65c 100644 --- a/packages/editor/src/extensions/task-item/task-item.ts +++ b/packages/editor/src/extensions/task-item/task-item.ts @@ -55,16 +55,7 @@ export const TaskItemNode = TaskItem.extend({ parseHTML() { return [ { - tag: "li", - getAttrs: (node: any) => { - if (node instanceof Node && node instanceof HTMLElement) { - return node.classList.contains("checklist--item") || - node.parentElement?.classList.contains("checklist") - ? null - : false; - } - return false; - }, + tag: "li.checklist--item", priority: 51 } ]; diff --git a/packages/editor/src/extensions/task-list/task-list.ts b/packages/editor/src/extensions/task-list/task-list.ts index 6b7516e93..a858970cf 100644 --- a/packages/editor/src/extensions/task-list/task-list.ts +++ b/packages/editor/src/extensions/task-list/task-list.ts @@ -89,13 +89,7 @@ export const TaskListNode = TaskList.extend({ parseHTML() { return [ { - tag: "ul", - getAttrs: (node) => { - if (node instanceof Node && node instanceof HTMLElement) { - return node.classList.contains("checklist") && null; - } - return false; - }, + tag: "ul.checklist", priority: 51 } ]; diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 767d675a2..3cd60e3c6 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -284,7 +284,9 @@ const useTiptap = ( KeyMap, WebClipNode, CheckList, - CheckListItem, + CheckListItem.configure({ + nested: true + }), // Quirks handlers Quirks.configure({ diff --git a/packages/editor/src/toolbar/tool-definitions.ts b/packages/editor/src/toolbar/tool-definitions.ts index 5073a36bd..bd21e1608 100644 --- a/packages/editor/src/toolbar/tool-definitions.ts +++ b/packages/editor/src/toolbar/tool-definitions.ts @@ -105,8 +105,8 @@ const tools: Record = { title: "Numbered list" }, checkList: { - icon: "checkList", - title: "Numbered list" + icon: "checklist", + title: "Checklist" }, fontFamily: { icon: "fontFamily", diff --git a/packages/editor/src/utils/prosemirror.ts b/packages/editor/src/utils/prosemirror.ts index a10e170a4..845820576 100644 --- a/packages/editor/src/utils/prosemirror.ts +++ b/packages/editor/src/utils/prosemirror.ts @@ -43,6 +43,8 @@ import { TaskItemNode } from "../extensions/task-item"; import { TaskListNode } from "../extensions/task-list"; import { LIST_NODE_TYPES } from "./node-types"; import TextStyle from "@tiptap/extension-text-style"; +import CheckList from "../extensions/check-list"; +import CheckListItem from "../extensions/check-list-item"; export type NodeWithOffset = { node?: ProsemirrorNode; @@ -61,6 +63,7 @@ export function hasSameAttributes(prev: Attrs, next: Attrs) { export function findListItemType(editor: Editor): string | null { const isTaskList = editor.isActive(TaskListNode.name); + const isCheckList = editor.isActive(CheckList.name); const isOutlineList = editor.isActive(OutlineList.name); const isList = editor.isActive(BulletList.name) || editor.isActive(OrderedList.name); @@ -71,6 +74,8 @@ export function findListItemType(editor: Editor): string | null { ? OutlineListItem.name : isTaskList ? TaskItemNode.name + : isCheckList + ? CheckListItem.name : null; } diff --git a/packages/editor/styles/styles.css b/packages/editor/styles/styles.css index 1a8ad1444..6532d7c26 100644 --- a/packages/editor/styles/styles.css +++ b/packages/editor/styles/styles.css @@ -621,34 +621,23 @@ p > *::selection { [dir="rtl"] .taskItemTools { right: unset; left: 0 } /* Check list */ -.ProseMirror ul[data-type="checkList"] { +.ProseMirror ul.simple-checklist { list-style: none; padding: 0; +} - p { - margin: 0; - } +.ProseMirror ul.simple-checklist > li { + display: flex; +} - li { - display: flex; +.ProseMirror ul.simple-checklist > li > input { + flex: 0 0 auto; + margin-right: 0.5rem; + user-select: none; + height: 18px; + accent-color: var(--accent); +} - > label { - flex: 0 0 auto; - margin-right: 0.5rem; - user-select: none; - } - - > div { - flex: 1 1 auto; - } - - ul li, - ol li { - display: list-item; - } - - ul[data-type="checkList"] > li { - display: flex; - } - } +.ProseMirror ul.simple-checklist > li > div { + flex: 1 1 auto; } \ No newline at end of file