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 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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue