From 1ddb045621cc37567fed33749b61ee85505a94dd Mon Sep 17 00:00:00 2001 From: thecodrr Date: Wed, 20 Apr 2022 18:38:36 +0500 Subject: [PATCH] feat: add complete task list support --- .../dist/extensions/taskitem/component.d.ts | 4 + .../dist/extensions/taskitem/component.js | 130 ++++++++++++ .../dist/extensions/taskitem/index.d.ts | 1 + .../editor/dist/extensions/taskitem/index.js | 1 + .../dist/extensions/taskitem/taskitem.d.ts | 3 + .../dist/extensions/taskitem/taskitem.js | 50 +++++ .../dist/extensions/tasklist/component.d.ts | 3 + .../dist/extensions/tasklist/component.js | 74 +++++++ .../dist/extensions/tasklist/index.d.ts | 1 + .../editor/dist/extensions/tasklist/index.js | 1 + .../dist/extensions/tasklist/tasklist.d.ts | 1 + .../dist/extensions/tasklist/tasklist.js | 47 +++++ packages/editor/dist/index.js | 12 +- .../editor/dist/toolbar/components/icon.js | 2 +- packages/editor/dist/toolbar/icons.d.ts | 6 +- packages/editor/dist/toolbar/icons.js | 8 +- packages/editor/dist/toolbar/toolbar.js | 2 +- packages/editor/dist/toolbar/tools/index.d.ts | 3 +- packages/editor/dist/toolbar/tools/index.js | 3 +- .../editor/dist/toolbar/tools/inline.d.ts | 1 + packages/editor/dist/toolbar/tools/lists.d.ts | 5 + packages/editor/dist/toolbar/tools/lists.js | 13 ++ packages/editor/package-lock.json | 38 ++++ packages/editor/package.json | 2 + .../src/extensions/task-item/component.tsx | 191 ++++++++++++++++++ .../editor/src/extensions/task-item/index.ts | 1 + .../src/extensions/task-item/task-item.ts | 62 ++++++ .../src/extensions/task-list/component.tsx | 109 ++++++++++ .../editor/src/extensions/task-list/index.ts | 1 + .../src/extensions/task-list/task-list.ts | 55 +++++ packages/editor/src/index.ts | 13 +- .../editor/src/toolbar/components/icon.tsx | 1 + packages/editor/src/toolbar/icons.ts | 10 +- packages/editor/src/toolbar/toolbar.tsx | 2 +- packages/editor/src/toolbar/tools/index.ts | 3 +- packages/editor/src/toolbar/tools/lists.tsx | 19 ++ 36 files changed, 864 insertions(+), 14 deletions(-) create mode 100644 packages/editor/dist/extensions/taskitem/component.d.ts create mode 100644 packages/editor/dist/extensions/taskitem/component.js create mode 100644 packages/editor/dist/extensions/taskitem/index.d.ts create mode 100644 packages/editor/dist/extensions/taskitem/index.js create mode 100644 packages/editor/dist/extensions/taskitem/taskitem.d.ts create mode 100644 packages/editor/dist/extensions/taskitem/taskitem.js create mode 100644 packages/editor/dist/extensions/tasklist/component.d.ts create mode 100644 packages/editor/dist/extensions/tasklist/component.js create mode 100644 packages/editor/dist/extensions/tasklist/index.d.ts create mode 100644 packages/editor/dist/extensions/tasklist/index.js create mode 100644 packages/editor/dist/extensions/tasklist/tasklist.d.ts create mode 100644 packages/editor/dist/extensions/tasklist/tasklist.js create mode 100644 packages/editor/src/extensions/task-item/component.tsx create mode 100644 packages/editor/src/extensions/task-item/index.ts create mode 100644 packages/editor/src/extensions/task-item/task-item.ts create mode 100644 packages/editor/src/extensions/task-list/component.tsx create mode 100644 packages/editor/src/extensions/task-list/index.ts create mode 100644 packages/editor/src/extensions/task-list/task-list.ts diff --git a/packages/editor/dist/extensions/taskitem/component.d.ts b/packages/editor/dist/extensions/taskitem/component.d.ts new file mode 100644 index 000000000..0d615e09b --- /dev/null +++ b/packages/editor/dist/extensions/taskitem/component.d.ts @@ -0,0 +1,4 @@ +/// +import { ImageProps } from "rebass"; +import { NodeViewProps } from "@tiptap/react"; +export declare function TaskItemComponent(props: ImageProps & NodeViewProps): JSX.Element; diff --git a/packages/editor/dist/extensions/taskitem/component.js b/packages/editor/dist/extensions/taskitem/component.js new file mode 100644 index 000000000..d2a4b04b5 --- /dev/null +++ b/packages/editor/dist/extensions/taskitem/component.js @@ -0,0 +1,130 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { Flex } from "rebass"; +import { NodeViewWrapper, NodeViewContent, } from "@tiptap/react"; +import { ThemeProvider } from "emotion-theming"; +import { Icon } from "../../toolbar/components/icon"; +import { Icons } from "../../toolbar/icons"; +import { findChildren, } from "@tiptap/core"; +import { useCallback } from "react"; +export function TaskItemComponent(props) { + var checked = props.node.attrs.checked; + var editor = props.editor, updateAttributes = props.updateAttributes, node = props.node, getPos = props.getPos; + // const [isOpen, setIsOpen] = useState(true); + // const elementRef = useRef(); + // const isActive = editor.isActive("attachment", { hash }); + // const [isToolbarVisible, setIsToolbarVisible] = useState(); + var theme = editor.storage.theme; + // useEffect(() => { + // setIsToolbarVisible(isActive); + // }, [isActive]); + var toggle = useCallback(function () { + if (!editor.isEditable) + return false; + updateAttributes({ checked: !checked }); + var tr = editor.state.tr; + var parentPos = getPos(); + toggleChildren(node, tr, !checked, parentPos); + editor.view.dispatch(tr); + return true; + }, [editor, getPos, node]); + var nestedTaskList = getChildren(node, getPos()).find(function (_a) { + var node = _a.node; + return node.type.name === "taskList"; + }); + var isNested = !!nestedTaskList; + return (_jsx(NodeViewWrapper, { children: _jsx(ThemeProvider, __assign({ theme: theme }, { children: _jsxs(Flex, __assign({ sx: { + mb: 2, + ":hover > .dragHandle, :hover > .toggleSublist": { + opacity: 1, + }, + } }, { children: [_jsxs(Flex, __assign({ sx: { flex: 1 } }, { children: [_jsx(Icon, { className: "dragHandle", draggable: "true", contentEditable: false, "data-drag-handle": true, path: Icons.dragHandle, sx: { + opacity: 0, + alignSelf: "start", + mr: 2, + cursor: "grab", + ".icon:hover path": { + fill: "var(--disabled) !important", + }, + }, size: 20 }), _jsx(Icon, { path: checked ? Icons.check : "", sx: { + border: "2px solid", + borderColor: checked ? "disabled" : "icon", + borderRadius: "default", + alignSelf: "start", + mr: 2, + p: "1px", + cursor: "pointer", + ":hover": { + borderColor: "disabled", + }, + ":hover .icon path": { + fill: "var(--disabled) !important", + }, + }, onMouseEnter: function (e) { + if (e.buttons > 0) { + toggle(); + } + }, onMouseDown: function (e) { + if (toggle()) + e.preventDefault(); + }, color: checked ? "disabled" : "icon", size: 13 }), _jsx(NodeViewContent, { as: "li", style: { + listStyleType: "none", + textDecorationLine: checked ? "line-through" : "none", + color: checked ? "var(--disabled)" : "var(--text)", + flex: 1, + } })] })), isNested && (_jsx(Icon, { className: "toggleSublist", path: nestedTaskList.node.attrs.collapsed + ? Icons.chevronDown + : Icons.chevronUp, sx: { + opacity: 0, + position: "absolute", + right: 0, + alignSelf: "start", + mr: 2, + cursor: "pointer", + ".icon:hover path": { + fill: "var(--disabled) !important", + }, + }, size: 20, onClick: function () { + editor + .chain() + .setNodeSelection(getPos()) + .command(function (_a) { + var tr = _a.tr; + var pos = nestedTaskList.pos, node = nestedTaskList.node; + tr.setNodeMarkup(pos, undefined, { + collapsed: !node.attrs.collapsed, + }); + return true; + }) + .run(); + } }))] })) })) })); +} +function toggleChildren(node, tr, toggleState, parentPos) { + var children = findChildren(node, function (node) { return node.type.name === "taskItem"; }); + for (var _i = 0, children_1 = children; _i < children_1.length; _i++) { + var pos = children_1[_i].pos; + // need to add 1 to get inside the node + var actualPos = pos + parentPos + 1; + tr.setNodeMarkup(actualPos, undefined, { + checked: toggleState, + }); + } + return tr; +} +function getChildren(node, parentPos) { + var children = []; + node.forEach(function (node, offset) { + children.push({ node: node, pos: parentPos + offset + 1 }); + }); + return children; +} diff --git a/packages/editor/dist/extensions/taskitem/index.d.ts b/packages/editor/dist/extensions/taskitem/index.d.ts new file mode 100644 index 000000000..3518ff3b2 --- /dev/null +++ b/packages/editor/dist/extensions/taskitem/index.d.ts @@ -0,0 +1 @@ +export * from "./task-item"; diff --git a/packages/editor/dist/extensions/taskitem/index.js b/packages/editor/dist/extensions/taskitem/index.js new file mode 100644 index 000000000..3518ff3b2 --- /dev/null +++ b/packages/editor/dist/extensions/taskitem/index.js @@ -0,0 +1 @@ +export * from "./task-item"; diff --git a/packages/editor/dist/extensions/taskitem/taskitem.d.ts b/packages/editor/dist/extensions/taskitem/taskitem.d.ts new file mode 100644 index 000000000..ed24800a1 --- /dev/null +++ b/packages/editor/dist/extensions/taskitem/taskitem.d.ts @@ -0,0 +1,3 @@ +export interface AttachmentOptions { +} +export declare const TaskItemNode: import("@tiptap/core").Node; diff --git a/packages/editor/dist/extensions/taskitem/taskitem.js b/packages/editor/dist/extensions/taskitem/taskitem.js new file mode 100644 index 000000000..b0adabe6c --- /dev/null +++ b/packages/editor/dist/extensions/taskitem/taskitem.js @@ -0,0 +1,50 @@ +import { mergeAttributes } from "@tiptap/core"; +import { TaskItem } from "@tiptap/extension-task-item"; +import { ReactNodeViewRenderer } from "@tiptap/react"; +import { TaskItemComponent } from "./component"; +export var TaskItemNode = TaskItem.extend({ + draggable: true, + addAttributes: function () { + return { + checked: { + default: false, + keepOnSplit: false, + parseHTML: function (element) { return element.classList.contains("checked"); }, + renderHTML: function (attributes) { return ({ + class: attributes.checked ? "checked" : "", + }); }, + }, + }; + }, + renderHTML: function (_a) { + var node = _a.node, HTMLAttributes = _a.HTMLAttributes; + return [ + "li", + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { + "data-type": this.name, + }), + 0, + ]; + }, + // addAttributes() { + // return { + // hash: getDataAttribute("hash"), + // filename: getDataAttribute("filename"), + // type: getDataAttribute("type"), + // size: getDataAttribute("size"), + // }; + // }, + // parseHTML() { + // return [ + // { + // tag: "span[data-hash]", + // }, + // ]; + // }, + // renderHTML({ HTMLAttributes }) { + // return ["span", mergeAttributes(HTMLAttributes)]; + // }, + addNodeView: function () { + return ReactNodeViewRenderer(TaskItemComponent); + }, +}); diff --git a/packages/editor/dist/extensions/tasklist/component.d.ts b/packages/editor/dist/extensions/tasklist/component.d.ts new file mode 100644 index 000000000..058874187 --- /dev/null +++ b/packages/editor/dist/extensions/tasklist/component.d.ts @@ -0,0 +1,3 @@ +/// +import { NodeViewProps } from "@tiptap/react"; +export declare function TaskListComponent(props: NodeViewProps): JSX.Element; diff --git a/packages/editor/dist/extensions/tasklist/component.js b/packages/editor/dist/extensions/tasklist/component.js new file mode 100644 index 000000000..49761bc3e --- /dev/null +++ b/packages/editor/dist/extensions/tasklist/component.js @@ -0,0 +1,74 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import { Box, Flex, Text } from "rebass"; +import { NodeViewWrapper, NodeViewContent, } from "@tiptap/react"; +import { findParentNodeClosestToPos, findChildren } from "@tiptap/core"; +import { ThemeProvider } from "emotion-theming"; +import { Icon } from "../../toolbar/components/icon"; +import { Icons } from "../../toolbar/icons"; +import { useEffect, useState } from "react"; +export function TaskListComponent(props) { + var editor = props.editor, getPos = props.getPos, node = props.node; + var collapsed = node.attrs.collapsed; + var _a = useState({ checked: 0, total: 0, percentage: 0 }), stats = _a[0], setStats = _a[1]; + var theme = editor.storage.theme; + var resolvedPos = editor.state.doc.resolve(getPos()); + var parentTaskItem = findParentNodeClosestToPos(resolvedPos, function (node) { return node.type.name === "taskItem"; }); + var nested = !!parentTaskItem; + useEffect(function () { + if (!parentTaskItem) + return; + var node = parentTaskItem.node, pos = parentTaskItem.pos; + var allChecked = areAllChecked(node); + var tr = editor.state.tr; + tr.setNodeMarkup(pos, node.type, { checked: allChecked }); + editor.view.dispatch(tr); + }, [parentTaskItem]); + useEffect(function () { + if (nested) + return; + var children = findChildren(node, function (node) { return node.type.name === "taskItem"; }); + var checked = children.filter(function (node) { return node.node.attrs.checked; }).length; + var total = children.length; + var percentage = Math.round((checked / total) * 100); + setStats({ checked: checked, total: total, percentage: percentage }); + }, [nested, node]); + console.log(collapsed); + return (_jsx(NodeViewWrapper, __assign({ style: { display: collapsed ? "none" : "block" } }, { children: _jsx(ThemeProvider, __assign({ theme: theme }, { children: _jsxs(Flex, __assign({ sx: { flexDirection: "column" } }, { children: [nested ? null : (_jsxs(Flex, __assign({ sx: { + position: "relative", + bg: "bgSecondary", + py: 1, + borderRadius: "default", + mb: 2, + alignItems: "center", + justifyContent: "end", + overflow: "hidden", + px: 2, + } }, { children: [_jsx(Box, { sx: { + height: "100%", + width: "".concat(stats.percentage, "%"), + position: "absolute", + bg: "border", + zIndex: 0, + left: 0, + transition: "width 250ms ease-out", + } }), _jsxs(Flex, __assign({ sx: { zIndex: 1 } }, { children: [_jsx(Icon, { path: Icons.checkbox, size: 15 }), _jsxs(Text, __assign({ variant: "body", sx: { ml: 1 } }, { children: [stats.checked, "/", stats.total] }))] }))] }))), _jsx(NodeViewContent, { as: "ul", style: { + paddingInlineStart: 0, + marginBlockStart: nested ? 15 : 0, + marginBlockEnd: 0, + } })] })) })) }))); +} +function areAllChecked(node) { + var children = findChildren(node, function (node) { return node.type.name === "taskItem"; }); + return children.every(function (node) { return node.node.attrs.checked; }); +} diff --git a/packages/editor/dist/extensions/tasklist/index.d.ts b/packages/editor/dist/extensions/tasklist/index.d.ts new file mode 100644 index 000000000..291e035cb --- /dev/null +++ b/packages/editor/dist/extensions/tasklist/index.d.ts @@ -0,0 +1 @@ +export * from "./task-list"; diff --git a/packages/editor/dist/extensions/tasklist/index.js b/packages/editor/dist/extensions/tasklist/index.js new file mode 100644 index 000000000..291e035cb --- /dev/null +++ b/packages/editor/dist/extensions/tasklist/index.js @@ -0,0 +1 @@ +export * from "./task-list"; diff --git a/packages/editor/dist/extensions/tasklist/tasklist.d.ts b/packages/editor/dist/extensions/tasklist/tasklist.d.ts new file mode 100644 index 000000000..1520f3f44 --- /dev/null +++ b/packages/editor/dist/extensions/tasklist/tasklist.d.ts @@ -0,0 +1 @@ +export declare const TaskListNode: import("@tiptap/core").Node; diff --git a/packages/editor/dist/extensions/tasklist/tasklist.js b/packages/editor/dist/extensions/tasklist/tasklist.js new file mode 100644 index 000000000..34d533bc8 --- /dev/null +++ b/packages/editor/dist/extensions/tasklist/tasklist.js @@ -0,0 +1,47 @@ +import { TaskList } from "@tiptap/extension-task-list"; +import { ReactNodeViewRenderer } from "@tiptap/react"; +import { TaskListComponent } from "./component"; +export var TaskListNode = TaskList.extend({ + addAttributes: function () { + return { + collapsed: { + default: false, + keepOnSplit: false, + parseHTML: function (element) { return element.dataset.collapsed === "true"; }, + renderHTML: function (attributes) { return ({ + "data-collapsed": attributes.collapsed === true, + }); }, + }, + }; + }, + // renderHTML({ node, HTMLAttributes }) { + // return [ + // "li", + // mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { + // "data-type": this.name, + // }), + // 0, + // ]; + // }, + // addAttributes() { + // return { + // hash: getDataAttribute("hash"), + // filename: getDataAttribute("filename"), + // type: getDataAttribute("type"), + // size: getDataAttribute("size"), + // }; + // }, + // parseHTML() { + // return [ + // { + // tag: "span[data-hash]", + // }, + // ]; + // }, + // renderHTML({ HTMLAttributes }) { + // return ["span", mergeAttributes(HTMLAttributes)]; + // }, + addNodeView: function () { + return ReactNodeViewRenderer(TaskListComponent); + }, +}); diff --git a/packages/editor/dist/index.js b/packages/editor/dist/index.js index cc020f8d3..3631263a3 100644 --- a/packages/editor/dist/index.js +++ b/packages/editor/dist/index.js @@ -47,6 +47,8 @@ import TableHeader from "@tiptap/extension-table-header"; import { ImageNode } from "./extensions/image"; import { useTheme } from "@notesnook/theme"; import { AttachmentNode } from "./extensions/attachment"; +import { TaskListNode } from "./extensions/task-list"; +import { TaskItemNode } from "./extensions/task-item"; EditorView.prototype.updateState = function updateState(state) { if (!this.docView) return; // This prevents the matchesNode error on hot reloads @@ -58,7 +60,11 @@ var useTiptap = function (options, deps) { var defaultOptions = useMemo(function () { return ({ extensions: [ TextStyle, - StarterKit, + StarterKit.configure({ + dropcursor: { + class: "drop-cursor", + }, + }), CharacterCount, Underline, Subscript, @@ -68,8 +74,9 @@ var useTiptap = function (options, deps) { FontFamily, BulletList, OrderedList, + TaskItemNode.configure({ nested: true }), + TaskListNode, Link, - ImageNode, Table.configure({ resizable: true, allowTableNodeSelection: true, @@ -89,6 +96,7 @@ var useTiptap = function (options, deps) { Placeholder.configure({ placeholder: "Start writing your note...", }), + ImageNode, AttachmentNode.configure({ onDownloadAttachment: onDownloadAttachment, }), diff --git a/packages/editor/dist/toolbar/components/icon.js b/packages/editor/dist/toolbar/components/icon.js index c9a8f6af4..f51d1ea20 100644 --- a/packages/editor/dist/toolbar/components/icon.js +++ b/packages/editor/dist/toolbar/components/icon.js @@ -30,7 +30,7 @@ function MDIIconWrapper(_a) { var themedColor = theme.colors ? theme.colors[color] : color; - return (_jsx(MDIIcon, { title: title, path: path, size: size + "px", style: { + return (_jsx(MDIIcon, { className: "icon", title: title, path: path, size: size + "px", style: { strokeWidth: stroke || "0px", stroke: themedColor, }, color: themedColor, spin: rotate })); diff --git a/packages/editor/dist/toolbar/icons.d.ts b/packages/editor/dist/toolbar/icons.d.ts index b44dc584b..9bdad04bc 100644 --- a/packages/editor/dist/toolbar/icons.d.ts +++ b/packages/editor/dist/toolbar/icons.d.ts @@ -22,9 +22,9 @@ export declare const Icons: { textColor: string; link: string; image: string; - chevronDown: string; colorClear: string; check: string; + checkbox: string; loading: string; more: string; upload: string; @@ -48,9 +48,13 @@ export declare const Icons: { deleteTable: string; mergeCells: string; splitCells: string; + checklist: string; + dragHandle: string; plus: string; close: string; delete: string; download: string; + chevronDown: string; + chevronUp: string; }; export declare type IconNames = keyof typeof Icons; diff --git a/packages/editor/dist/toolbar/icons.js b/packages/editor/dist/toolbar/icons.js index e91f836f2..cc242fd49 100644 --- a/packages/editor/dist/toolbar/icons.js +++ b/packages/editor/dist/toolbar/icons.js @@ -1,4 +1,4 @@ -import { mdiAttachment, mdiBorderHorizontal, mdiCheck, mdiChevronDown, mdiCodeBraces, mdiCodeTags, mdiDotsVertical, mdiFormatAlignCenter, mdiFormatAlignJustify, mdiFormatAlignLeft, mdiFormatAlignRight, mdiFormatBold, mdiFormatClear, mdiFormatColorHighlight, mdiFormatColorText, mdiFormatItalic, mdiFormatListBulleted, mdiFormatListNumbered, mdiFormatQuoteClose, mdiFormatStrikethrough, mdiFormatSubscript, mdiFormatSuperscript, mdiFormatTextdirectionLToR, mdiFormatTextdirectionRToL, mdiFormatUnderline, mdiImage, mdiInvertColorsOff, mdiLinkPlus, mdiLoading, mdiTable, mdiTableBorder, mdiTableRowPlusBefore, mdiTableRowRemove, mdiTableColumnPlusAfter, mdiTableColumnPlusBefore, mdiTableColumnRemove, mdiUploadOutline, mdiPlus, mdiSquareRoundedBadgeOutline, mdiFormatColorFill, mdiBorderAllVariant, mdiClose, mdiSortDescending, mdiArrowExpandRight, mdiArrowExpandLeft, mdiArrowExpandDown, mdiArrowExpandUp, mdiTrashCanOutline, mdiTableMergeCells, mdiTableSplitCell, mdiDeleteOutline, mdiDownloadOutline, } from "@mdi/js"; +import { mdiAttachment, mdiBorderHorizontal, mdiCheck, mdiChevronDown, mdiCodeBraces, mdiCodeTags, mdiDotsVertical, mdiFormatAlignCenter, mdiFormatAlignJustify, mdiFormatAlignLeft, mdiFormatAlignRight, mdiFormatBold, mdiFormatClear, mdiFormatColorHighlight, mdiFormatColorText, mdiFormatItalic, mdiFormatListBulleted, mdiFormatListNumbered, mdiFormatQuoteClose, mdiFormatStrikethrough, mdiFormatSubscript, mdiFormatSuperscript, mdiFormatTextdirectionLToR, mdiFormatTextdirectionRToL, mdiFormatUnderline, mdiImage, mdiInvertColorsOff, mdiLinkPlus, mdiLoading, mdiTable, mdiTableBorder, mdiTableRowPlusBefore, mdiTableRowRemove, mdiTableColumnPlusAfter, mdiTableColumnPlusBefore, mdiTableColumnRemove, mdiUploadOutline, mdiPlus, mdiSquareRoundedBadgeOutline, mdiFormatColorFill, mdiBorderAllVariant, mdiClose, mdiSortDescending, mdiArrowExpandRight, mdiArrowExpandLeft, mdiArrowExpandDown, mdiArrowExpandUp, mdiTrashCanOutline, mdiTableMergeCells, mdiTableSplitCell, mdiDeleteOutline, mdiDownloadOutline, mdiFormatListCheckbox, mdiDrag, mdiCheckboxMarkedOutline, mdiChevronUp, } from "@mdi/js"; export var Icons = { bold: mdiFormatBold, italic: mdiFormatItalic, @@ -23,9 +23,9 @@ export var Icons = { textColor: mdiFormatColorText, link: mdiLinkPlus, image: mdiImage, - chevronDown: mdiChevronDown, colorClear: mdiInvertColorsOff, check: mdiCheck, + checkbox: mdiCheckboxMarkedOutline, loading: mdiLoading, more: mdiDotsVertical, upload: mdiUploadOutline, @@ -49,8 +49,12 @@ export var Icons = { deleteTable: mdiTrashCanOutline, mergeCells: mdiTableMergeCells, splitCells: mdiTableSplitCell, + checklist: mdiFormatListCheckbox, + dragHandle: mdiDrag, plus: mdiPlus, close: mdiClose, delete: mdiDeleteOutline, download: mdiDownloadOutline, + chevronDown: mdiChevronDown, + chevronUp: mdiChevronUp, }; diff --git a/packages/editor/dist/toolbar/toolbar.js b/packages/editor/dist/toolbar/toolbar.js index ba9e1508a..95c1fb83b 100644 --- a/packages/editor/dist/toolbar/toolbar.js +++ b/packages/editor/dist/toolbar/toolbar.js @@ -25,7 +25,7 @@ export function Toolbar(props) { ["subscript", "superscript", "horizontalRule"], ["codeblock", "blockquote"], ["formatClear", "ltr", "rtl"], - ["numberedList", "bulletList"], + ["numberedList", "bulletList", "checklist"], ["link", "image", "attachment", "table"], ["textColor", "highlight"], ]; diff --git a/packages/editor/dist/toolbar/tools/index.d.ts b/packages/editor/dist/toolbar/tools/index.d.ts index 31f49b7e2..9f6b7983b 100644 --- a/packages/editor/dist/toolbar/tools/index.d.ts +++ b/packages/editor/dist/toolbar/tools/index.d.ts @@ -4,7 +4,7 @@ import { FontSize, FontFamily } from "./font"; import { AlignCenter, AlignLeft, AlignRight, AlignJustify } from "./alignment"; import { Blockquote, CodeBlock, HorizontalRule, Image, Table } from "./block"; import { Headings } from "./headings"; -import { NumberedList, BulletList } from "./lists"; +import { NumberedList, BulletList, Checklist } from "./lists"; import { LeftToRight, RightToLeft } from "./text-direction"; import { Highlight, TextColor } from "./colors"; declare const tools: { @@ -30,6 +30,7 @@ declare const tools: { rtl: RightToLeft; numberedList: NumberedList; bulletList: BulletList; + checklist: Checklist; textColor: TextColor; highlight: Highlight; link: Link; diff --git a/packages/editor/dist/toolbar/tools/index.js b/packages/editor/dist/toolbar/tools/index.js index 52ce892bd..399523476 100644 --- a/packages/editor/dist/toolbar/tools/index.js +++ b/packages/editor/dist/toolbar/tools/index.js @@ -3,7 +3,7 @@ import { FontSize, FontFamily } from "./font"; import { AlignCenter, AlignLeft, AlignRight, AlignJustify } from "./alignment"; import { Blockquote, CodeBlock, HorizontalRule, Image, Table } from "./block"; import { Headings } from "./headings"; -import { NumberedList, BulletList } from "./lists"; +import { NumberedList, BulletList, Checklist } from "./lists"; import { LeftToRight, RightToLeft } from "./text-direction"; import { Highlight, TextColor } from "./colors"; var tools = { @@ -29,6 +29,7 @@ var tools = { rtl: new RightToLeft(), numberedList: new NumberedList(), bulletList: new BulletList(), + checklist: new Checklist(), textColor: new TextColor(), highlight: new Highlight(), link: new Link(), diff --git a/packages/editor/dist/toolbar/tools/inline.d.ts b/packages/editor/dist/toolbar/tools/inline.d.ts index 118f86bc1..90c680b7b 100644 --- a/packages/editor/dist/toolbar/tools/inline.d.ts +++ b/packages/editor/dist/toolbar/tools/inline.d.ts @@ -1,3 +1,4 @@ +/// import { ITool, ToolProps } from "../types"; import { ToolId } from "."; import { IconNames } from "../icons"; diff --git a/packages/editor/dist/toolbar/tools/lists.d.ts b/packages/editor/dist/toolbar/tools/lists.d.ts index 05e58e754..fe8d31586 100644 --- a/packages/editor/dist/toolbar/tools/lists.d.ts +++ b/packages/editor/dist/toolbar/tools/lists.d.ts @@ -29,4 +29,9 @@ declare type BulletListStyleTypes = "circle" | "square" | "disc"; export declare class BulletList extends ListTool { constructor(); } +export declare class Checklist implements ITool { + id: ToolId; + title: string; + render: (props: ToolProps) => JSX.Element; +} export {}; diff --git a/packages/editor/dist/toolbar/tools/lists.js b/packages/editor/dist/toolbar/tools/lists.js index 4fbabeb1d..303774692 100644 --- a/packages/editor/dist/toolbar/tools/lists.js +++ b/packages/editor/dist/toolbar/tools/lists.js @@ -127,6 +127,19 @@ var BulletList = /** @class */ (function (_super) { return BulletList; }(ListTool)); export { BulletList }; +var Checklist = /** @class */ (function () { + function Checklist() { + var _this = this; + this.id = "checklist"; + this.title = "Checklist"; + this.render = function (props) { + var editor = props.editor; + return (_jsx(ToolButton, { title: _this.title, id: _this.id, icon: "checklist", onClick: function () { return editor.chain().focus().toggleTaskList().run(); }, toggled: false })); + }; + } + return Checklist; +}()); +export { Checklist }; function ListThumbnail(props) { var listStyleType = props.listStyleType; return (_jsx(Flex, __assign({ as: "ul", sx: { diff --git a/packages/editor/package-lock.json b/packages/editor/package-lock.json index f6019d7fe..3fc2cbb4c 100644 --- a/packages/editor/package-lock.json +++ b/packages/editor/package-lock.json @@ -26,6 +26,8 @@ "@tiptap/extension-table-cell": "^2.0.0-beta.20", "@tiptap/extension-table-header": "^2.0.0-beta.22", "@tiptap/extension-table-row": "^2.0.0-beta.19", + "@tiptap/extension-task-item": "^2.0.0-beta.31", + "@tiptap/extension-task-list": "^2.0.0-beta.26", "@tiptap/extension-text-align": "^2.0.0-beta.29", "@tiptap/extension-text-style": "^2.0.0-beta.23", "@tiptap/extension-underline": "^2.0.0-beta.23", @@ -4141,6 +4143,30 @@ "@tiptap/core": "^2.0.0-beta.1" } }, + "node_modules/@tiptap/extension-task-item": { + "version": "2.0.0-beta.31", + "resolved": "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-2.0.0-beta.31.tgz", + "integrity": "sha512-9MCInLAf/l/wDD1N3GgOImemloFARi1l9AJ5acfo+sDjN52yfvaLb//lvLJ6IGz4xGepeAyCME8Qns8UGqG4RQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0-beta.1" + } + }, + "node_modules/@tiptap/extension-task-list": { + "version": "2.0.0-beta.26", + "resolved": "https://registry.npmjs.org/@tiptap/extension-task-list/-/extension-task-list-2.0.0-beta.26.tgz", + "integrity": "sha512-7zPpz9eOUCnFyWNDFYPCUJ39gjID+mCI5BuXyXrjJjDfm8wxg/xTgg9+KC6xakczos7DypnhzlRKSs4EFczeUg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0-beta.1" + } + }, "node_modules/@tiptap/extension-text": { "version": "2.0.0-beta.15", "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.0.0-beta.15.tgz", @@ -27708,6 +27734,18 @@ "integrity": "sha512-ldEVDpIUX7ZqbViTy4c/RfyNGRv++O/r3A/Ivuon1PykaDDTbPlp5JM89FunAD39cLAbo2HKtweqdmzCMlZsqA==", "requires": {} }, + "@tiptap/extension-task-item": { + "version": "2.0.0-beta.31", + "resolved": "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-2.0.0-beta.31.tgz", + "integrity": "sha512-9MCInLAf/l/wDD1N3GgOImemloFARi1l9AJ5acfo+sDjN52yfvaLb//lvLJ6IGz4xGepeAyCME8Qns8UGqG4RQ==", + "requires": {} + }, + "@tiptap/extension-task-list": { + "version": "2.0.0-beta.26", + "resolved": "https://registry.npmjs.org/@tiptap/extension-task-list/-/extension-task-list-2.0.0-beta.26.tgz", + "integrity": "sha512-7zPpz9eOUCnFyWNDFYPCUJ39gjID+mCI5BuXyXrjJjDfm8wxg/xTgg9+KC6xakczos7DypnhzlRKSs4EFczeUg==", + "requires": {} + }, "@tiptap/extension-text": { "version": "2.0.0-beta.15", "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.0.0-beta.15.tgz", diff --git a/packages/editor/package.json b/packages/editor/package.json index a43993456..19d7eeb44 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -22,6 +22,8 @@ "@tiptap/extension-table-cell": "^2.0.0-beta.20", "@tiptap/extension-table-header": "^2.0.0-beta.22", "@tiptap/extension-table-row": "^2.0.0-beta.19", + "@tiptap/extension-task-item": "^2.0.0-beta.31", + "@tiptap/extension-task-list": "^2.0.0-beta.26", "@tiptap/extension-text-align": "^2.0.0-beta.29", "@tiptap/extension-text-style": "^2.0.0-beta.23", "@tiptap/extension-underline": "^2.0.0-beta.23", diff --git a/packages/editor/src/extensions/task-item/component.tsx b/packages/editor/src/extensions/task-item/component.tsx new file mode 100644 index 000000000..a07e6e9cf --- /dev/null +++ b/packages/editor/src/extensions/task-item/component.tsx @@ -0,0 +1,191 @@ +import { Box, Flex, Image, ImageProps, Text } from "rebass"; +import { + NodeViewWrapper, + NodeViewProps, + NodeViewContent, + FloatingMenu, +} from "@tiptap/react"; +import { ThemeProvider } from "emotion-theming"; +import { Theme } from "@notesnook/theme"; +import { Icon } from "../../toolbar/components/icon"; +import { Icons } from "../../toolbar/icons"; +import { Node } from "prosemirror-model"; +import { Transaction, Selection } from "prosemirror-state"; +import { + findParentNodeClosestToPos, + findChildren, + NodeWithPos, +} from "@tiptap/core"; +import { useCallback, useState } from "react"; + +export function TaskItemComponent(props: ImageProps & NodeViewProps) { + const { checked } = props.node.attrs; + + const { editor, updateAttributes, node, getPos } = props; + // const [isOpen, setIsOpen] = useState(true); + // const elementRef = useRef(); + // const isActive = editor.isActive("attachment", { hash }); + // const [isToolbarVisible, setIsToolbarVisible] = useState(); + const theme = editor.storage.theme as Theme; + + // useEffect(() => { + // setIsToolbarVisible(isActive); + // }, [isActive]); + + const toggle = useCallback(() => { + if (!editor.isEditable) return false; + updateAttributes({ checked: !checked }); + + const tr = editor.state.tr; + const parentPos = getPos(); + + toggleChildren(node, tr, !checked, parentPos); + + editor.view.dispatch(tr); + return true; + }, [editor, getPos, node]); + const nestedTaskList = getChildren(node, getPos()).find( + ({ node }) => node.type.name === "taskList" + ); + const isNested = !!nestedTaskList; + + return ( + + + .dragHandle, :hover > .toggleSublist": { + opacity: 1, + }, + }} + > + + + { + if (e.buttons > 0) { + toggle(); + } + }} + onMouseDown={(e) => { + if (toggle()) e.preventDefault(); + }} + color={checked ? "disabled" : "icon"} + size={13} + /> + + + + {isNested && ( + { + editor + .chain() + .setNodeSelection(getPos()) + .command(({ tr }) => { + const { pos, node } = nestedTaskList; + tr.setNodeMarkup(pos, undefined, { + collapsed: !node.attrs.collapsed, + }); + return true; + }) + .run(); + }} + /> + )} + + + {/* + + + + + + */} + + ); +} + +function toggleChildren( + node: Node, + tr: Transaction, + toggleState: boolean, + parentPos: number +): Transaction { + const children = findChildren(node, (node) => node.type.name === "taskItem"); + for (const { pos } of children) { + // need to add 1 to get inside the node + const actualPos = pos + parentPos + 1; + tr.setNodeMarkup(actualPos, undefined, { + checked: toggleState, + }); + } + return tr; +} + +function getChildren(node: Node, parentPos: number) { + const children: NodeWithPos[] = []; + node.forEach((node, offset) => { + children.push({ node, pos: parentPos + offset + 1 }); + }); + return children; +} diff --git a/packages/editor/src/extensions/task-item/index.ts b/packages/editor/src/extensions/task-item/index.ts new file mode 100644 index 000000000..3518ff3b2 --- /dev/null +++ b/packages/editor/src/extensions/task-item/index.ts @@ -0,0 +1 @@ +export * from "./task-item"; diff --git a/packages/editor/src/extensions/task-item/task-item.ts b/packages/editor/src/extensions/task-item/task-item.ts new file mode 100644 index 000000000..77a69aafe --- /dev/null +++ b/packages/editor/src/extensions/task-item/task-item.ts @@ -0,0 +1,62 @@ +import { nodeInputRule, mergeAttributes } from "@tiptap/core"; +import { TaskItem } from "@tiptap/extension-task-item"; +import { ReactNodeViewRenderer } from "@tiptap/react"; +import { Attribute } from "@tiptap/core"; +import { TaskItemComponent } from "./component"; + +export interface AttachmentOptions { + // HTMLAttributes: Record; + // onDownloadAttachment: (attachment: Attachment) => boolean; +} + +export const TaskItemNode = TaskItem.extend({ + draggable: true, + + addAttributes() { + return { + checked: { + default: false, + keepOnSplit: false, + parseHTML: (element) => element.classList.contains("checked"), + renderHTML: (attributes) => ({ + class: attributes.checked ? "checked" : "", + }), + }, + }; + }, + + renderHTML({ node, HTMLAttributes }) { + return [ + "li", + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { + "data-type": this.name, + }), + 0, + ]; + }, + + // addAttributes() { + // return { + // hash: getDataAttribute("hash"), + // filename: getDataAttribute("filename"), + // type: getDataAttribute("type"), + // size: getDataAttribute("size"), + // }; + // }, + + // parseHTML() { + // return [ + // { + // tag: "span[data-hash]", + // }, + // ]; + // }, + + // renderHTML({ HTMLAttributes }) { + // return ["span", mergeAttributes(HTMLAttributes)]; + // }, + + addNodeView() { + return ReactNodeViewRenderer(TaskItemComponent); + }, +}); diff --git a/packages/editor/src/extensions/task-list/component.tsx b/packages/editor/src/extensions/task-list/component.tsx new file mode 100644 index 000000000..4da1b1ccd --- /dev/null +++ b/packages/editor/src/extensions/task-list/component.tsx @@ -0,0 +1,109 @@ +import { Box, Flex, Image, ImageProps, Text } from "rebass"; +import { + NodeViewWrapper, + NodeViewProps, + NodeViewContent, + FloatingMenu, +} from "@tiptap/react"; +import { Node } from "prosemirror-model"; +import { Transaction, Selection } from "prosemirror-state"; +import { findParentNodeClosestToPos, findChildren } from "@tiptap/core"; +import { ThemeProvider } from "emotion-theming"; +import { Theme } from "@notesnook/theme"; +import { Icon } from "../../toolbar/components/icon"; +import { Icons } from "../../toolbar/icons"; +import { useEffect, useState } from "react"; + +export function TaskListComponent(props: NodeViewProps) { + const { editor, getPos, node } = props; + const { collapsed } = node.attrs; + const [stats, setStats] = useState({ checked: 0, total: 0, percentage: 0 }); + + const theme = editor.storage.theme as Theme; + const resolvedPos = editor.state.doc.resolve(getPos()); + const parentTaskItem = findParentNodeClosestToPos( + resolvedPos, + (node) => node.type.name === "taskItem" + ); + const nested = !!parentTaskItem; + + useEffect(() => { + if (!parentTaskItem) return; + const { node, pos } = parentTaskItem; + const allChecked = areAllChecked(node); + const tr = editor.state.tr; + tr.setNodeMarkup(pos, node.type, { checked: allChecked }); + editor.view.dispatch(tr); + }, [parentTaskItem]); + + useEffect(() => { + if (nested) return; + const children = findChildren( + node, + (node) => node.type.name === "taskItem" + ); + const checked = children.filter((node) => node.node.attrs.checked).length; + const total = children.length; + const percentage = Math.round((checked / total) * 100); + setStats({ checked, total, percentage }); + }, [nested, node]); + console.log(collapsed); + return ( + + + + {nested ? null : ( + + + + + + {stats.checked}/{stats.total} + + + {/* + {stats.percentage}% done + */} + + )} + + + + + ); +} + +function areAllChecked(node: Node) { + const children = findChildren(node, (node) => node.type.name === "taskItem"); + return children.every((node) => node.node.attrs.checked); +} diff --git a/packages/editor/src/extensions/task-list/index.ts b/packages/editor/src/extensions/task-list/index.ts new file mode 100644 index 000000000..291e035cb --- /dev/null +++ b/packages/editor/src/extensions/task-list/index.ts @@ -0,0 +1 @@ +export * from "./task-list"; diff --git a/packages/editor/src/extensions/task-list/task-list.ts b/packages/editor/src/extensions/task-list/task-list.ts new file mode 100644 index 000000000..796b7ef7d --- /dev/null +++ b/packages/editor/src/extensions/task-list/task-list.ts @@ -0,0 +1,55 @@ +import { nodeInputRule, mergeAttributes } from "@tiptap/core"; +import { TaskList } from "@tiptap/extension-task-list"; +import { ReactNodeViewRenderer } from "@tiptap/react"; +import { Attribute } from "@tiptap/core"; +import { TaskListComponent } from "./component"; + +export const TaskListNode = TaskList.extend({ + addAttributes() { + return { + collapsed: { + default: false, + keepOnSplit: false, + parseHTML: (element) => element.dataset.collapsed === "true", + renderHTML: (attributes) => ({ + "data-collapsed": attributes.collapsed === true, + }), + }, + }; + }, + + // renderHTML({ node, HTMLAttributes }) { + // return [ + // "li", + // mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { + // "data-type": this.name, + // }), + // 0, + // ]; + // }, + + // addAttributes() { + // return { + // hash: getDataAttribute("hash"), + // filename: getDataAttribute("filename"), + // type: getDataAttribute("type"), + // size: getDataAttribute("size"), + // }; + // }, + + // parseHTML() { + // return [ + // { + // tag: "span[data-hash]", + // }, + // ]; + // }, + + // renderHTML({ HTMLAttributes }) { + // return ["span", mergeAttributes(HTMLAttributes)]; + // }, + + addNodeView() { + return ReactNodeViewRenderer(TaskListComponent); + }, +}); diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 842a62737..7ee0903f4 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -26,6 +26,8 @@ import { ImageNode } from "./extensions/image"; import { ThemeConfig } from "@notesnook/theme/dist/theme/types"; import { useTheme } from "@notesnook/theme"; import { AttachmentNode, AttachmentOptions } from "./extensions/attachment"; +import { TaskListNode } from "./extensions/task-list"; +import { TaskItemNode } from "./extensions/task-item"; EditorView.prototype.updateState = function updateState(state) { if (!(this as any).docView) return; // This prevents the matchesNode error on hot reloads @@ -49,7 +51,11 @@ const useTiptap = ( () => ({ extensions: [ TextStyle, - StarterKit, + StarterKit.configure({ + dropcursor: { + class: "drop-cursor", + }, + }), CharacterCount, Underline, Subscript, @@ -59,8 +65,9 @@ const useTiptap = ( FontFamily, BulletList, OrderedList, + TaskItemNode.configure({ nested: true }), + TaskListNode, Link, - ImageNode, Table.configure({ resizable: true, allowTableNodeSelection: true, @@ -80,6 +87,8 @@ const useTiptap = ( Placeholder.configure({ placeholder: "Start writing your note...", }), + + ImageNode, AttachmentNode.configure({ onDownloadAttachment, }), diff --git a/packages/editor/src/toolbar/components/icon.tsx b/packages/editor/src/toolbar/components/icon.tsx index b1b64f23a..6df86b219 100644 --- a/packages/editor/src/toolbar/components/icon.tsx +++ b/packages/editor/src/toolbar/components/icon.tsx @@ -28,6 +28,7 @@ function MDIIconWrapper({ return ( { } } +export class Checklist implements ITool { + id: ToolId = "checklist"; + title = "Checklist"; + + render = (props: ToolProps) => { + const { editor } = props; + + return ( + editor.chain().focus().toggleTaskList().run()} + toggled={false} + /> + ); + }; +} + type ListThumbnailProps = { listStyleType: string }; function ListThumbnail(props: ListThumbnailProps) { const { listStyleType } = props;