diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index bbbcba5..621316b 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -3,6 +3,8 @@ import { motion } from 'framer-motion'; import { Properties } from '../Properties/Properties'; import { IContainerModel } from '../../Interfaces/ContainerModel'; import { getDepth, MakeIterator } from '../../utils/itertools'; +import { Menu } from '../Menu/Menu'; +import { MenuItem } from '../Menu/MenuItem'; interface IElementsSidebarProps { MainContainer: IContainerModel | null @@ -11,9 +13,77 @@ interface IElementsSidebarProps { SelectedContainer: IContainerModel | null onPropertyChange: (key: string, value: string) => void selectContainer: (container: IContainerModel) => void + deleteContainer: (containerid: string) => void +} + +interface Point { + x: number + y: number +} + +interface IElementsSidebarState { + isContextMenuOpen: boolean + contextMenuPosition: Point + onClickContainerId: string } export class ElementsSidebar extends React.PureComponent { + public state: IElementsSidebarState; + public elementRef: React.RefObject; + + 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 iterateChilds(handleContainer: (container: IContainerModel) => void): React.ReactNode { if (this.props.MainContainer == null) { return null; @@ -56,6 +126,7 @@ export class ElementsSidebar extends React.PureComponent `w-full elements-sidebar-row whitespace-pre text-left text-sm font-medium transition-all ${selectedClass}` } + id={key} key={key} onClick={() => this.props.selectContainer(container)}> { text } @@ -68,9 +139,17 @@ export class ElementsSidebar extends React.PureComponent
Elements
-
+
{ containerRows }
+ + this.props.deleteContainer(this.state.onClickContainerId)} /> +
); diff --git a/src/Components/Menu/Menu.tsx b/src/Components/Menu/Menu.tsx new file mode 100644 index 0000000..fde491f --- /dev/null +++ b/src/Components/Menu/Menu.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; + +interface IMenuProps { + className?: string + x: number + y: number + isOpen: boolean + children: React.ReactNode[] | React.ReactNode +} + +export const Menu: React.FC = (props) => { + const visible = props.isOpen ? 'visible opacity-1' : 'invisible opacity-0'; + return ( +
+ { props.children } +
+ ); +}; diff --git a/src/Components/Menu/MenuItem.tsx b/src/Components/Menu/MenuItem.tsx new file mode 100644 index 0000000..9edc8ac --- /dev/null +++ b/src/Components/Menu/MenuItem.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; + +interface IMenuItemProps { + className?: string + text: string + onClick: () => void +} + +export const MenuItem: React.FC = (props) => { + return ( + + ); +}; diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index 37420bd..46ae454 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -15,6 +15,7 @@ interface IUIProps { historyCurrentStep: number AvailableContainers: AvailableContainer[] SelectContainer: (container: ContainerModel) => void + DeleteContainer: (containerId: string) => void OnPropertyChange: (key: string, value: string) => void AddContainer: (type: string) => void SaveEditorAsJSON: () => void @@ -97,6 +98,7 @@ export class UI extends React.PureComponent { isHistoryOpen={this.state.isHistoryOpen} onPropertyChange={this.props.OnPropertyChange} selectContainer={this.props.SelectContainer} + deleteContainer={this.props.DeleteContainer} /> { }); } + public DeleteContainer(containerId: string): void { + const history = this.getCurrentHistory(); + const current = history[this.state.historyCurrentStep]; + + if (current.MainContainer === null) { + throw new Error('[DeleteContainer] Error: Tried to delete a container without a main container'); + } + + const mainContainerClone: IContainerModel = structuredClone(current.MainContainer); + const container = findContainerById(mainContainerClone, containerId); + + if (container === undefined) { + throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`); + } + + if (container === mainContainerClone) { + // TODO: Implement alert + throw new Error('[DeleteContainer] Tried to delete the main container! Deleting the main container is not allowed !'); + } + + if (container === null || container === undefined) { + throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); + } + + if (container.parent != null) { + const index = container.parent.children.indexOf(container); + if (index > -1) { + container.parent.children.splice(index, 1); + } + } + + this.setState( + { + history: history.concat([{ + SelectedContainer: null, + SelectedContainerId: '', + MainContainer: mainContainerClone, + TypeCounters: Object.assign({}, current.TypeCounters) + }]), + historyCurrentStep: history.length + }); + } + /** * Handled the property change event in the properties form * @param key Property name @@ -286,6 +329,7 @@ class Editor extends React.Component { historyCurrentStep={this.state.historyCurrentStep} AvailableContainers={this.state.configuration.AvailableContainers} SelectContainer={(container) => this.SelectContainer(container)} + DeleteContainer={(containerId: string) => this.DeleteContainer(containerId)} OnPropertyChange={(key, value) => this.OnPropertyChange(key, value)} AddContainer={(type) => this.AddContainer(type)} SaveEditorAsJSON={() => this.SaveEditorAsJSON()} diff --git a/src/index.scss b/src/index.scss index 6b98e1e..1bc3361 100644 --- a/src/index.scss +++ b/src/index.scss @@ -44,4 +44,8 @@ text-xs font-bold transition-all duration-100 scale-0 origin-left; } + + .contextmenu-item { + @apply px-2 py-1 hover:bg-slate-300 text-left + } } \ No newline at end of file