Merged PR 16: Transform every single class components into functional component
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing

This improve greatly the performance and the code cleaning.
It allows us to separate the inseparable class methods into modules functions
This commit is contained in:
Eric Nguyen 2022-08-09 15:15:56 +00:00
parent 1fc11adbaa
commit d9e06537e8
33 changed files with 1298 additions and 1261 deletions

View file

@ -5,22 +5,6 @@ import { ElementsSidebar } from './ElementsSidebar';
import { IContainerModel } from '../../Interfaces/ContainerModel';
describe.concurrent('Elements sidebar', () => {
it('No elements', () => {
render(<ElementsSidebar
MainContainer={null}
isOpen={true}
isHistoryOpen={false}
SelectedContainer={null}
onPropertyChange={() => {}}
SelectContainer={() => {}}
DeleteContainer={() => {}}
/>);
expect(screen.getByText(/Elements/i));
expect(screen.queryByText('id')).toBeNull();
expect(screen.queryByText(/main/i)).toBeNull();
});
it('With a MainContainer', () => {
render(<ElementsSidebar
MainContainer={{
@ -42,6 +26,7 @@ describe.concurrent('Elements sidebar', () => {
onPropertyChange={() => {}}
SelectContainer={() => {}}
DeleteContainer={() => {}}
AddContainer={() => {}}
/>);
expect(screen.getByText(/Elements/i));
@ -72,6 +57,7 @@ describe.concurrent('Elements sidebar', () => {
onPropertyChange={() => {}}
SelectContainer={() => {}}
DeleteContainer={() => {}}
AddContainer={() => {}}
/>);
expect(screen.getByText(/Elements/i));
@ -157,6 +143,7 @@ describe.concurrent('Elements sidebar', () => {
onPropertyChange={() => {}}
SelectContainer={() => {}}
DeleteContainer={() => {}}
AddContainer={() => {}}
/>);
expect(screen.getByText(/Elements/i));
@ -210,6 +197,7 @@ describe.concurrent('Elements sidebar', () => {
onPropertyChange={() => {}}
SelectContainer={selectContainer}
DeleteContainer={() => {}}
AddContainer={() => {}}
/>);
expect(screen.getByText(/Elements/i));
@ -232,6 +220,7 @@ describe.concurrent('Elements sidebar', () => {
onPropertyChange={() => {}}
SelectContainer={selectContainer}
DeleteContainer={() => {}}
AddContainer={() => {}}
/>);
expect((propertyId as HTMLInputElement).value === 'main').toBeFalsy();

View file

@ -2,12 +2,14 @@ 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 { getDepth, MakeIterator } from '../../utils/itertools';
import { Menu } from '../Menu/Menu';
import { MenuItem } from '../Menu/MenuItem';
import { handleDragLeave, handleDragOver, handleLeftClick, handleOnDrop, handleRightClick } from './MouseEventHandlers';
import { Point } from '../../Interfaces/Point';
interface IElementsSidebarProps {
MainContainer: IContainerModel | null
MainContainer: IContainerModel
isOpen: boolean
isHistoryOpen: boolean
SelectedContainer: IContainerModel | null
@ -17,248 +19,131 @@ interface IElementsSidebarProps {
AddContainer: (index: number, type: string, parent: string) => void
}
interface Point {
x: number
y: number
}
function createRows(
container: IContainerModel,
props: IElementsSidebarProps,
containerRows: React.ReactNode[]
): void {
const depth: number = getDepth(container);
const key = container.properties.id.toString();
const text = '|\t'.repeat(depth) + key;
const selectedClass: string = props.SelectedContainer !== undefined &&
props.SelectedContainer !== null &&
props.SelectedContainer.properties.id === container.properties.id
? 'border-l-4 bg-slate-400/60 hover:bg-slate-400'
: 'bg-slate-300/60 hover:bg-slate-300';
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();
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;
}
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 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 border-blue-500 elements-sidebar-row whitespace-pre
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 border-blue-500 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)}
onDragLeave={(event) => handleDragLeave(event)}
onClick={() => this.props.SelectContainer(container)}
>
{ text }
</motion.button>
);
});
}
id={key}
key={key}
onDrop={(event) => handleOnDrop(event, props.MainContainer, props.AddContainer)}
onDragOver={(event) => handleDragOver(event, props.MainContainer)}
onDragLeave={(event) => handleDragLeave(event)}
onClick={() => 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>
export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElementsSidebarProps): JSX.Element => {
// States
const [isContextMenuOpen, setIsContextMenuOpen] = React.useState<boolean>(false);
const [onClickContainerId, setOnClickContainerId] = React.useState<string>('');
const [contextMenuPosition, setContextMenuPosition] = React.useState<Point>({
x: 0,
y: 0
});
const elementRef = React.useRef<HTMLDivElement>(null);
// Event listeners
React.useEffect(() => {
elementRef.current?.addEventListener(
'contextmenu',
(event) => handleRightClick(
event,
setIsContextMenuOpen,
setOnClickContainerId,
setContextMenuPosition
));
window.addEventListener(
'click',
(event) => handleLeftClick(
isContextMenuOpen,
setIsContextMenuOpen,
setOnClickContainerId
));
return () => {
elementRef.current?.addEventListener(
'contextmenu',
(event) => handleRightClick(
event,
setIsContextMenuOpen,
setOnClickContainerId,
setContextMenuPosition
));
window.removeEventListener(
'click',
(event) => handleLeftClick(
isContextMenuOpen,
setIsContextMenuOpen,
setOnClickContainerId
));
};
}, []);
// Render
let isOpenClasses = '-right-64';
if (props.isOpen) {
isOpenClasses = props.isHistoryOpen
? 'right-64'
: 'right-0';
}
const containerRows: React.ReactNode[] = [];
const it = MakeIterator(props.MainContainer);
for (const container of it) {
createRows(
container,
props,
containerRows
);
}
}
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);
}
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={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={contextMenuPosition.x}
y={contextMenuPosition.y}
isOpen={isContextMenuOpen}
>
<MenuItem className='contextmenu-item' text='Delete' onClick={() => props.DeleteContainer(onClickContainerId)} />
</Menu>
<Properties properties={props.SelectedContainer?.properties} onChange={props.onPropertyChange}></Properties>
</div>
);
};

View file

@ -0,0 +1,137 @@
import { IContainerModel } from '../../Interfaces/ContainerModel';
import { Point } from '../../Interfaces/Point';
import { findContainerById } from '../../utils/itertools';
export function handleRightClick(
event: MouseEvent,
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
setOnClickContainerId: React.Dispatch<React.SetStateAction<string>>,
setContextMenuPosition: React.Dispatch<React.SetStateAction<Point>>
): void {
event.preventDefault();
if (!(event.target instanceof HTMLButtonElement)) {
setIsContextMenuOpen(false);
setOnClickContainerId('');
return;
}
const contextMenuPosition: Point = { x: event.pageX, y: event.pageY };
setIsContextMenuOpen(true);
setOnClickContainerId(event.target.id);
setContextMenuPosition(contextMenuPosition);
}
export function handleLeftClick(
isContextMenuOpen: boolean,
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
setOnClickContainerId: React.Dispatch<React.SetStateAction<string>>
): void {
if (!isContextMenuOpen) {
return;
}
setIsContextMenuOpen(false);
setOnClickContainerId('');
}
export function removeBorderClasses(target: HTMLButtonElement): void {
target.classList.remove('border-t-8');
target.classList.remove('border-8');
target.classList.remove('border-b-8');
}
export function handleDragLeave(event: React.DragEvent): void {
const target: HTMLButtonElement = event.target as HTMLButtonElement;
removeBorderClasses(target);
}
export function handleDragOver(
event: React.DragEvent,
mainContainer: IContainerModel
): 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 (target.id === 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');
}
}
export function handleOnDrop(
event: React.DragEvent,
mainContainer: IContainerModel,
addContainer: (index: number, type: string, parent: string) => void
): void {
event.preventDefault();
const type = event.dataTransfer.getData('type');
const target: HTMLButtonElement = event.target as HTMLButtonElement;
removeBorderClasses(target);
const targetContainer: IContainerModel | undefined = findContainerById(
mainContainer,
target.id
);
if (targetContainer === undefined) {
throw new Error('[handleOnDrop] Tried to drop onto a unknown container!');
}
if (targetContainer === mainContainer) {
// if the container is the root, only add type as child
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);
addContainer(
index,
type,
targetContainer.parent.properties.id
);
} else if (y < 24) {
addContainer(
targetContainer.children.length,
type,
targetContainer.properties.id);
} else {
const index = targetContainer.parent.children.indexOf(targetContainer);
addContainer(
index + 1,
type,
targetContainer.parent.properties.id
);
}
}