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 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<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 {
// States
const divRef = React.useRef<HTMLDivElement>(null);
const [isDragActive, setDragActive] = React.useState<boolean>(false);
const [,height] = useSize(divRef);
const defaultElementRows = GetContainerRowsState(props.mainContainer, props.containers);
const [elementRows, setElementRows] = React.useState<Map<string, IElementRow>>(
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(<span
key={`${key}-${i}`}
@ -281,6 +369,25 @@ function ElementsListRow(
></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}>
<button type="button"
className={`transition-all ${isDragActive
@ -293,6 +400,7 @@ function ElementsListRow(
key={key}
title={container.properties.warning}
onClick={() => { selectContainer(container.properties.id); }}
onDoubleClick={() => { toggleExpandContainer(container.properties.id); }}
onDrop={(event) => {
HandleOnDrop(event, containers, mainContainer, addContainer);
}}
@ -303,6 +411,7 @@ function ElementsListRow(
{text}
{container.properties.warning.length > 0 &&
<ExclamationTriangleIcon className='pl-2 w-7' />}
{expandButton}
</button>
{isDragActive &&
<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
};
if (enableHideChildrenInTreeview && root.properties.hideChildrenInTreeview) {
if (
(enableHideChildrenInTreeview && root.properties.hideChildrenInTreeview) ||
('isClosed' in root && root.isClosed === true)
) {
return;
}