222 lines
7 KiB
TypeScript
222 lines
7 KiB
TypeScript
import * as React from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { Properties } from '../Properties/Properties';
|
|
import { IContainerModel } from '../../Interfaces/ContainerModel';
|
|
import { findContainerById, getDepth, MakeIterator } from '../../utils/itertools';
|
|
import { Menu } from '../Menu/Menu';
|
|
import { MenuItem } from '../Menu/MenuItem';
|
|
|
|
interface IElementsSidebarProps {
|
|
MainContainer: IContainerModel | null
|
|
isOpen: boolean
|
|
isHistoryOpen: boolean
|
|
SelectedContainer: IContainerModel | null
|
|
onPropertyChange: (key: string, value: string) => void
|
|
SelectContainer: (container: IContainerModel) => void
|
|
DeleteContainer: (containerid: string) => void
|
|
AddContainer: (index: number, type: string, parent: string) => void
|
|
}
|
|
|
|
interface Point {
|
|
x: number
|
|
y: number
|
|
}
|
|
|
|
interface IElementsSidebarState {
|
|
isContextMenuOpen: boolean
|
|
contextMenuPosition: Point
|
|
onClickContainerId: string
|
|
}
|
|
|
|
export class ElementsSidebar extends React.PureComponent<IElementsSidebarProps> {
|
|
public state: IElementsSidebarState;
|
|
public elementRef: React.RefObject<HTMLDivElement>;
|
|
|
|
constructor(props: IElementsSidebarProps) {
|
|
super(props);
|
|
this.state = {
|
|
isContextMenuOpen: false,
|
|
contextMenuPosition: {
|
|
x: 0,
|
|
y: 0
|
|
},
|
|
onClickContainerId: ''
|
|
};
|
|
this.elementRef = React.createRef();
|
|
}
|
|
|
|
componentDidMount(): void {
|
|
this.elementRef.current?.addEventListener('contextmenu', (event) => this.handleRightClick(event));
|
|
window.addEventListener('click', (event) => this.handleLeftClick(event));
|
|
}
|
|
|
|
componentWillUnmount(): void {
|
|
this.elementRef.current?.removeEventListener('contextmenu', (event) => this.handleRightClick(event));
|
|
window.removeEventListener('click', (event) => this.handleLeftClick(event));
|
|
}
|
|
|
|
public handleRightClick(event: MouseEvent): void {
|
|
event.preventDefault();
|
|
|
|
if (!(event.target instanceof HTMLButtonElement)) {
|
|
this.setState({
|
|
isContextMenuOpen: false,
|
|
onClickContainerId: ''
|
|
});
|
|
return;
|
|
}
|
|
|
|
const contextMenuPosition: Point = { x: event.pageX, y: event.pageY };
|
|
this.setState({
|
|
isContextMenuOpen: true,
|
|
contextMenuPosition,
|
|
onClickContainerId: event.target.id
|
|
});
|
|
}
|
|
|
|
public handleLeftClick(event: MouseEvent): void {
|
|
if (!this.state.isContextMenuOpen) {
|
|
return;
|
|
}
|
|
|
|
this.setState({
|
|
isContextMenuOpen: false,
|
|
onClickContainerId: ''
|
|
});
|
|
}
|
|
|
|
public handleDragOver(event: React.DragEvent): void {
|
|
event.preventDefault();
|
|
}
|
|
|
|
public handleOnDrop(event: React.DragEvent): void {
|
|
event.preventDefault();
|
|
const type = event.dataTransfer.getData('type');
|
|
const target: HTMLButtonElement = event.target as HTMLButtonElement;
|
|
|
|
if (this.props.MainContainer === null) {
|
|
throw new Error('[handleOnDrop] Tried to drop into the tree without a required MainContainer!');
|
|
}
|
|
|
|
const targetContainer: IContainerModel | undefined = findContainerById(
|
|
this.props.MainContainer,
|
|
target.id
|
|
);
|
|
|
|
if (targetContainer === undefined) {
|
|
throw new Error('[handleOnDrop] Tried to drop onto a unknown container!');
|
|
}
|
|
|
|
if (targetContainer === this.props.MainContainer) {
|
|
// if the container is the root, only add type as child
|
|
this.props.AddContainer(
|
|
targetContainer.children.length,
|
|
type,
|
|
targetContainer.properties.id);
|
|
return;
|
|
}
|
|
|
|
if (targetContainer.parent === null ||
|
|
targetContainer.parent === undefined) {
|
|
throw new Error('[handleDrop] Tried to drop into a child container without a parent!');
|
|
}
|
|
|
|
const rect = target.getBoundingClientRect();
|
|
const y = event.clientY - rect.top; // y position within the element.
|
|
|
|
// locate the hitboxes
|
|
if (y < 12) {
|
|
const index = targetContainer.parent.children.indexOf(targetContainer);
|
|
this.props.AddContainer(
|
|
index,
|
|
type,
|
|
targetContainer.parent.properties.id
|
|
);
|
|
} else if (y < 24) {
|
|
this.props.AddContainer(
|
|
targetContainer.children.length,
|
|
type,
|
|
targetContainer.properties.id);
|
|
} else {
|
|
const index = targetContainer.parent.children.indexOf(targetContainer);
|
|
this.props.AddContainer(
|
|
index + 1,
|
|
type,
|
|
targetContainer.parent.properties.id
|
|
);
|
|
}
|
|
}
|
|
|
|
public iterateChilds(handleContainer: (container: IContainerModel) => void): React.ReactNode {
|
|
if (this.props.MainContainer == null) {
|
|
return null;
|
|
}
|
|
|
|
const it = MakeIterator(this.props.MainContainer);
|
|
for (const container of it) {
|
|
handleContainer(container);
|
|
}
|
|
}
|
|
|
|
public render(): JSX.Element {
|
|
let isOpenClasses = '-right-64';
|
|
if (this.props.isOpen) {
|
|
isOpenClasses = this.props.isHistoryOpen
|
|
? 'right-64'
|
|
: 'right-0';
|
|
}
|
|
|
|
const containerRows: React.ReactNode[] = [];
|
|
this.iterateChilds((container: IContainerModel) => {
|
|
const depth: number = getDepth(container);
|
|
const key = container.properties.id.toString();
|
|
const text = '|\t'.repeat(depth) + key;
|
|
const selectedClass: string = this.props.SelectedContainer !== undefined &&
|
|
this.props.SelectedContainer !== null &&
|
|
this.props.SelectedContainer.properties.id === container.properties.id
|
|
? 'border-l-4 border-blue-500 bg-slate-400/60 hover:bg-slate-400'
|
|
: 'bg-slate-300/60 hover:bg-slate-300';
|
|
containerRows.push(
|
|
<motion.button
|
|
whileHover={{ scale: 1.05 }}
|
|
whileTap={{ scale: 1.2 }}
|
|
initial={{ opacity: 0, scale: 0 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{
|
|
duration: 0.150
|
|
}}
|
|
className={
|
|
`w-full elements-sidebar-row whitespace-pre
|
|
text-left text-sm font-medium transition-all ${selectedClass}`
|
|
}
|
|
id={key}
|
|
key={key}
|
|
onDrop={(event) => this.handleOnDrop(event)}
|
|
onDragOver={(event) => this.handleDragOver(event)}
|
|
onClick={() => this.props.SelectContainer(container)}>
|
|
{ text }
|
|
</motion.button>
|
|
);
|
|
});
|
|
|
|
return (
|
|
<div className={`fixed flex flex-col bg-slate-100 text-gray-800 transition-all h-screen w-64 overflow-y-auto z-20 ${isOpenClasses}`}>
|
|
<div className='bg-slate-100 font-bold sidebar-title'>
|
|
Elements
|
|
</div>
|
|
<div ref={this.elementRef} className='overflow-y-auto overflow-x-hidden text-gray-800 flex-grow'>
|
|
{ containerRows }
|
|
</div>
|
|
<Menu
|
|
className='transition-opacity rounded bg-slate-200 py-1 drop-shadow-xl'
|
|
x={this.state.contextMenuPosition.x}
|
|
y={this.state.contextMenuPosition.y}
|
|
isOpen={this.state.isContextMenuOpen}
|
|
>
|
|
<MenuItem className='contextmenu-item' text='Delete' onClick={() => this.props.DeleteContainer(this.state.onClickContainerId)} />
|
|
</Menu>
|
|
<Properties properties={this.props.SelectedContainer?.properties} onChange={this.props.onPropertyChange}></Properties>
|
|
</div>
|
|
);
|
|
}
|
|
}
|