From 87c4ea1fe55faf5df906f52b8485c48fd1ebe7a3 Mon Sep 17 00:00:00 2001 From: Eric Nguyen Date: Mon, 29 Aug 2022 15:03:47 +0000 Subject: [PATCH] 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 --- .eslintrc.cjs | 2 +- src/Components/Editor/Editor.tsx | 88 ++++++++++++--- .../ElementsSidebar/ElementsSidebar.tsx | 40 +------ .../ElementsSidebar/MouseEventHandlers.ts | 84 --------------- src/Components/Menu/Menu.tsx | 102 ++++++++++++++++-- .../SymbolsSidebar/MouseEventHandlers.ts | 84 --------------- .../SymbolsSidebar/SymbolsSidebar.tsx | 38 +------ src/index.scss | 4 + 8 files changed, 173 insertions(+), 269 deletions(-) delete mode 100644 src/Components/ElementsSidebar/MouseEventHandlers.ts delete mode 100644 src/Components/SymbolsSidebar/MouseEventHandlers.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 153ac9f..c1c1dbe 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -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' diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx index 87c83fb..8851537 100644 --- a/src/Components/Editor/Editor.tsx +++ b/src/Components/Editor/Editor.tsx @@ -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, 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, + history: IHistoryState[], + historyCurrentStep: number, + setHistory: React.Dispatch>, + setHistoryCurrentStep: React.Dispatch> +): void { + menuActions.set( + 'elements-sidebar-row', + [{ + text: 'Delete', + action: (target: HTMLElement) => { + const id = target.id; + DeleteContainer( + id, + history, + historyCurrentStep, + setHistory, + setHistoryCurrentStep + ); + } + }] + ); + menuActions.set( + 'symbols-sidebar-row', + [{ + text: 'Delete', + action: (target: HTMLElement) => { + const id = target.id; + DeleteSymbol( + id, + history, + historyCurrentStep, + setHistory, + setHistoryCurrentStep + ); + } + }] ); -} - -export function GetCurrentHistoryState(history: IHistoryState[], historyCurrentStep: number): IHistoryState { - return history[historyCurrentStep]; } function UseShortcuts( @@ -106,10 +128,12 @@ function UseWindowEvents( } export function Editor(props: IEditorProps): JSX.Element { + // States const [history, setHistory] = React.useState(structuredClone(props.history)); const [historyCurrentStep, setHistoryCurrentStep] = React.useState(props.historyCurrentStep); const editorRef = useRef(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} + editorRef.current} + actions={menuActions} + className="z-30 transition-opacity rounded bg-slate-200 py-1 drop-shadow-xl" + /> ); } + +export function UpdateCounters(counters: Record, 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]; +} diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx index bf5b419..49df647 100644 --- a/src/Components/ElementsSidebar/ElementsSidebar.tsx +++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx @@ -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(false); - const [onClickContainerId, setOnClickContainerId] = React.useState(''); - const [contextMenuPosition, setContextMenuPosition] = React.useState({ - x: 0, - y: 0 - }); - - const elementRef = React.useRef(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}`} >
Elements
-
+
- - { - setIsContextMenuOpen(false); - props.deleteContainer(onClickContainerId); - } } /> - , - setIsContextMenuOpen: Dispatch>, - setOnClickContainerId: Dispatch>, - setContextMenuPosition: Dispatch> -): 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>, - setOnClickContainerId: React.Dispatch>, - setContextMenuPosition: React.Dispatch> -): 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>, - setOnClickContainerId: React.Dispatch> -): void { - if (!isContextMenuOpen) { - return; - } - - setIsContextMenuOpen(false); - setOnClickContainerId(''); -} diff --git a/src/Components/Menu/Menu.tsx b/src/Components/Menu/Menu.tsx index 53a5d0b..4556c41 100644 --- a/src/Components/Menu/Menu.tsx +++ b/src/Components/Menu/Menu.tsx @@ -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 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>, + setContextMenuPosition: React.Dispatch>, + setTarget: React.Dispatch> +): 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({ + x: 0, + y: 0 + }); + const [target, setTarget] = React.useState(); + + 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) => + action.action(target)} /> + ); + break; + }; + } + + const visible = isOpen ? 'visible opacity-1' : 'invisible opacity-0'; return (
- {props.children} + { children }
); } diff --git a/src/Components/SymbolsSidebar/MouseEventHandlers.ts b/src/Components/SymbolsSidebar/MouseEventHandlers.ts deleted file mode 100644 index 6021122..0000000 --- a/src/Components/SymbolsSidebar/MouseEventHandlers.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { RefObject, Dispatch, SetStateAction, useEffect } from 'react'; -import { IPoint } from '../../Interfaces/IPoint'; - -export function UseMouseEvents( - isContextMenuOpen: boolean, - elementRef: RefObject, - setIsContextMenuOpen: Dispatch>, - setOnClickSymbolId: Dispatch>, - setContextMenuPosition: Dispatch> -): 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>, - setOnClickSymbolId: React.Dispatch>, - setContextMenuPosition: React.Dispatch> -): 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>, - setOnClickContainerId: React.Dispatch> -): void { - if (!isContextMenuOpen) { - return; - } - - setIsContextMenuOpen(false); - setOnClickContainerId(''); -} diff --git a/src/Components/SymbolsSidebar/SymbolsSidebar.tsx b/src/Components/SymbolsSidebar/SymbolsSidebar.tsx index 01854d1..bf3663e 100644 --- a/src/Components/SymbolsSidebar/SymbolsSidebar.tsx +++ b/src/Components/SymbolsSidebar/SymbolsSidebar.tsx @@ -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(false); - const [onClickSymbolId, setOnClickSymbolId] = React.useState(''); - const [contextMenuPosition, setContextMenuPosition] = React.useState({ - x: 0, - y: 0 - }); - - const elementRef = React.useRef(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 (
-
+
- - { - setIsContextMenuOpen(false); - props.deleteSymbol(onClickSymbolId); - } } /> -