editor: migrate to vanilla js outline list

This commit is contained in:
Abdullah Atta
2023-02-21 16:55:52 +05:00
committed by Abdullah Atta
parent 882217be63
commit 46a635d4f4
5 changed files with 155 additions and 282 deletions

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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 (
<Flex>
<Flex
className="outline"
sx={{
flexDirection: "column",
alignItems: "center",
mt: isMobile ? "0px" : "3px"
}}
>
{isNested ? (
<>
<ToggleIconButton
editor={editor}
isCollapsed={isCollapsed}
isMobile={isMobile}
node={node}
getPos={getPos}
textDirection={undefined}
/>
<ToggleIconButton
editor={editor}
isCollapsed={isCollapsed}
isMobile={isMobile}
node={node}
getPos={getPos}
textDirection={"rtl"}
/>
</>
) : (
<Icon
path={Icons.circle}
size={isMobile ? 24 : 18}
sx={{ transform: "scale(0.4)" }}
/>
)}
{isNested && !isCollapsed && (
<Box
sx={{
flex: 1,
width: 1,
mt: 2,
backgroundColor: "border",
borderRadius: 50,
flexShrink: 0,
cursor: "pointer",
transition: "all .2s ease-in-out",
":hover": {
backgroundColor: "fontTertiary",
width: 4
}
}}
contentEditable={false}
/>
)}
</Flex>
<Text
ref={forwardRef}
sx={{
pl: 1,
listStyleType: "none",
flex: 1
}}
/>
</Flex>
);
}
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 (
<Icon
className={`outline-list-item-toggle ${textDirection || ""}`}
path={
isCollapsed
? textDirection === "rtl"
? Icons.chevronLeft
: Icons.chevronRight
: Icons.chevronDown
}
title={
isCollapsed ? "Click to uncollapse list" : "Click to collapse list"
}
sx={{
cursor: "pointer",
transition: "all .2s ease-in-out",
":hover": {
transform: ["unset", "scale(1.3)"]
},
":active": {
transform: ["scale(1.3)", "unset"]
},
".icon:hover path": {
fill: "var(--checked) !important"
}
}}
size={isMobile ? 24 : 18}
onMouseDown={(e) => e.preventDefault()}
onTouchEnd={(e) => {
e.preventDefault();
toggleOutlineList(editor, node, isCollapsed, getPos());
}}
onClick={() => {
toggleOutlineList(editor, node, isCollapsed, getPos());
}}
/>
);
}

View File

@@ -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<string, unknown>;
@@ -46,6 +44,19 @@ export const OutlineListItem = Node.create<ListItemOptions>({
};
},
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<ListItemOptions>({
},
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()
// );
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<OutlineListAttributes>
) {
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 (
<>
<Text
className="outline-list"
as={"div"}
ref={forwardRef}
dir={textDirection}
sx={{
ul: {
display: collapsed ? "none" : "block",
paddingInlineStart: 0,
paddingLeft: 0,
marginBlockStart: isNested ? 1 : 0,
marginBlockEnd: 0
},
li: {
listStyleType: "none"
}
}}
/>
</>
);
}

View File

@@ -18,8 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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<OutlineListOptions>({
};
},
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<OutlineListOptions>({
},
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
};
};
}
});

View File

@@ -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;
}
}