diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx index 0e58142..bca12ba 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx @@ -12,8 +12,8 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={null} onPropertyChange={() => {}} - selectContainer={() => {}} - deleteContainer={() => {}} + SelectContainer={() => {}} + DeleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -40,8 +40,8 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={null} onPropertyChange={() => {}} - selectContainer={() => {}} - deleteContainer={() => {}} + SelectContainer={() => {}} + DeleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -70,8 +70,8 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={MainContainer} onPropertyChange={() => {}} - selectContainer={() => {}} - deleteContainer={() => {}} + SelectContainer={() => {}} + DeleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -155,8 +155,8 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={MainContainer} onPropertyChange={() => {}} - selectContainer={() => {}} - deleteContainer={() => {}} + SelectContainer={() => {}} + DeleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -208,8 +208,8 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={SelectedContainer} onPropertyChange={() => {}} - selectContainer={selectContainer} - deleteContainer={() => {}} + SelectContainer={selectContainer} + DeleteContainer={() => {}} />); expect(screen.getByText(/Elements/i)); @@ -230,8 +230,8 @@ describe.concurrent('Elements sidebar', () => { isHistoryOpen={false} SelectedContainer={SelectedContainer} onPropertyChange={() => {}} - selectContainer={selectContainer} - deleteContainer={() => {}} + SelectContainer={selectContainer} + DeleteContainer={() => {}} />); expect((propertyId as HTMLInputElement).value === 'main').toBeFalsy(); diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index 621316b..0159b4d 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { motion } from 'framer-motion'; import { Properties } from '../Properties/Properties'; import { IContainerModel } from '../../Interfaces/ContainerModel'; -import { getDepth, MakeIterator } from '../../utils/itertools'; +import { findContainerById, getDepth, MakeIterator } from '../../utils/itertools'; import { Menu } from '../Menu/Menu'; import { MenuItem } from '../Menu/MenuItem'; @@ -12,8 +12,9 @@ interface IElementsSidebarProps { isHistoryOpen: boolean SelectedContainer: IContainerModel | null onPropertyChange: (key: string, value: string) => void - selectContainer: (container: IContainerModel) => void - deleteContainer: (containerid: string) => void + SelectContainer: (container: IContainerModel) => void + DeleteContainer: (containerid: string) => void + AddContainer: (index: number, type: string, parent: string) => void } interface Point { @@ -84,6 +85,97 @@ export class ElementsSidebar extends React.PureComponent }); } + public handleDragOver(event: React.DragEvent): void { + event.preventDefault(); + const target: HTMLButtonElement = event.target as HTMLButtonElement; + const rect = target.getBoundingClientRect(); + const y = event.clientY - rect.top; // y position within the element. + + if (this.props.MainContainer === null) { + throw new Error('[handleOnDrop] Tried to drop into the tree without a required MainContainer!'); + } + + if (target.id === this.props.MainContainer.properties.id) { + target.classList.add('border-8'); + target.classList.remove('border-t-8'); + target.classList.remove('border-b-8'); + return; + } + + if (y < 12) { + target.classList.add('border-t-8'); + target.classList.remove('border-b-8'); + target.classList.remove('border-8'); + } else if (y < 24) { + target.classList.add('border-8'); + target.classList.remove('border-t-8'); + target.classList.remove('border-b-8'); + } else { + target.classList.add('border-b-8'); + target.classList.remove('border-8'); + target.classList.remove('border-t-8'); + } + } + + public handleOnDrop(event: React.DragEvent): void { + event.preventDefault(); + const type = event.dataTransfer.getData('type'); + const target: HTMLButtonElement = event.target as HTMLButtonElement; + removeBorderClasses(target); + + 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; @@ -111,7 +203,7 @@ export class ElementsSidebar extends React.PureComponent 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' + ? 'border-l-4 bg-slate-400/60 hover:bg-slate-400' : 'bg-slate-300/60 hover:bg-slate-300'; containerRows.push( duration: 0.150 }} className={ - `w-full elements-sidebar-row whitespace-pre + `w-full border-blue-500 elements-sidebar-row whitespace-pre text-left text-sm font-medium transition-all ${selectedClass}` } id={key} key={key} - onClick={() => this.props.selectContainer(container)}> + onDrop={(event) => this.handleOnDrop(event)} + onDragOver={(event) => this.handleDragOver(event)} + onDragLeave={(event) => handleDragLeave(event)} + onClick={() => this.props.SelectContainer(container)} + > { text } ); @@ -148,10 +244,21 @@ export class ElementsSidebar extends React.PureComponent y={this.state.contextMenuPosition.y} isOpen={this.state.isContextMenuOpen} > - this.props.deleteContainer(this.state.onClickContainerId)} /> + this.props.DeleteContainer(this.state.onClickContainerId)} /> ); } } + +function removeBorderClasses(target: HTMLButtonElement): void { + target.classList.remove('border-t-8'); + target.classList.remove('border-8'); + target.classList.remove('border-b-8'); +} + +function handleDragLeave(event: React.DragEvent): void { + const target: HTMLButtonElement = event.target as HTMLButtonElement; + removeBorderClasses(target); +} diff --git a/src/Components/Sidebar/Sidebar.tsx b/src/Components/Sidebar/Sidebar.tsx index c50730e..de06e61 100644 --- a/src/Components/Sidebar/Sidebar.tsx +++ b/src/Components/Sidebar/Sidebar.tsx @@ -8,14 +8,21 @@ interface ISidebarProps { buttonOnClick: (type: string) => void } +function handleDragStart(event: React.DragEvent): void { + event.dataTransfer.setData('type', (event.target as HTMLButtonElement).id); +} + export default class Sidebar extends React.PureComponent { public render(): JSX.Element { const listElements = this.props.componentOptions.map(componentOption => diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index 46ae454..e0d9b6c 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -17,7 +17,8 @@ interface IUIProps { SelectContainer: (container: ContainerModel) => void DeleteContainer: (containerId: string) => void OnPropertyChange: (key: string, value: string) => void - AddContainer: (type: string) => void + AddContainerToSelectedContainer: (type: string) => void + AddContainer: (index: number, type: string, parentId: string) => void SaveEditorAsJSON: () => void SaveEditorAsSVG: () => void LoadState: (move: number) => void @@ -89,7 +90,7 @@ export class UI extends React.PureComponent { this.props.AddContainer(type)} + buttonOnClick={(type: string) => this.props.AddContainerToSelectedContainer(type)} /> { isOpen={this.state.isElementsSidebarOpen} isHistoryOpen={this.state.isHistoryOpen} onPropertyChange={this.props.OnPropertyChange} - selectContainer={this.props.SelectContainer} - deleteContainer={this.props.DeleteContainer} + SelectContainer={this.props.SelectContainer} + DeleteContainer={this.props.DeleteContainer} + AddContainer={this.props.AddContainer} /> { * @param type The type of container * @returns void */ - public AddContainer(type: string): void { + public AddContainerToSelectedContainer(type: string): void { const history = this.getCurrentHistory(); const current = history[history.length - 1]; @@ -204,13 +204,22 @@ class Editor extends React.Component { return; } + const parent = current.SelectedContainer; + this.AddContainer(parent.children.length, type, parent.properties.id); + } + + public AddContainer(index: number, type: string, parentId: string): void { + const history = this.getCurrentHistory(); + const current = history[history.length - 1]; + if (current.MainContainer === null || current.MainContainer === undefined) { return; } // Get the preset properties from the API - const properties = this.props.configuration.AvailableContainers.find(option => option.Type === type); + const properties = this.props.configuration.AvailableContainers + .find(option => option.Type === type); if (properties === undefined) { throw new Error(`[AddContainer] Object type not found. Found: ${type}`); @@ -230,35 +239,32 @@ class Editor extends React.Component { const clone: IContainerModel = structuredClone(current.MainContainer); // Find the parent - const it = MakeIterator(clone); - let parent: ContainerModel | null = null; - for (const child of it) { - if (child.properties.id === current.SelectedContainer.properties.id) { - parent = child as ContainerModel; - break; - } - } + const parentClone: IContainerModel | undefined = findContainerById( + clone, parentId + ); - if (parent === null) { - throw new Error('[OnPropertyChange] Container model was not found among children of the main container!'); + if (parentClone === null || parentClone === undefined) { + throw new Error('[AddContainer] Container model was not found among children of the main container!'); } let x = 0; - const lastChild: IContainerModel | undefined = parent.children.at(-1); - if (lastChild !== undefined) { - x = lastChild.properties.x + Number(lastChild.properties.width); + if (index !== 0) { + const lastChild: IContainerModel | undefined = parentClone.children.at(index - 1); + if (lastChild !== undefined) { + x = lastChild.properties.x + Number(lastChild.properties.width); + } } // Create the container const newContainer = new ContainerModel( - parent, + parentClone, { id: `${type}-${count}`, - parentId: parent.properties.id, + parentId: parentClone.properties.id, x, y: 0, width: properties?.Width, - height: parent.properties.height, + height: parentClone.properties.height, ...properties.Style }, [], @@ -268,15 +274,19 @@ class Editor extends React.Component { ); // And push it the the parent children - parent.children.push(newContainer); + if (index === parentClone.children.length) { + parentClone.children.push(newContainer); + } else { + parentClone.children.splice(index, 0, newContainer); + } // Update the state this.setState({ history: history.concat([{ MainContainer: clone, TypeCounters: newCounters, - SelectedContainer: parent, - SelectedContainerId: parent.properties.id + SelectedContainer: parentClone, + SelectedContainerId: parentClone.properties.id }]), historyCurrentStep: history.length }); @@ -331,7 +341,8 @@ class Editor extends React.Component { SelectContainer={(container) => this.SelectContainer(container)} DeleteContainer={(containerId: string) => this.DeleteContainer(containerId)} OnPropertyChange={(key, value) => this.OnPropertyChange(key, value)} - AddContainer={(type) => this.AddContainer(type)} + AddContainerToSelectedContainer={(type) => this.AddContainerToSelectedContainer(type)} + AddContainer={(index, type, parentId) => this.AddContainer(index, type, parentId)} SaveEditorAsJSON={() => this.SaveEditorAsJSON()} SaveEditorAsSVG={() => this.SaveEditorAsSVG()} LoadState={(move) => this.LoadState(move)}