From 623003b60cfa6382d0b0ef11c63292b4e5db0598 Mon Sep 17 00:00:00 2001 From: Eric NGUYEN Date: Fri, 24 Feb 2023 12:38:42 +0100 Subject: [PATCH] Implement closable treeview --- .../ElementsSidebar/ElementsSidebar.tsx | 125 ++++++++++++++++-- src/utils/itertools.ts | 5 +- 2 files changed, 121 insertions(+), 9 deletions(-) diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index 11bffe4..222be2e 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import useSize from '@react-hook/size'; import { FixedSizeList as List } from 'react-window'; -import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; +import { ChevronRightIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline'; import { ContainerProperties } from '../ContainerProperties/ContainerProperties'; import { type IContainerModel } from '../../Interfaces/IContainerModel'; import { FindContainerById, MakeRecursionDFSIterator } from '../../utils/itertools'; @@ -34,9 +34,13 @@ function RemoveBorderClasses(target: HTMLElement, exception: string = ''): void function HandleDragLeave(event: React.DragEvent): void { let target: HTMLButtonElement = event.target as HTMLButtonElement; + // TODO: Find a better solution that this vvv if ((target.parentElement?.classList.contains('elements-sidebar-row')) ?? false) { target = target.parentElement as HTMLButtonElement; } + if ((target.parentElement?.parentElement?.classList.contains('elements-sidebar-row')) ?? false) { + target = target.parentElement?.parentElement as HTMLButtonElement; + } RemoveBorderClasses(target); } @@ -48,9 +52,13 @@ function HandleDragOver( event.preventDefault(); let target = event.target as HTMLElement; + // TODO: Find a better solution that this vvv if ((target.parentElement?.classList.contains('elements-sidebar-row')) ?? false) { target = target.parentElement as HTMLButtonElement; } + if ((target.parentElement?.parentElement?.classList.contains('elements-sidebar-row')) ?? false) { + target = target.parentElement?.parentElement as HTMLButtonElement; + } const rect = target.getBoundingClientRect(); const y = event.clientY - rect.top; // y position within the element. @@ -82,9 +90,13 @@ function HandleOnDrop( const type = event.dataTransfer.getData('type'); let target: HTMLButtonElement = event.target as HTMLButtonElement; + // TODO: Find a better solution that this vvv if ((target.parentElement?.classList.contains('elements-sidebar-row')) ?? false) { target = target.parentElement as HTMLButtonElement; } + if ((target.parentElement?.parentElement?.classList.contains('elements-sidebar-row')) ?? false) { + target = target.parentElement?.parentElement as HTMLButtonElement; + } RemoveBorderClasses(target); @@ -159,17 +171,87 @@ function useDragComponentsListener( }); } +/** + * Hook to update elementsRows state when the dictionary of containers update + * @param mainContainer Root container + * @param containers Dictionary of containers + * @param oldElementRows Old dictionary of rows + * @param setElementRows Setter of the dictionary of rows + */ +function useElementRows( + mainContainer: IContainerModel, + containers: Map, + oldElementRows: Map, + setElementRows: (newContainers: Map) => void +): void { + React.useEffect(() => { + const newContainerRows = GetContainerRowsState(mainContainer, containers, oldElementRows); + setElementRows(newContainerRows); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mainContainer, containers, setElementRows]); +} + +/** + * Toggle a row in the tree + * @param id Id of the container to close/open + * @param elementRows Dictionary of containers + */ +function ToggleExpandRow( + id: string, + elementRows: Map, + setElementRows: (newElementRows: Map) => void +): void { + const row = elementRows.get(id); + if (row === undefined) { + return; + } + row.isClosed = !row.isClosed; + setElementRows(new Map(elementRows)); +} + +interface IElementRow extends IContainerModel { + depth: number + isClosed: boolean + isSelected: boolean +} + +function GetContainerRowsState( + mainContainer: IContainerModel, + containers: Map, + oldContainerRows?: Map | undefined +): Map { + const containerRows = new Map(); + const it = MakeRecursionDFSIterator(mainContainer, containers, 0, [0, 0], true); + for (const { depth, container } of it) { + const oldContainerRow = oldContainerRows?.get(container.properties.id); + const containerRow = { + ...container, + depth, + isClosed: oldContainerRow?.isClosed ?? false, + isSelected: oldContainerRow?.isSelected ?? false + }; + + containerRows.set(container.properties.id, containerRow); + } + return containerRows; +} + export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element { // States const divRef = React.useRef(null); const [isDragActive, setDragActive] = React.useState(false); const [,height] = useSize(divRef); + const defaultElementRows = GetContainerRowsState(props.mainContainer, props.containers); + const [elementRows, setElementRows] = React.useState>( + defaultElementRows + ); // Hooks useDragComponentsListener(setDragActive); + useElementRows(props.mainContainer, props.containers, elementRows, setElementRows); // Render - const it = MakeRecursionDFSIterator(props.mainContainer, props.containers, 0, [0, 0], true); + const it = MakeRecursionDFSIterator(props.mainContainer, elementRows, 0, [0, 0], true); const containers = [...it]; function Row({ index, style @@ -177,7 +259,8 @@ export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element { index: number style: React.CSSProperties }): JSX.Element { - const { container, depth } = containers[index]; + const { container: containerRow, depth } = containers[index]; + const container = containerRow as IElementRow; const key = container.properties.id.toString(); const text = container.properties.displayedText === key ? `${key}` @@ -200,7 +283,8 @@ export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element { props.mainContainer, isDragActive, props.addContainer, - props.selectContainer + props.selectContainer, + (containerId) => { ToggleExpandRow(containerId, elementRows, setElementRows); } ) ); } @@ -246,7 +330,7 @@ export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element { function ElementsListRow( key: string, index: number, - container: IContainerModel, + container: IElementRow, depth: number, isSelected: boolean, style: React.CSSProperties, @@ -255,15 +339,17 @@ function ElementsListRow( mainContainer: IContainerModel, isDragActive: boolean, addContainer: (index: number, type: string, parent: string) => void, - selectContainer: (containerId: string) => void + selectContainer: (containerId: string) => void, + toggleExpandContainer: (containerId: string) => void ): JSX.Element { - const verticalBars: JSX.Element[] = []; - + // Style let buttonClass = 'bg-blue-500 shadow-lg shadow-blue-500/60' + ' hover:bg-blue-600 hover:shadow-blue-500 text-slate-50 border-blue-600'; + let chevronClass = 'border-slate-50'; let verticalBarClass = 'border-l-blue-400 group-hover:border-l-blue-300'; if (!isSelected) { const isPair = (index & 1) === 0; + chevronClass = 'border-slate-600'; if (isPair) { buttonClass = 'bg-slate-300/20 border-slate-500/50'; } else { @@ -274,6 +360,8 @@ function ElementsListRow( buttonClass += ' hover:bg-slate-400 hover:shadow-slate-400'; } + // Vertical bars + const verticalBars: JSX.Element[] = []; for (let i = 0; i < depth; i++) { verticalBars.push(); } + // Expand button + let expandButton; + if (container.children.length > 0 && container !== mainContainer) { + expandButton = ( + + ); + } + return
{isDragActive &&
diff --git a/src/utils/itertools.ts b/src/utils/itertools.ts index 7f4b518..2dbbbdd 100644 --- a/src/utils/itertools.ts +++ b/src/utils/itertools.ts @@ -105,7 +105,10 @@ export function * MakeRecursionDFSIterator( currentTransform }; - if (enableHideChildrenInTreeview && root.properties.hideChildrenInTreeview) { + if ( + (enableHideChildrenInTreeview && root.properties.hideChildrenInTreeview) || + ('isClosed' in root && root.isClosed === true) + ) { return; }