From 09a009049fe9f0029d4f2dc45c264c21cdbc2867 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Tue, 9 Mar 2021 11:26:02 +0300 Subject: [PATCH] Expanding parent nodes when scrolling along child ones Signed-off-by: Alex Andreev --- package.json | 1 + .../components/scroll-spy/scroll-spy.tsx | 5 ++ .../components/tree-view/tree-view.tsx | 64 ++++++++++++++----- yarn.lock | 12 ++++ 4 files changed, 67 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 4bbea8b17d..fd108c0579 100644 --- a/package.json +++ b/package.json @@ -306,6 +306,7 @@ "concurrently": "^5.2.0", "css-element-queries": "^1.2.3", "css-loader": "^3.5.3", + "deepdash-es": "^5.3.5", "dompurify": "^2.0.11", "electron": "^9.4.0", "electron-builder": "^22.7.0", diff --git a/src/renderer/components/scroll-spy/scroll-spy.tsx b/src/renderer/components/scroll-spy/scroll-spy.tsx index eb6df9375b..62eac57701 100644 --- a/src/renderer/components/scroll-spy/scroll-spy.tsx +++ b/src/renderer/components/scroll-spy/scroll-spy.tsx @@ -29,6 +29,7 @@ export function ScrollSpy(props: Props) { sections.forEach(section => { const id = section.getAttribute("id"); + const parentId = section.parentElement.id; const name = section.querySelector("h1, h2, h3, h4, h5, h6").textContent; const selected = id === activeElementId; @@ -38,6 +39,7 @@ export function ScrollSpy(props: Props) { children.push({ id, + parentId, name, selected, children: getNavigation(section) @@ -56,6 +58,9 @@ export function ScrollSpy(props: Props) { const observeSections = () => { const options: IntersectionObserverInit = { threshold: [0], + // Shrinking root area from the bottom + // Allows to fire observer event only if target scrolled up to top of the page) + // https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#creating_an_intersection_observer rootMargin: "0px 0px -85%", }; diff --git a/src/renderer/components/tree-view/tree-view.tsx b/src/renderer/components/tree-view/tree-view.tsx index d565e8ec0f..be77092b86 100644 --- a/src/renderer/components/tree-view/tree-view.tsx +++ b/src/renderer/components/tree-view/tree-view.tsx @@ -1,14 +1,15 @@ import "./tree-view.scss"; -import React from "react"; +import React, { useEffect, useRef } from "react"; import { Icon } from "../icon"; import TreeView from "@material-ui/lab/TreeView"; import TreeItem from "@material-ui/lab/TreeItem"; import { cssNames } from "../../utils"; -import flattenDeep from "lodash/flattenDeep"; +import findDeep from "deepdash-es/findDeep"; export interface NavigationTree { id: string; + parentId: string; name: string; selected?: boolean; children?: NavigationTree[]; @@ -18,14 +19,49 @@ interface Props { data: NavigationTree[] } +const scrollToItem = (id: string) => { + const element = document.getElementById(id); + + element?.scrollIntoView(); +}; + +const getSelectedNode = (data: NavigationTree[]) => { + return findDeep(data, (value, key) => key === "selected" && value === true)?.parent; +}; + export function RecursiveTreeView({ data }: Props) { + const [expanded, setExpanded] = React.useState([]); + const prevData = useRef(data); + + const handleToggle = (event: React.ChangeEvent<{}>, nodeIds: string[]) => { + setExpanded(nodeIds); + }; + + const expandTopLevelNodes = () => { + setExpanded(data.map(node => node.id)); + }; + + const expandParentNode = () => { + const node = getSelectedNode(data) as any as NavigationTree; + const id = node?.parentId; + + if (id && !expanded.includes(id)) { + setExpanded([...expanded, id]); + } + }; + + const onLabelClick = (event: React.MouseEvent, nodeId: string) => { + event.preventDefault(); + scrollToItem(nodeId); + }; + const renderTree = (nodes: NavigationTree[]) => { return nodes.map(node => ( scrollToItem(node.id)} + onLabelClick={(event) => onLabelClick(event, node.id)} className={cssNames({selected: node.selected})} > {Array.isArray(node.children) ? node.children.map((node) => renderTree([node])) : null} @@ -33,17 +69,14 @@ export function RecursiveTreeView({ data }: Props) { )); }; - const scrollToItem = (id: string) => { - const element = document.getElementById(id); - - element?.scrollIntoView(); - }; - - const getAllNodeIds = (nodes: NavigationTree[]): string[] => { - return flattenDeep(nodes.map(node => { - return [node.id, ...node.children.map(item => getAllNodeIds([item]))]; - })); - }; + useEffect(() => { + if (!prevData.current.length) { + expandTopLevelNodes(); + } else { + expandParentNode(); + } + prevData.current = data; + }, [data]); if (!data.length) { return null; @@ -52,7 +85,8 @@ export function RecursiveTreeView({ data }: Props) { return ( } defaultExpandIcon={} > diff --git a/yarn.lock b/yarn.lock index e0c8816416..ef135e0aea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4381,6 +4381,13 @@ deep-is@^0.1.3, deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= +deepdash-es@^5.3.5: + version "5.3.5" + resolved "https://registry.yarnpkg.com/deepdash-es/-/deepdash-es-5.3.5.tgz#1217365f6508bb47f1f90def6b4f1c4b5f4ff6eb" + integrity sha512-bezxT+LqAu1Ly8I2LAEFle3fdEAc/bHld9cMAbgYzY+69+P9qTkGtNvC2ZQJEP4C1C2Fx9gVn/TCoQlAivWPDA== + dependencies: + lodash-es "^4.17.15" + deepmerge@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" @@ -8637,6 +8644,11 @@ lockfile@^1.0.4: dependencies: signal-exit "^3.0.2" +lodash-es@^4.17.15: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"