Implement closable treeview

This commit is contained in:
Eric NGUYEN 2023-02-24 12:38:42 +01:00
parent e1592f56e7
commit 623003b60c
2 changed files with 121 additions and 9 deletions

View file

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import useSize from '@react-hook/size'; import useSize from '@react-hook/size';
import { FixedSizeList as List } from 'react-window'; 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 { ContainerProperties } from '../ContainerProperties/ContainerProperties';
import { type IContainerModel } from '../../Interfaces/IContainerModel'; import { type IContainerModel } from '../../Interfaces/IContainerModel';
import { FindContainerById, MakeRecursionDFSIterator } from '../../utils/itertools'; import { FindContainerById, MakeRecursionDFSIterator } from '../../utils/itertools';
@ -34,9 +34,13 @@ function RemoveBorderClasses(target: HTMLElement, exception: string = ''): void
function HandleDragLeave(event: React.DragEvent): void { function HandleDragLeave(event: React.DragEvent): void {
let target: HTMLButtonElement = event.target as HTMLButtonElement; let target: HTMLButtonElement = event.target as HTMLButtonElement;
// TODO: Find a better solution that this vvv
if ((target.parentElement?.classList.contains('elements-sidebar-row')) ?? false) { if ((target.parentElement?.classList.contains('elements-sidebar-row')) ?? false) {
target = target.parentElement as HTMLButtonElement; target = target.parentElement as HTMLButtonElement;
} }
if ((target.parentElement?.parentElement?.classList.contains('elements-sidebar-row')) ?? false) {
target = target.parentElement?.parentElement as HTMLButtonElement;
}
RemoveBorderClasses(target); RemoveBorderClasses(target);
} }
@ -48,9 +52,13 @@ function HandleDragOver(
event.preventDefault(); event.preventDefault();
let target = event.target as HTMLElement; let target = event.target as HTMLElement;
// TODO: Find a better solution that this vvv
if ((target.parentElement?.classList.contains('elements-sidebar-row')) ?? false) { if ((target.parentElement?.classList.contains('elements-sidebar-row')) ?? false) {
target = target.parentElement as HTMLButtonElement; 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 rect = target.getBoundingClientRect();
const y = event.clientY - rect.top; // y position within the element. const y = event.clientY - rect.top; // y position within the element.
@ -82,9 +90,13 @@ function HandleOnDrop(
const type = event.dataTransfer.getData('type'); const type = event.dataTransfer.getData('type');
let target: HTMLButtonElement = event.target as HTMLButtonElement; let target: HTMLButtonElement = event.target as HTMLButtonElement;
// TODO: Find a better solution that this vvv
if ((target.parentElement?.classList.contains('elements-sidebar-row')) ?? false) { if ((target.parentElement?.classList.contains('elements-sidebar-row')) ?? false) {
target = target.parentElement as HTMLButtonElement; target = target.parentElement as HTMLButtonElement;
} }
if ((target.parentElement?.parentElement?.classList.contains('elements-sidebar-row')) ?? false) {
target = target.parentElement?.parentElement as HTMLButtonElement;
}
RemoveBorderClasses(target); 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<string, IContainerModel>,
oldElementRows: Map<string, IElementRow>,
setElementRows: (newContainers: Map<string, IElementRow>) => 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<string, IElementRow>,
setElementRows: (newElementRows: Map<string, IElementRow>) => 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<string, IContainerModel>,
oldContainerRows?: Map<string, IElementRow> | undefined
): Map<string, IElementRow> {
const containerRows = new Map<string, IElementRow>();
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 { export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element {
// States // States
const divRef = React.useRef<HTMLDivElement>(null); const divRef = React.useRef<HTMLDivElement>(null);
const [isDragActive, setDragActive] = React.useState<boolean>(false); const [isDragActive, setDragActive] = React.useState<boolean>(false);
const [,height] = useSize(divRef); const [,height] = useSize(divRef);
const defaultElementRows = GetContainerRowsState(props.mainContainer, props.containers);
const [elementRows, setElementRows] = React.useState<Map<string, IElementRow>>(
defaultElementRows
);
// Hooks // Hooks
useDragComponentsListener(setDragActive); useDragComponentsListener(setDragActive);
useElementRows(props.mainContainer, props.containers, elementRows, setElementRows);
// Render // 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]; const containers = [...it];
function Row({ function Row({
index, style index, style
@ -177,7 +259,8 @@ export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element {
index: number index: number
style: React.CSSProperties style: React.CSSProperties
}): JSX.Element { }): 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 key = container.properties.id.toString();
const text = container.properties.displayedText === key const text = container.properties.displayedText === key
? `${key}` ? `${key}`
@ -200,7 +283,8 @@ export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element {
props.mainContainer, props.mainContainer,
isDragActive, isDragActive,
props.addContainer, props.addContainer,
props.selectContainer props.selectContainer,
(containerId) => { ToggleExpandRow(containerId, elementRows, setElementRows); }
) )
); );
} }
@ -246,7 +330,7 @@ export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element {
function ElementsListRow( function ElementsListRow(
key: string, key: string,
index: number, index: number,
container: IContainerModel, container: IElementRow,
depth: number, depth: number,
isSelected: boolean, isSelected: boolean,
style: React.CSSProperties, style: React.CSSProperties,
@ -255,15 +339,17 @@ function ElementsListRow(
mainContainer: IContainerModel, mainContainer: IContainerModel,
isDragActive: boolean, isDragActive: boolean,
addContainer: (index: number, type: string, parent: string) => void, addContainer: (index: number, type: string, parent: string) => void,
selectContainer: (containerId: string) => void selectContainer: (containerId: string) => void,
toggleExpandContainer: (containerId: string) => void
): JSX.Element { ): JSX.Element {
const verticalBars: JSX.Element[] = []; // Style
let buttonClass = 'bg-blue-500 shadow-lg shadow-blue-500/60' + 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'; ' 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'; let verticalBarClass = 'border-l-blue-400 group-hover:border-l-blue-300';
if (!isSelected) { if (!isSelected) {
const isPair = (index & 1) === 0; const isPair = (index & 1) === 0;
chevronClass = 'border-slate-600';
if (isPair) { if (isPair) {
buttonClass = 'bg-slate-300/20 border-slate-500/50'; buttonClass = 'bg-slate-300/20 border-slate-500/50';
} else { } else {
@ -274,6 +360,8 @@ function ElementsListRow(
buttonClass += ' hover:bg-slate-400 hover:shadow-slate-400'; buttonClass += ' hover:bg-slate-400 hover:shadow-slate-400';
} }
// Vertical bars
const verticalBars: JSX.Element[] = [];
for (let i = 0; i < depth; i++) { for (let i = 0; i < depth; i++) {
verticalBars.push(<span verticalBars.push(<span
key={`${key}-${i}`} key={`${key}-${i}`}
@ -281,6 +369,25 @@ function ElementsListRow(
></span>); ></span>);
} }
// Expand button
let expandButton;
if (container.children.length > 0 && container !== mainContainer) {
expandButton = (
<button
key={`${key}-chevron`}
className={'ml-2 w-5 h-5 group transition-all'}
onClick={() => { toggleExpandContainer(container.properties.id); }}>
<ChevronRightIcon
className={`rounded-full ${chevronClass}
hover:border-red-500 hover:text-red-500
border-2 transition-all text-lg ${container.isClosed
? ''
: 'rotate-90'}`}/>
</button>
);
}
return <div style={style}> return <div style={style}>
<button type="button" <button type="button"
className={`transition-all ${isDragActive className={`transition-all ${isDragActive
@ -293,6 +400,7 @@ function ElementsListRow(
key={key} key={key}
title={container.properties.warning} title={container.properties.warning}
onClick={() => { selectContainer(container.properties.id); }} onClick={() => { selectContainer(container.properties.id); }}
onDoubleClick={() => { toggleExpandContainer(container.properties.id); }}
onDrop={(event) => { onDrop={(event) => {
HandleOnDrop(event, containers, mainContainer, addContainer); HandleOnDrop(event, containers, mainContainer, addContainer);
}} }}
@ -303,6 +411,7 @@ function ElementsListRow(
{text} {text}
{container.properties.warning.length > 0 && {container.properties.warning.length > 0 &&
<ExclamationTriangleIcon className='pl-2 w-7' />} <ExclamationTriangleIcon className='pl-2 w-7' />}
{expandButton}
</button> </button>
{isDragActive && {isDragActive &&
<hr className='border-t-4 h-[4px] border-t-red-500 transition-all animate-pulse'></hr> <hr className='border-t-4 h-[4px] border-t-red-500 transition-all animate-pulse'></hr>

View file

@ -105,7 +105,10 @@ export function * MakeRecursionDFSIterator(
currentTransform currentTransform
}; };
if (enableHideChildrenInTreeview && root.properties.hideChildrenInTreeview) { if (
(enableHideChildrenInTreeview && root.properties.hideChildrenInTreeview) ||
('isClosed' in root && root.isClosed === true)
) {
return; return;
} }