Merged PR 171: Refactor the multiple context menus into a single component + Fix eslint
Refactor the multiple context menus into a single component + Fix eslint
This commit is contained in:
parent
ad126c6c28
commit
87c4ea1fe5
8 changed files with 173 additions and 269 deletions
|
@ -4,7 +4,6 @@ module.exports = {
|
|||
es2021: true
|
||||
},
|
||||
extends: [
|
||||
'only-warn',
|
||||
'plugin:react/recommended',
|
||||
'standard-with-typescript'
|
||||
],
|
||||
|
@ -18,6 +17,7 @@ module.exports = {
|
|||
project: './tsconfig.json'
|
||||
},
|
||||
plugins: [
|
||||
'only-warn',
|
||||
'react',
|
||||
'react-hooks',
|
||||
'@typescript-eslint'
|
||||
|
|
|
@ -12,6 +12,7 @@ import { IEditorState } from '../../Interfaces/IEditorState';
|
|||
import { MAX_HISTORY } from '../../utils/default';
|
||||
import { AddSymbol, OnPropertyChange as OnSymbolPropertyChange, DeleteSymbol, SelectSymbol } from './Actions/SymbolOperations';
|
||||
import { FindContainerById } from '../../utils/itertools';
|
||||
import { Menu } from '../Menu/Menu';
|
||||
|
||||
interface IEditorProps {
|
||||
root: Element | Document
|
||||
|
@ -20,24 +21,45 @@ interface IEditorProps {
|
|||
historyCurrentStep: number
|
||||
}
|
||||
|
||||
export function UpdateCounters(counters: Record<string, number>, type: string): void {
|
||||
if (counters[type] === null ||
|
||||
counters[type] === undefined) {
|
||||
counters[type] = 0;
|
||||
} else {
|
||||
counters[type]++;
|
||||
}
|
||||
}
|
||||
|
||||
export function GetCurrentHistory(history: IHistoryState[], historyCurrentStep: number): IHistoryState[] {
|
||||
return history.slice(
|
||||
Math.max(0, history.length - MAX_HISTORY),
|
||||
historyCurrentStep + 1
|
||||
function InitActions(
|
||||
menuActions: Map<any, any>,
|
||||
history: IHistoryState[],
|
||||
historyCurrentStep: number,
|
||||
setHistory: React.Dispatch<React.SetStateAction<IHistoryState[]>>,
|
||||
setHistoryCurrentStep: React.Dispatch<React.SetStateAction<number>>
|
||||
): void {
|
||||
menuActions.set(
|
||||
'elements-sidebar-row',
|
||||
[{
|
||||
text: 'Delete',
|
||||
action: (target: HTMLElement) => {
|
||||
const id = target.id;
|
||||
DeleteContainer(
|
||||
id,
|
||||
history,
|
||||
historyCurrentStep,
|
||||
setHistory,
|
||||
setHistoryCurrentStep
|
||||
);
|
||||
}
|
||||
|
||||
export function GetCurrentHistoryState(history: IHistoryState[], historyCurrentStep: number): IHistoryState {
|
||||
return history[historyCurrentStep];
|
||||
}]
|
||||
);
|
||||
menuActions.set(
|
||||
'symbols-sidebar-row',
|
||||
[{
|
||||
text: 'Delete',
|
||||
action: (target: HTMLElement) => {
|
||||
const id = target.id;
|
||||
DeleteSymbol(
|
||||
id,
|
||||
history,
|
||||
historyCurrentStep,
|
||||
setHistory,
|
||||
setHistoryCurrentStep
|
||||
);
|
||||
}
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
function UseShortcuts(
|
||||
|
@ -106,10 +128,12 @@ function UseWindowEvents(
|
|||
}
|
||||
|
||||
export function Editor(props: IEditorProps): JSX.Element {
|
||||
// States
|
||||
const [history, setHistory] = React.useState<IHistoryState[]>(structuredClone(props.history));
|
||||
const [historyCurrentStep, setHistoryCurrentStep] = React.useState<number>(props.historyCurrentStep);
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Events
|
||||
UseShortcuts(history, historyCurrentStep, setHistoryCurrentStep);
|
||||
UseWindowEvents(
|
||||
props.root,
|
||||
|
@ -121,6 +145,11 @@ export function Editor(props: IEditorProps): JSX.Element {
|
|||
setHistoryCurrentStep
|
||||
);
|
||||
|
||||
// Context Menu
|
||||
const menuActions = new Map();
|
||||
InitActions(menuActions, history, historyCurrentStep, setHistory, setHistoryCurrentStep);
|
||||
|
||||
// Render
|
||||
const configuration = props.configuration;
|
||||
const current = GetCurrentHistoryState(history, historyCurrentStep);
|
||||
const selected = FindContainerById(current.mainContainer, current.selectedContainerId);
|
||||
|
@ -208,6 +237,31 @@ export function Editor(props: IEditorProps): JSX.Element {
|
|||
>
|
||||
{current.mainContainer}
|
||||
</SVG>
|
||||
<Menu
|
||||
getListener={() => editorRef.current}
|
||||
actions={menuActions}
|
||||
className="z-30 transition-opacity rounded bg-slate-200 py-1 drop-shadow-xl"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function UpdateCounters(counters: Record<string, number>, type: string): void {
|
||||
if (counters[type] === null ||
|
||||
counters[type] === undefined) {
|
||||
counters[type] = 0;
|
||||
} else {
|
||||
counters[type]++;
|
||||
}
|
||||
}
|
||||
|
||||
export function GetCurrentHistory(history: IHistoryState[], historyCurrentStep: number): IHistoryState[] {
|
||||
return history.slice(
|
||||
Math.max(0, history.length - MAX_HISTORY),
|
||||
historyCurrentStep + 1
|
||||
);
|
||||
}
|
||||
|
||||
export function GetCurrentHistoryState(history: IHistoryState[], historyCurrentStep: number): IHistoryState {
|
||||
return history[historyCurrentStep];
|
||||
}
|
||||
|
|
|
@ -3,10 +3,6 @@ import { FixedSizeList as List } from 'react-window';
|
|||
import { Properties } from '../ContainerProperties/ContainerProperties';
|
||||
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
||||
import { GetDepth, MakeIterator } from '../../utils/itertools';
|
||||
import { Menu } from '../Menu/Menu';
|
||||
import { MenuItem } from '../Menu/MenuItem';
|
||||
import { UseMouseEvents } from './MouseEventHandlers';
|
||||
import { IPoint } from '../../Interfaces/IPoint';
|
||||
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||
import { PropertyType } from '../../Enums/PropertyType';
|
||||
|
||||
|
@ -22,29 +18,9 @@ interface IElementsSidebarProps {
|
|||
type?: PropertyType
|
||||
) => void
|
||||
selectContainer: (containerId: string) => void
|
||||
deleteContainer: (containerid: string) => void
|
||||
}
|
||||
|
||||
export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element {
|
||||
// States
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = React.useState<boolean>(false);
|
||||
const [onClickContainerId, setOnClickContainerId] = React.useState<string>('');
|
||||
const [contextMenuPosition, setContextMenuPosition] = React.useState<IPoint>({
|
||||
x: 0,
|
||||
y: 0
|
||||
});
|
||||
|
||||
const elementRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
// Event listeners
|
||||
UseMouseEvents(
|
||||
isContextMenuOpen,
|
||||
elementRef,
|
||||
setIsContextMenuOpen,
|
||||
setOnClickContainerId,
|
||||
setContextMenuPosition
|
||||
);
|
||||
|
||||
// Render
|
||||
let isOpenClasses = '-right-64';
|
||||
if (props.isOpen) {
|
||||
|
@ -90,7 +66,7 @@ export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element {
|
|||
className={`fixed flex flex-col bg-slate-100 text-gray-800 transition-all h-full w-64 overflow-y-auto z-20 ${isOpenClasses}`}
|
||||
>
|
||||
<div className="bg-slate-100 font-bold sidebar-title">Elements</div>
|
||||
<div ref={elementRef} className="h-96 text-gray-800">
|
||||
<div className="h-96 text-gray-800">
|
||||
<List
|
||||
className="List divide-y divide-black"
|
||||
itemCount={containers.length}
|
||||
|
@ -101,20 +77,6 @@ export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element {
|
|||
{Row}
|
||||
</List>
|
||||
</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={() => {
|
||||
setIsContextMenuOpen(false);
|
||||
props.deleteContainer(onClickContainerId);
|
||||
} } />
|
||||
</Menu>
|
||||
<Properties
|
||||
properties={props.selectedContainer?.properties}
|
||||
symbols={props.symbols}
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
import React, { RefObject, Dispatch, SetStateAction, useEffect } from 'react';
|
||||
import { IPoint } from '../../Interfaces/IPoint';
|
||||
|
||||
export function UseMouseEvents(
|
||||
isContextMenuOpen: boolean,
|
||||
elementRef: RefObject<HTMLDivElement>,
|
||||
setIsContextMenuOpen: Dispatch<SetStateAction<boolean>>,
|
||||
setOnClickContainerId: Dispatch<SetStateAction<string>>,
|
||||
setContextMenuPosition: Dispatch<SetStateAction<IPoint>>
|
||||
): void {
|
||||
useEffect(() => {
|
||||
function OnContextMenu(event: MouseEvent): void {
|
||||
return HandleRightClick(
|
||||
event,
|
||||
setIsContextMenuOpen,
|
||||
setOnClickContainerId,
|
||||
setContextMenuPosition
|
||||
);
|
||||
}
|
||||
|
||||
function OnLeftClick(): void {
|
||||
return HandleLeftClick(
|
||||
isContextMenuOpen,
|
||||
setIsContextMenuOpen,
|
||||
setOnClickContainerId
|
||||
);
|
||||
}
|
||||
|
||||
elementRef.current?.addEventListener(
|
||||
'contextmenu',
|
||||
OnContextMenu
|
||||
);
|
||||
|
||||
window.addEventListener(
|
||||
'click',
|
||||
OnLeftClick
|
||||
);
|
||||
|
||||
return () => {
|
||||
elementRef.current?.removeEventListener(
|
||||
'contextmenu',
|
||||
OnContextMenu
|
||||
);
|
||||
|
||||
window.removeEventListener(
|
||||
'click',
|
||||
OnLeftClick
|
||||
);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function HandleRightClick(
|
||||
event: MouseEvent,
|
||||
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
setOnClickContainerId: React.Dispatch<React.SetStateAction<string>>,
|
||||
setContextMenuPosition: React.Dispatch<React.SetStateAction<IPoint>>
|
||||
): void {
|
||||
event.preventDefault();
|
||||
|
||||
if (!(event.target instanceof HTMLButtonElement)) {
|
||||
setIsContextMenuOpen(false);
|
||||
setOnClickContainerId('');
|
||||
return;
|
||||
}
|
||||
|
||||
const contextMenuPosition: IPoint = { 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('');
|
||||
}
|
|
@ -1,23 +1,109 @@
|
|||
import * as React from 'react';
|
||||
import { IPoint } from '../../Interfaces/IPoint';
|
||||
import { MenuItem } from './MenuItem';
|
||||
|
||||
interface IMenuProps {
|
||||
getListener: () => HTMLElement | null
|
||||
actions: Map<string, IAction[]>
|
||||
className?: string
|
||||
x: number
|
||||
y: number
|
||||
isOpen: boolean
|
||||
children: React.ReactNode[] | React.ReactNode
|
||||
}
|
||||
|
||||
export interface IAction {
|
||||
/** displayed */
|
||||
text: string
|
||||
|
||||
/** function to be called on button click */
|
||||
action: (target: HTMLElement) => void
|
||||
}
|
||||
|
||||
function UseMouseEvents(
|
||||
getListener: () => HTMLElement | null,
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
setContextMenuPosition: React.Dispatch<React.SetStateAction<IPoint>>,
|
||||
setTarget: React.Dispatch<React.SetStateAction<HTMLElement | undefined>>
|
||||
): void {
|
||||
React.useEffect(() => {
|
||||
function OnContextMenu(event: MouseEvent): void {
|
||||
event.preventDefault();
|
||||
const contextMenuPosition: IPoint = { x: event.pageX, y: event.pageY };
|
||||
|
||||
setIsOpen(true);
|
||||
setContextMenuPosition(contextMenuPosition);
|
||||
setTarget(event.target as HTMLElement); // this is wrong since target can be null and not undefined
|
||||
}
|
||||
|
||||
function OnLeftClick(): void {
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
getListener()?.addEventListener(
|
||||
'contextmenu',
|
||||
OnContextMenu
|
||||
);
|
||||
|
||||
window.addEventListener(
|
||||
'click',
|
||||
OnLeftClick
|
||||
);
|
||||
|
||||
return () => {
|
||||
getListener()?.removeEventListener(
|
||||
'contextmenu',
|
||||
OnContextMenu
|
||||
);
|
||||
|
||||
window.removeEventListener(
|
||||
'click',
|
||||
OnLeftClick
|
||||
);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function Menu(props: IMenuProps): JSX.Element {
|
||||
const visible = props.isOpen ? 'visible opacity-1' : 'invisible opacity-0';
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [contextMenuPosition, setContextMenuPosition] = React.useState<IPoint>({
|
||||
x: 0,
|
||||
y: 0
|
||||
});
|
||||
const [target, setTarget] = React.useState<HTMLElement>();
|
||||
|
||||
UseMouseEvents(
|
||||
props.getListener,
|
||||
setIsOpen,
|
||||
setContextMenuPosition,
|
||||
setTarget
|
||||
);
|
||||
|
||||
let children;
|
||||
|
||||
if (target !== undefined) {
|
||||
for (const className of target.classList) {
|
||||
const actions = props.actions.get(className);
|
||||
if (actions === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
children = actions.map((action, index) =>
|
||||
<MenuItem
|
||||
key={`contextmenu-item-${index}`}
|
||||
className="contextmenu-item"
|
||||
text={action.text}
|
||||
onClick={() => action.action(target)} />
|
||||
);
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
const visible = isOpen ? 'visible opacity-1' : 'invisible opacity-0';
|
||||
return (
|
||||
<div
|
||||
className={`fixed ${props.className ?? ''} ${visible}`}
|
||||
style={{
|
||||
left: props.x,
|
||||
top: props.y
|
||||
left: contextMenuPosition.x,
|
||||
top: contextMenuPosition.y
|
||||
}}>
|
||||
{props.children}
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
import { RefObject, Dispatch, SetStateAction, useEffect } from 'react';
|
||||
import { IPoint } from '../../Interfaces/IPoint';
|
||||
|
||||
export function UseMouseEvents(
|
||||
isContextMenuOpen: boolean,
|
||||
elementRef: RefObject<HTMLDivElement>,
|
||||
setIsContextMenuOpen: Dispatch<SetStateAction<boolean>>,
|
||||
setOnClickSymbolId: Dispatch<SetStateAction<string>>,
|
||||
setContextMenuPosition: Dispatch<SetStateAction<IPoint>>
|
||||
): void {
|
||||
useEffect(() => {
|
||||
function OnContextMenu(event: MouseEvent): void {
|
||||
return HandleRightClick(
|
||||
event,
|
||||
setIsContextMenuOpen,
|
||||
setOnClickSymbolId,
|
||||
setContextMenuPosition
|
||||
);
|
||||
}
|
||||
|
||||
function OnLeftClick(): void {
|
||||
return HandleLeftClick(
|
||||
isContextMenuOpen,
|
||||
setIsContextMenuOpen,
|
||||
setOnClickSymbolId
|
||||
);
|
||||
}
|
||||
|
||||
elementRef.current?.addEventListener(
|
||||
'contextmenu',
|
||||
OnContextMenu
|
||||
);
|
||||
|
||||
window.addEventListener(
|
||||
'click',
|
||||
OnLeftClick
|
||||
);
|
||||
|
||||
return () => {
|
||||
elementRef.current?.removeEventListener(
|
||||
'contextmenu',
|
||||
OnContextMenu
|
||||
);
|
||||
|
||||
window.removeEventListener(
|
||||
'click',
|
||||
OnLeftClick
|
||||
);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function HandleRightClick(
|
||||
event: MouseEvent,
|
||||
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
setOnClickSymbolId: React.Dispatch<React.SetStateAction<string>>,
|
||||
setContextMenuPosition: React.Dispatch<React.SetStateAction<IPoint>>
|
||||
): void {
|
||||
event.preventDefault();
|
||||
|
||||
if (!(event.target instanceof HTMLButtonElement)) {
|
||||
setIsContextMenuOpen(false);
|
||||
setOnClickSymbolId('');
|
||||
return;
|
||||
}
|
||||
|
||||
const contextMenuPosition: IPoint = { x: event.pageX, y: event.pageY };
|
||||
setIsContextMenuOpen(true);
|
||||
setOnClickSymbolId(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('');
|
||||
}
|
|
@ -1,9 +1,5 @@
|
|||
import * as React from 'react';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
import { Menu } from '../Menu/Menu';
|
||||
import { MenuItem } from '../Menu/MenuItem';
|
||||
import { UseMouseEvents } from './MouseEventHandlers';
|
||||
import { IPoint } from '../../Interfaces/IPoint';
|
||||
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||
import { SymbolProperties } from '../SymbolProperties/SymbolProperties';
|
||||
|
||||
|
@ -18,25 +14,6 @@ interface ISymbolsSidebarProps {
|
|||
}
|
||||
|
||||
export function SymbolsSidebar(props: ISymbolsSidebarProps): JSX.Element {
|
||||
// States
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = React.useState<boolean>(false);
|
||||
const [onClickSymbolId, setOnClickSymbolId] = React.useState<string>('');
|
||||
const [contextMenuPosition, setContextMenuPosition] = React.useState<IPoint>({
|
||||
x: 0,
|
||||
y: 0
|
||||
});
|
||||
|
||||
const elementRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
// Event listeners
|
||||
UseMouseEvents(
|
||||
isContextMenuOpen,
|
||||
elementRef,
|
||||
setIsContextMenuOpen,
|
||||
setOnClickSymbolId,
|
||||
setContextMenuPosition
|
||||
);
|
||||
|
||||
// Render
|
||||
let isOpenClasses = '-right-64';
|
||||
if (props.isOpen) {
|
||||
|
@ -57,7 +34,7 @@ export function SymbolsSidebar(props: ISymbolsSidebarProps): JSX.Element {
|
|||
|
||||
return (
|
||||
<button type="button"
|
||||
className={`w-full border-blue-500 elements-sidebar-row whitespace-pre
|
||||
className={`w-full border-blue-500 symbols-sidebar-row whitespace-pre
|
||||
text-left text-sm font-medium transition-all ${selectedClass}`}
|
||||
id={key}
|
||||
key={key}
|
||||
|
@ -74,7 +51,7 @@ export function SymbolsSidebar(props: ISymbolsSidebarProps): JSX.Element {
|
|||
<div className='bg-slate-100 font-bold sidebar-title'>
|
||||
Elements
|
||||
</div>
|
||||
<div ref={elementRef} className='h-96 text-gray-800'>
|
||||
<div className='h-96 text-gray-800'>
|
||||
<List
|
||||
className='List divide-y divide-black'
|
||||
itemCount={containers.length}
|
||||
|
@ -85,17 +62,6 @@ export function SymbolsSidebar(props: ISymbolsSidebarProps): JSX.Element {
|
|||
{Row}
|
||||
</List>
|
||||
</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={() => {
|
||||
setIsContextMenuOpen(false);
|
||||
props.deleteSymbol(onClickSymbolId);
|
||||
} } />
|
||||
</Menu>
|
||||
<SymbolProperties
|
||||
symbol={props.symbols.get(props.selectedSymbolId)}
|
||||
symbols={props.symbols}
|
||||
|
|
|
@ -19,6 +19,10 @@
|
|||
@apply pl-6 pr-6 pt-2 pb-2 w-full
|
||||
}
|
||||
|
||||
.symbols-sidebar-row {
|
||||
@apply elements-sidebar-row
|
||||
}
|
||||
|
||||
.close-button {
|
||||
@apply transition-all w-full h-auto p-4 flex
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue