mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 19:49:54 +02:00
editor: implement simple checklists
This commit is contained in:
committed by
Abdullah Atta
parent
85c812f271
commit
4b9f284ed9
@@ -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";
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<string, any>;
|
||||
checkListTypeName: string;
|
||||
}
|
||||
|
||||
export const inputRegex = /^\s*(\[([( |x])?\])\s$/;
|
||||
|
||||
export const CheckListItem = Node.create<CheckListItemOptions>({
|
||||
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"
|
||||
})
|
||||
})
|
||||
];
|
||||
}
|
||||
});
|
||||
23
packages/editor/src/extensions/check-list-item/index.ts
Normal file
23
packages/editor/src/extensions/check-list-item/index.ts
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { CheckListItem } from "./check-list-item";
|
||||
|
||||
export * from "./check-list-item";
|
||||
|
||||
export default CheckListItem;
|
||||
87
packages/editor/src/extensions/check-list/check-list.ts
Normal file
87
packages/editor/src/extensions/check-list/check-list.ts
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { mergeAttributes, Node } from "@tiptap/core";
|
||||
|
||||
export interface CheckListOptions {
|
||||
itemTypeName: string;
|
||||
HTMLAttributes: Record<string, any>;
|
||||
}
|
||||
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
checkList: {
|
||||
/**
|
||||
* Toggle a check list
|
||||
*/
|
||||
toggleCheckList: () => ReturnType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const CheckList = Node.create<CheckListOptions>({
|
||||
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()
|
||||
};
|
||||
}
|
||||
});
|
||||
23
packages/editor/src/extensions/check-list/index.ts
Normal file
23
packages/editor/src/extensions/check-list/index.ts
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { CheckList } from "./check-list";
|
||||
|
||||
export * from "./check-list";
|
||||
|
||||
export default CheckList;
|
||||
@@ -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]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -249,6 +249,7 @@ export const Icons = {
|
||||
readonlyOff: mdiPencil,
|
||||
selectAllUnchecked: mdiCheckboxMultipleBlankOutline,
|
||||
selectAllChecked: mdiCheckboxMultipleMarked,
|
||||
checkList: mdiCheckboxMarkedOutline,
|
||||
none: ""
|
||||
};
|
||||
|
||||
|
||||
@@ -104,6 +104,10 @@ const tools: Record<ToolId, ToolDefinition> = {
|
||||
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"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -174,6 +174,18 @@ export function BulletList(props: ToolProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export function CheckList(props: ToolProps) {
|
||||
const { editor, ...toolProps } = props;
|
||||
|
||||
return (
|
||||
<ToolButton
|
||||
{...toolProps}
|
||||
toggled={false}
|
||||
onClick={() => editor.current?.chain().focus().toggleCheckList().run()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Indent(props: ToolProps) {
|
||||
const { editor, ...toolProps } = props;
|
||||
const isBottom = useToolbarLocation() === "bottom";
|
||||
|
||||
@@ -618,4 +618,37 @@ p > *::selection {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
[dir="rtl"] .taskItemTools { right: unset; left: 0 }
|
||||
[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user