From 46a635d4f4246998bc0b388af43345cc54f3dec4 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Tue, 21 Feb 2023 16:55:52 +0500 Subject: [PATCH] editor: migrate to vanilla js outline list --- .../outline-list-item/component.tsx | 175 ------------------ .../outline-list-item/outline-list-item.ts | 95 ++++++++-- .../src/extensions/outline-list/component.tsx | 59 ------ .../extensions/outline-list/outline-list.ts | 31 +--- packages/editor/styles/styles.css | 77 +++++++- 5 files changed, 155 insertions(+), 282 deletions(-) delete mode 100644 packages/editor/src/extensions/outline-list-item/component.tsx delete mode 100644 packages/editor/src/extensions/outline-list/component.tsx diff --git a/packages/editor/src/extensions/outline-list-item/component.tsx b/packages/editor/src/extensions/outline-list-item/component.tsx deleted file mode 100644 index 4f291930e..000000000 --- a/packages/editor/src/extensions/outline-list-item/component.tsx +++ /dev/null @@ -1,175 +0,0 @@ -/* -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 { Box, Flex, Text } from "@theme-ui/components"; -import { GetPosNode, ReactNodeViewProps } from "../react"; -import { Icon } from "../../toolbar/components/icon"; -import { Icons } from "../../toolbar/icons"; -import { Node as ProsemirrorNode } from "prosemirror-model"; -import { findChildren } from "@tiptap/core"; -import { OutlineList } from "../outline-list/outline-list"; -import { useIsMobile } from "../../toolbar/stores/toolbar-store"; -import { Editor } from "../../types"; -import { TextDirections } from "../text-direction"; - -export function OutlineListItemComponent(props: ReactNodeViewProps) { - const { editor, node, getPos, forwardRef } = props; - - const isMobile = useIsMobile(); - const isNested = node.lastChild?.type.name === OutlineList.name; - const isCollapsed = isNested && node.lastChild?.attrs.collapsed; - - return ( - - - {isNested ? ( - <> - - - - ) : ( - - )} - - {isNested && !isCollapsed && ( - - )} - - - - ); -} - -function toggleOutlineList( - editor: Editor, - node: ProsemirrorNode, - isCollapsed: boolean, - nodePos: number -) { - const [subList] = findChildren( - node, - (node) => node.type.name === OutlineList.name - ); - if (!subList) return; - const { pos } = subList; - - editor.current?.commands.toggleOutlineCollapse( - pos + nodePos + 1, - !isCollapsed - ); -} - -type ToggleIconButtonProps = { - textDirection: TextDirections; - isCollapsed: boolean; - isMobile: boolean; - - editor: Editor; - node: ProsemirrorNode; - getPos: GetPosNode; -}; -function ToggleIconButton(props: ToggleIconButtonProps) { - const { textDirection, isCollapsed, isMobile, editor, node, getPos } = props; - - return ( - e.preventDefault()} - onTouchEnd={(e) => { - e.preventDefault(); - toggleOutlineList(editor, node, isCollapsed, getPos()); - }} - onClick={() => { - toggleOutlineList(editor, node, isCollapsed, getPos()); - }} - /> - ); -} diff --git a/packages/editor/src/extensions/outline-list-item/outline-list-item.ts b/packages/editor/src/extensions/outline-list-item/outline-list-item.ts index 478d13ff0..cef020848 100644 --- a/packages/editor/src/extensions/outline-list-item/outline-list-item.ts +++ b/packages/editor/src/extensions/outline-list-item/outline-list-item.ts @@ -22,8 +22,6 @@ import { NodeType } from "prosemirror-model"; import { findParentNodeOfTypeClosestToPos } from "../../utils/prosemirror"; import { onArrowUpPressed, onBackspacePressed } from "../list-item/commands"; import { OutlineList } from "../outline-list/outline-list"; -import { createNodeView } from "../react"; -import { OutlineListItemComponent } from "./component"; export interface ListItemOptions { HTMLAttributes: Record; @@ -46,6 +44,19 @@ export const OutlineListItem = Node.create({ }; }, + addAttributes() { + return { + collapsed: { + default: false, + keepOnSplit: false, + parseHTML: (element) => element.dataset.collapsed === "true", + renderHTML: (attributes) => ({ + "data-collapsed": attributes.collapsed === true + }) + } + }; + }, + content: "heading* paragraph block*", defining: true, @@ -114,10 +125,69 @@ export const OutlineListItem = Node.create({ }, addNodeView() { - return createNodeView(OutlineListItemComponent, { - contentDOMFactory: true, - wrapperFactory: () => document.createElement("li") - }); + return ({ node, getPos, editor }) => { + const isNested = node.lastChild?.type.name === OutlineList.name; + + const li = document.createElement("li"); + + if (node.attrs.collapsed) li.classList.add("collapsed"); + else li.classList.remove("collapsed"); + + if (isNested) li.classList.add("nested"); + else li.classList.remove("nested"); + + function onClick(e: MouseEvent | TouchEvent) { + if (!(e.target instanceof HTMLParagraphElement)) return; + if (!li.classList.contains("nested")) return; + + const clientX = + e instanceof MouseEvent ? e.clientX : e.touches[0].clientX; + const clientY = + e instanceof MouseEvent ? e.clientY : e.touches[0].clientY; + + const { x, y } = li.getBoundingClientRect(); + + const hitArea = { width: 26, height: 24 }; + if ( + clientX >= x - hitArea.width && + clientX <= x && + clientY >= y && + clientY <= y + hitArea.height + ) { + const pos = typeof getPos === "function" ? getPos() : 0; + if (!pos) return; + + e.preventDefault(); + editor.commands.toggleOutlineCollapse( + pos, + !li.classList.contains("collapsed") + ); + } + } + + li.onmousedown = onClick; + li.ontouchstart = onClick; + + return { + dom: li, + contentDOM: li, + update: (updatedNode) => { + if (updatedNode.type !== this.type) { + return false; + } + const isNested = + updatedNode.lastChild?.type.name === OutlineList.name; + + if (updatedNode.attrs.collapsed) li.classList.add("collapsed"); + else li.classList.remove("collapsed"); + + if (isNested) li.classList.add("nested"); + else li.classList.remove("nested"); + + return true; + } + }; + }; } }); @@ -139,17 +209,4 @@ function findSublist(editor: Editor, type: NodeType) { const subListPos = listItem.pos + subList.pos + 1; return { isCollapsed, isNested, subListPos }; - // return ( - // this.editor - // .chain() - // .command(({ tr }) => { - // tr.setNodeMarkup(listItem.pos + subList.pos + 1, undefined, { - // collapsed: !isCollapsed, - // }); - // return true; - // }) - // //.setTextSelection(listItem.pos + subList.pos + 1) - // //.splitListItem(this.name) - // .run() - // ); } diff --git a/packages/editor/src/extensions/outline-list/component.tsx b/packages/editor/src/extensions/outline-list/component.tsx deleted file mode 100644 index 0500eb79a..000000000 --- a/packages/editor/src/extensions/outline-list/component.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* -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 { Text } from "@theme-ui/components"; -import { ReactNodeViewProps } from "../react"; -import { useMemo } from "react"; -import { OutlineListAttributes } from "./outline-list"; -import { OutlineListItem } from "../outline-list-item"; - -export function OutlineListComponent( - props: ReactNodeViewProps -) { - const { editor, getPos, node, forwardRef } = props; - const { collapsed, textDirection } = node.attrs; - - const isNested = useMemo(() => { - const pos = editor.state.doc.resolve(getPos()); - return pos.parent?.type.name === OutlineListItem.name; - }, [editor, getPos]); - - return ( - <> - - - ); -} diff --git a/packages/editor/src/extensions/outline-list/outline-list.ts b/packages/editor/src/extensions/outline-list/outline-list.ts index 459519d4c..4fc73efe5 100644 --- a/packages/editor/src/extensions/outline-list/outline-list.ts +++ b/packages/editor/src/extensions/outline-list/outline-list.ts @@ -18,8 +18,6 @@ along with this program. If not, see . */ import { Node, mergeAttributes, wrappingInputRule } from "@tiptap/core"; -import { createNodeView } from "../react"; -import { OutlineListComponent } from "./component"; export type OutlineListAttributes = { collapsed: boolean; @@ -51,19 +49,6 @@ export const OutlineList = Node.create({ }; }, - addAttributes() { - return { - collapsed: { - default: false, - keepOnSplit: false, - parseHTML: (element) => element.dataset.collapsed === "true", - renderHTML: (attributes) => ({ - "data-collapsed": attributes.collapsed === true - }) - } - }; - }, - group: "block list", content: `${outlineListItemName}+`, @@ -113,13 +98,13 @@ export const OutlineList = Node.create({ }, addNodeView() { - return createNodeView(OutlineListComponent, { - contentDOMFactory: () => { - const content = document.createElement("ul"); - content.classList.add(`${this.name.toLowerCase()}-content-wrapper`); - content.style.whiteSpace = "inherit"; - return { dom: content }; - } - }); + return () => { + const ul = document.createElement("ul"); + ul.classList.add("outline-list"); + return { + dom: ul, + contentDOM: ul + }; + }; } }); diff --git a/packages/editor/styles/styles.css b/packages/editor/styles/styles.css index 77ec0bf6a..fee6edf99 100644 --- a/packages/editor/styles/styles.css +++ b/packages/editor/styles/styles.css @@ -41,7 +41,7 @@ border: 2px solid var(--primary); } -.ProseMirror code:not(pre code){ +.ProseMirror code:not(pre code) { background-color: var(--bgSecondary); border: 1px solid var(--border); border-radius: 5px; @@ -51,7 +51,7 @@ font-size: 10pt !important; } -.ProseMirror code > span { +.ProseMirror code > span { font-family: ui-monospace, SFMono-Regular, SF Mono, Consolas, Liberation Mono, Menlo, monospace !important; } @@ -64,7 +64,8 @@ margin-bottom: 5px; } -.ProseMirror ul ul, .ProseMirror ol ol { +.ProseMirror ul ul, +.ProseMirror ol ol { margin-top: 5px; } @@ -90,8 +91,7 @@ } .ProseMirror > div.codeblock-view-content-wrap, -.ProseMirror > div.taskList-view-content-wrap, -.ProseMirror > div.outlineList-view-content-wrap { +.ProseMirror > div.taskList-view-content-wrap { margin-top: 1em; margin-bottom: 1em; } @@ -471,6 +471,71 @@ p > *::selection { padding-top: 2px; } */ +/* Outline lists */ + +.ProseMirror > .outline-list { + padding-left: 18px; +} + +.outline-list { + list-style-type: none; + position: relative; +} + +.outline-list li ul { + padding-left: 25px; +} + +.outline-list li.collapsed .outline-list { + display: none; +} +.outline-list li > p { + position: relative; +} + +.outline-list li > p::before { + position: absolute; + top: 4px; + left: -22px; + cursor: pointer; + content: ""; + background-size: 18px; + width: 18px; + height: 18px; + + background-color: var(--icon); + mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9IiM4ODg4ODgiIGQ9Ik03LjQxIDguNThMMTIgMTMuMTdsNC41OS00LjU5TDE4IDEwbC02IDZsLTYtNmwxLjQxLTEuNDJaIi8+PC9zdmc+) + no-repeat 50% 50%; + mask-size: cover; + border: 1px solid var(--background); + + transform: rotate(0); + transition: transform 250ms ease; +} + +.outline-list li:not(.nested) > p::before { + mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxZW0iIGhlaWdodD0iMWVtIiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGZpbGw9IiM4ODg4ODgiIGQ9Ik0xMiAyQTEwIDEwIDAgMCAwIDIgMTJhMTAgMTAgMCAwIDAgMTAgMTBhMTAgMTAgMCAwIDAgMTAtMTBBMTAgMTAgMCAwIDAgMTIgMloiLz48L3N2Zz4=) + no-repeat 50% 50%; + scale: 0.4; +} + +.outline-list li.collapsed > p::before { + transform: rotate(-90deg); +} + +.outline-list li .outline-list::before { + content: " "; + position: absolute; + height: 100%; + left: -14px; + border-left: 1px solid var(--border); + transition: border 200ms ease-in-out; +} + +.outline-list li .outline-list:hover:before { + border-left: 1px solid var(--bgSecondaryHover); +} + /* RTL */ [dir="rtl"] * { @@ -498,4 +563,4 @@ blockquote[dir="rtl"] { [dir="rtl"] .outline-list-item-toggle:not(.rtl) { display: none; -} \ No newline at end of file +}