From 1acf8cc655f5ac67590bc65e50c0e55beecaf539 Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Wed, 19 Feb 2025 14:02:36 +0500 Subject: [PATCH] web: use virtualized tree for rendering toc --- .../components/editor/table-of-contents.tsx | 166 +++++++++++------- 1 file changed, 107 insertions(+), 59 deletions(-) diff --git a/apps/web/src/components/editor/table-of-contents.tsx b/apps/web/src/components/editor/table-of-contents.tsx index b21dbecbd..8ab1424d8 100644 --- a/apps/web/src/components/editor/table-of-contents.tsx +++ b/apps/web/src/components/editor/table-of-contents.tsx @@ -36,17 +36,22 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import React, { useEffect, useState } from "react"; -import { Cross } from "../icons"; +import React, { useEffect, useLayoutEffect, useState } from "react"; +import { ChevronDown, ChevronRight, Circle, Cross } from "../icons"; import { useEditorStore } from "../../stores/editor-store"; import ScrollContainer from "../scroll-container"; import { ScopedThemeProvider } from "../theme-provider"; import { Section } from "../properties"; -import { scrollIntoViewById } from "@notesnook/editor"; +import { scrollIntoViewById, TOCItem } from "@notesnook/editor"; import { Button, Flex, Text } from "@theme-ui/components"; import { useEditorManager } from "./manager"; import { TITLE_BAR_HEIGHT } from "../title-bar"; import { strings } from "@notesnook/intl"; +import { + TreeNode, + VirtualizedTree, + VirtualizedTreeHandle +} from "../virtualized-tree"; type TableOfContentsProps = { sessionId: string; @@ -54,34 +59,35 @@ type TableOfContentsProps = { function TableOfContents(props: TableOfContentsProps) { const { sessionId } = props; - const [active, setActive] = useState([]); - const toggleTableOfContents = useEditorStore( - (store) => store.toggleTableOfContents - ); + const treeRef = React.useRef>(null); const tableOfContents = useEditorManager( (store) => store.editors[sessionId]?.tableOfContents || [] ); - useEffect(() => { + useLayoutEffect(() => { + treeRef.current?.refresh(); const editorScroll = document.getElementById(`editorScroll_${sessionId}`); - if (!editorScroll) return; function onScroll() { const scrollTop = editorScroll?.scrollTop || 0; const height = editorScroll?.clientHeight || 0; - const active = tableOfContents.filter((t, i, array) => { - const next = array.at(i + 1); + for (let i = 0; i < tableOfContents.length; i++) { + const toc = tableOfContents[i]; + const element = document.getElementById(`toc-${toc.id}`); + if (!element) continue; + + const next = tableOfContents.at(i + 1); const viewport = scrollTop + height - 160; const lessThanNext = next ? scrollTop <= next.top : true; - const isInViewport = t.top <= viewport && t.top >= scrollTop; - const isActive = scrollTop >= t.top && lessThanNext; - return isInViewport || isActive; - }); - setActive(active.map((a) => a.id)); + const isInViewport = toc.top <= viewport && toc.top >= scrollTop; + const isActive = scrollTop >= toc.top && lessThanNext; + + element.classList.toggle("active", isActive || isInViewport); + } } - editorScroll.addEventListener("scroll", onScroll); + editorScroll?.addEventListener("scroll", onScroll); onScroll(); return () => { - editorScroll.removeEventListener("scroll", onScroll); + editorScroll?.removeEventListener("scroll", onScroll); }; }, [sessionId, tableOfContents]); @@ -107,58 +113,100 @@ function TableOfContents(props: TableOfContentsProps) { flexDirection: "column" }} > - -
toggleTableOfContents(false)} - size={18} - sx={{ mr: 1, cursor: "pointer" }} - /> - } +
+ - - {tableOfContents.length <= 0 ? ( - - {strings.noHeadingsFound()}. - - ) : ( - tableOfContents.map((t) => ( + {tableOfContents.length <= 0 ? ( + + {strings.noHeadingsFound()}. + + ) : ( + { + const nodes: TreeNode[] = []; + for (let i = 0; i < tableOfContents.length; i++) { + const item = tableOfContents[i]; + if (item.level !== depth + 1) continue; + nodes.push({ + id: item.id, + data: item, + depth: depth + 1, + parentId: id, + hasChildren: + i + 1 < tableOfContents.length && + tableOfContents[i + 1].level > item.level, + expanded: true + }); + } + return nodes; + }} + renderItem={({ item, collapse, expand, expanded }) => ( + ) : ( + + )} + {item.data.title} - )) - )} - -
- + )} + > + )} + +
);