mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-24 04:00:59 +01:00
editor: migrate to vanilla js outline list
This commit is contained in:
committed by
Abdullah Atta
parent
882217be63
commit
46a635d4f4
@@ -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());
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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()
|
||||
// );
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user