diff --git a/packages/editor/src/extensions.ts b/packages/editor/src/extensions.ts
index a8f1e2363..96e7f8e20 100644
--- a/packages/editor/src/extensions.ts
+++ b/packages/editor/src/extensions.ts
@@ -45,3 +45,5 @@ import "./extensions/list-item";
import "./extensions/outline-list";
import "./extensions/outline-list-item";
import "./extensions/table";
+import "./extensions/check-list";
+import "./extensions/check-list-item";
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
new file mode 100644
index 000000000..5e78c2f76
--- /dev/null
+++ b/packages/editor/src/extensions/check-list-item/check-list-item.ts
@@ -0,0 +1,208 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+import {
+ KeyboardShortcutCommand,
+ mergeAttributes,
+ Node,
+ wrappingInputRule
+} from "@tiptap/core";
+import { Node as ProseMirrorNode } from "@tiptap/pm/model";
+
+export interface CheckListItemOptions {
+ onReadOnlyChecked?: (node: ProseMirrorNode, checked: boolean) => boolean;
+ nested: boolean;
+ HTMLAttributes: Record;
+ checkListTypeName: string;
+}
+
+export const inputRegex = /^\s*(\[([( |x])?\])\s$/;
+
+export const CheckListItem = Node.create({
+ name: "checkListItem",
+
+ addOptions() {
+ return {
+ nested: false,
+ HTMLAttributes: {},
+ checkListTypeName: "checkList"
+ };
+ },
+
+ content() {
+ return this.options.nested ? "paragraph block*" : "paragraph+";
+ },
+
+ defining: true,
+
+ addAttributes() {
+ return {
+ checked: {
+ default: false,
+ keepOnSplit: false,
+ parseHTML: (element) => element.getAttribute("data-checked") === "true",
+ renderHTML: (attributes) => ({
+ "data-checked": attributes.checked
+ })
+ }
+ };
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: `li[data-type="${this.name}"]`,
+ priority: 51
+ }
+ ];
+ },
+
+ renderHTML({ node, HTMLAttributes }) {
+ return [
+ "li",
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
+ "data-type": this.name
+ }),
+ [
+ "label",
+ [
+ "input",
+ {
+ type: "checkbox",
+ checked: node.attrs.checked ? "checked" : null
+ }
+ ],
+ ["span"]
+ ],
+ ["div", 0]
+ ];
+ },
+
+ addKeyboardShortcuts() {
+ const shortcuts: {
+ [key: string]: KeyboardShortcutCommand;
+ } = {
+ Enter: () => this.editor.commands.splitListItem(this.name),
+ "Shift-Tab": () => this.editor.commands.liftListItem(this.name)
+ };
+
+ if (!this.options.nested) {
+ return shortcuts;
+ }
+
+ return {
+ ...shortcuts,
+ Tab: () => this.editor.commands.sinkListItem(this.name)
+ };
+ },
+
+ addNodeView() {
+ return ({ node, HTMLAttributes, 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.type = "checkbox";
+ checkbox.addEventListener("change", (event) => {
+ // 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()
+ .focus(undefined, { scrollIntoView: false })
+ .command(({ tr }) => {
+ const position = getPos();
+ const currentNode = tr.doc.nodeAt(position);
+
+ tr.setNodeMarkup(position, undefined, {
+ ...currentNode?.attrs,
+ checked
+ });
+
+ return true;
+ })
+ .run();
+ }
+ if (!editor.isEditable && this.options.onReadOnlyChecked) {
+ // Reset state if onReadOnlyChecked returns false
+ if (!this.options.onReadOnlyChecked(node, checked)) {
+ checkbox.checked = !checkbox.checked;
+ }
+ }
+ });
+
+ 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);
+ });
+
+ return {
+ dom: listItem,
+ contentDOM: content,
+ update: (updatedNode) => {
+ if (updatedNode.type !== this.type) {
+ return false;
+ }
+
+ listItem.dataset.checked = updatedNode.attrs.checked;
+ if (updatedNode.attrs.checked) {
+ checkbox.setAttribute("checked", "checked");
+ } else {
+ checkbox.removeAttribute("checked");
+ }
+
+ return true;
+ }
+ };
+ };
+ },
+
+ addInputRules() {
+ return [
+ wrappingInputRule({
+ find: inputRegex,
+ type: this.type,
+ getAttributes: (match) => ({
+ checked: match[match.length - 1] === "x"
+ })
+ })
+ ];
+ }
+});
diff --git a/packages/editor/src/extensions/check-list-item/index.ts b/packages/editor/src/extensions/check-list-item/index.ts
new file mode 100644
index 000000000..ddf88e3a9
--- /dev/null
+++ b/packages/editor/src/extensions/check-list-item/index.ts
@@ -0,0 +1,23 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+import { CheckListItem } from "./check-list-item";
+
+export * from "./check-list-item";
+
+export default CheckListItem;
diff --git a/packages/editor/src/extensions/check-list/check-list.ts b/packages/editor/src/extensions/check-list/check-list.ts
new file mode 100644
index 000000000..7338f4439
--- /dev/null
+++ b/packages/editor/src/extensions/check-list/check-list.ts
@@ -0,0 +1,87 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+import { mergeAttributes, Node } from "@tiptap/core";
+
+export interface CheckListOptions {
+ itemTypeName: string;
+ HTMLAttributes: Record;
+}
+
+declare module "@tiptap/core" {
+ interface Commands {
+ checkList: {
+ /**
+ * Toggle a check list
+ */
+ toggleCheckList: () => ReturnType;
+ };
+ }
+}
+
+export const CheckList = Node.create({
+ name: "checkList",
+
+ addOptions() {
+ return {
+ itemTypeName: "checkListItem",
+ HTMLAttributes: {}
+ };
+ },
+
+ group: "block list",
+
+ content() {
+ return `${this.options.itemTypeName}+`;
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: `ul[data-type="${this.name}"]`,
+ priority: 51
+ }
+ ];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return [
+ "ul",
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
+ "data-type": this.name
+ }),
+ 0
+ ];
+ },
+
+ addCommands() {
+ return {
+ toggleCheckList:
+ () =>
+ ({ commands }) => {
+ return commands.toggleList(this.name, this.options.itemTypeName);
+ }
+ };
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ "Mod-Shift-9": () => this.editor.commands.toggleCheckList()
+ };
+ }
+});
diff --git a/packages/editor/src/extensions/check-list/index.ts b/packages/editor/src/extensions/check-list/index.ts
new file mode 100644
index 000000000..42bab716e
--- /dev/null
+++ b/packages/editor/src/extensions/check-list/index.ts
@@ -0,0 +1,23 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+import { CheckList } from "./check-list";
+
+export * from "./check-list";
+
+export default CheckList;
diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts
index 3bfb5eee4..767d675a2 100644
--- a/packages/editor/src/index.ts
+++ b/packages/editor/src/index.ts
@@ -83,6 +83,8 @@ import Clipboard, { ClipboardOptions } from "./extensions/clipboard";
import Blockquote from "./extensions/blockquote";
import { Quirks } from "./extensions/quirks";
import { LIST_NODE_TYPES } from "./utils/node-types";
+import CheckList from "./extensions/check-list";
+import CheckListItem from "./extensions/check-list-item";
declare global {
// eslint-disable-next-line no-var
@@ -281,6 +283,8 @@ const useTiptap = (
DateTime.configure({ dateFormat, timeFormat }),
KeyMap,
WebClipNode,
+ CheckList,
+ CheckListItem,
// Quirks handlers
Quirks.configure({
@@ -296,6 +300,7 @@ const useTiptap = (
...LIST_NODE_TYPES
]
}),
+
ListKeymap.configure({
listTypes: [
{
@@ -309,6 +314,10 @@ const useTiptap = (
{
itemName: OutlineListItem.name,
wrapperNames: [OutlineList.name]
+ },
+ {
+ itemName: CheckListItem.name,
+ wrapperNames: [CheckList.name]
}
]
})
diff --git a/packages/editor/src/toolbar/icons.ts b/packages/editor/src/toolbar/icons.ts
index 2bd7e962c..f18a6b405 100644
--- a/packages/editor/src/toolbar/icons.ts
+++ b/packages/editor/src/toolbar/icons.ts
@@ -249,6 +249,7 @@ export const Icons = {
readonlyOff: mdiPencil,
selectAllUnchecked: mdiCheckboxMultipleBlankOutline,
selectAllChecked: mdiCheckboxMultipleMarked,
+ checkList: mdiCheckboxMarkedOutline,
none: ""
};
diff --git a/packages/editor/src/toolbar/tool-definitions.ts b/packages/editor/src/toolbar/tool-definitions.ts
index c12ff8121..5073a36bd 100644
--- a/packages/editor/src/toolbar/tool-definitions.ts
+++ b/packages/editor/src/toolbar/tool-definitions.ts
@@ -104,6 +104,10 @@ const tools: Record = {
icon: "numberedList",
title: "Numbered list"
},
+ checkList: {
+ icon: "checkList",
+ title: "Numbered list"
+ },
fontFamily: {
icon: "fontFamily",
title: "Font family"
@@ -408,7 +412,7 @@ const defaultPresets: Record<"default" | "minimal", ToolbarDefinition> = {
],
["fontSize"],
["headings", "fontFamily"],
- ["numberedList", "bulletList"],
+ ["checkList", "numberedList", "bulletList"],
["addLink"],
["alignment", "textDirection"],
["clearformatting"]
diff --git a/packages/editor/src/toolbar/tools/index.ts b/packages/editor/src/toolbar/tools/index.ts
index a87fbf771..4edfdcdb4 100644
--- a/packages/editor/src/toolbar/tools/index.ts
+++ b/packages/editor/src/toolbar/tools/index.ts
@@ -35,7 +35,7 @@ import { InsertBlock } from "./block";
import { FontSize, FontFamily } from "./font";
import { Alignment } from "./alignment";
import { Headings } from "./headings";
-import { NumberedList, BulletList, Outdent, Indent } from "./lists";
+import { NumberedList, BulletList, Outdent, Indent, CheckList } from "./lists";
import { TextDirection } from "./text-direction";
import { Highlight, TextColor } from "./colors";
import {
@@ -117,6 +117,7 @@ const tools = {
insertBlock: InsertBlock,
numberedList: NumberedList,
bulletList: BulletList,
+ checkList: CheckList,
fontSize: FontSize,
fontFamily: FontFamily,
headings: Headings,
diff --git a/packages/editor/src/toolbar/tools/lists.tsx b/packages/editor/src/toolbar/tools/lists.tsx
index c5da7a9d3..80ef91887 100644
--- a/packages/editor/src/toolbar/tools/lists.tsx
+++ b/packages/editor/src/toolbar/tools/lists.tsx
@@ -174,6 +174,18 @@ export function BulletList(props: ToolProps) {
);
}
+export function CheckList(props: ToolProps) {
+ const { editor, ...toolProps } = props;
+
+ return (
+ editor.current?.chain().focus().toggleCheckList().run()}
+ />
+ );
+}
+
export function Indent(props: ToolProps) {
const { editor, ...toolProps } = props;
const isBottom = useToolbarLocation() === "bottom";
diff --git a/packages/editor/styles/styles.css b/packages/editor/styles/styles.css
index 2dbff27c2..1a8ad1444 100644
--- a/packages/editor/styles/styles.css
+++ b/packages/editor/styles/styles.css
@@ -618,4 +618,37 @@ p > *::selection {
transform: rotate(90deg);
}
-[dir="rtl"] .taskItemTools { right: unset; left: 0 }
\ No newline at end of file
+[dir="rtl"] .taskItemTools { right: unset; left: 0 }
+
+/* Check list */
+.ProseMirror ul[data-type="checkList"] {
+ list-style: none;
+ padding: 0;
+
+ p {
+ margin: 0;
+ }
+
+ li {
+ display: flex;
+
+ > 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;
+ }
+ }
+}
\ No newline at end of file