Implement closable treeview
This commit is contained in:
parent
e1592f56e7
commit
623003b60c
2 changed files with 121 additions and 9 deletions
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue