From 565fb9296a0e8dda58085cb7cf0ae5936ff9435a Mon Sep 17 00:00:00 2001 From: hamed-musallam <35760236+hamed-musallam@users.noreply.github.com> Date: Mon, 6 Nov 2023 10:28:58 +0100 Subject: [PATCH] refactor: resizer and draggable components (#2731) * refactor: useDraggable hook * refactor: resizer component and its hook * refactor: lefting resizer state up and remove key * refactor: create HOC for the resizer component * refactor: resizer component and remove div resizer --- .../1d/integral/IntegralResizable.tsx | 23 +-- .../1d/multiAnalysis/AnalysisRange.tsx | 21 +-- src/component/1d/ranges/Range.tsx | 29 ++-- src/component/elements/ResizerWithScale.tsx | 41 +++++ .../elements/draggable/SVGDraggable.tsx | 53 +++---- .../elements/draggable/useDraggable.ts | 140 ++++++++---------- src/component/elements/resizer/DivResizer.tsx | 95 ------------ src/component/elements/resizer/Resizer.tsx | 33 ----- src/component/elements/resizer/SVGResizer.tsx | 34 ++++- src/component/elements/resizer/useResizer.tsx | 74 ++++----- 10 files changed, 209 insertions(+), 334 deletions(-) create mode 100644 src/component/elements/ResizerWithScale.tsx delete mode 100644 src/component/elements/resizer/DivResizer.tsx delete mode 100644 src/component/elements/resizer/Resizer.tsx diff --git a/src/component/1d/integral/IntegralResizable.tsx b/src/component/1d/integral/IntegralResizable.tsx index 46bd8410a..98bd1c0a2 100644 --- a/src/component/1d/integral/IntegralResizable.tsx +++ b/src/component/1d/integral/IntegralResizable.tsx @@ -4,9 +4,8 @@ import { Integral } from 'nmr-processing'; import { useChartData } from '../../context/ChartContext'; import { useDispatch } from '../../context/DispatchContext'; -import { useGlobal } from '../../context/GlobalContext'; import { useScaleChecked } from '../../context/ScaleContext'; -import Resizer from '../../elements/resizer/Resizer'; +import { ResizerWithScale } from '../../elements/ResizerWithScale'; import { HighlightEventSource, useHighlight } from '../../highlight/index'; import { useResizerStatus } from '../../hooks/useResizerStatus'; @@ -52,10 +51,9 @@ function IntegralResizable({ integralFormat, }: IntegralResizableProps) { const { height, margin } = useChartData(); - const { viewerRef } = useGlobal(); const { scaleX } = useScaleChecked(); const dispatch = useDispatch(); - const { id, integral } = integralData; + const { id, integral, to, from } = integralData; const highlight = useHighlight([id], { type: HighlightEventSource.INTEGRAL, extra: { id }, @@ -74,25 +72,20 @@ function IntegralResizable({ }); } - const from = integralData.from ? scaleX()(integralData.from) : 0; - const to = integralData.to ? scaleX()(integralData.to) : 0; - const bottom = height - margin.bottom; - const isResizeingActive = useResizerStatus('integral'); + const isResizingActive = useResizerStatus('integral'); return ( highlight.show()} onMouseLeave={() => highlight.hide()} > - {({ x1, x2 }, isActive) => { const width = x2 - x1; @@ -119,7 +112,7 @@ function IntegralResizable({ ); }} - + ); } diff --git a/src/component/1d/multiAnalysis/AnalysisRange.tsx b/src/component/1d/multiAnalysis/AnalysisRange.tsx index 852271d5d..8aabd8ad6 100644 --- a/src/component/1d/multiAnalysis/AnalysisRange.tsx +++ b/src/component/1d/multiAnalysis/AnalysisRange.tsx @@ -2,10 +2,9 @@ import { css } from '@emotion/react'; import { useCallback } from 'react'; -import { useGlobal } from '../../context/GlobalContext'; import { usePreferences } from '../../context/PreferencesContext'; import { useScaleChecked } from '../../context/ScaleContext'; -import Resizer from '../../elements/resizer/Resizer'; +import { ResizerWithScale } from '../../elements/ResizerWithScale'; import { useHighlight } from '../../highlight'; import { useResizerStatus } from '../../hooks/useResizerStatus'; @@ -55,7 +54,6 @@ function AnalysisRange({ }); const { scaleX } = useScaleChecked(); const { dispatch } = usePreferences(); - const { viewerRef } = useGlobal(); const resizeEndHandler = useCallback( (resized) => { @@ -71,18 +69,15 @@ function AnalysisRange({ [activeTab, columnKey, dispatch, scaleX], ); - const from = scaleX()(rangeData.from); - const to = scaleX()(rangeData.to); - const isResizeingActive = useResizerStatus('multipleSpectraAnalysis'); + const { from, to } = rangeData; + const isResizingActive = useResizerStatus('multipleSpectraAnalysis'); return ( - {({ x1, x2 }, isActive) => ( )} - + ); } diff --git a/src/component/1d/ranges/Range.tsx b/src/component/1d/ranges/Range.tsx index 8ab9ea38a..40e44b59c 100644 --- a/src/component/1d/ranges/Range.tsx +++ b/src/component/1d/ranges/Range.tsx @@ -10,14 +10,13 @@ import { } from '../../assignment/AssignmentsContext'; import { filterForIDsWithAssignment } from '../../assignment/utilities/filterForIDsWithAssignment'; import { useDispatch } from '../../context/DispatchContext'; -import { useGlobal } from '../../context/GlobalContext'; -import { useScaleChecked } from '../../context/ScaleContext'; -import Resizer from '../../elements/resizer/Resizer'; +import { ResizerWithScale } from '../../elements/ResizerWithScale'; import { HighlightEventSource, useHighlight } from '../../highlight'; import { useResizerStatus } from '../../hooks/useResizerStatus'; import { options } from '../../toolbar/ToolTypes'; import { IntegralIndicator } from '../integral/IntegralIndicator'; import { MultiplicityTree } from '../multiplicityTree/MultiplicityTree'; +import { useScaleX } from '../utilities/scale'; import { AssignmentActionsButtons } from './AssignmentActionsButtons'; @@ -46,8 +45,7 @@ function Range({ selectedTool, relativeFormat, }: RangeProps) { - const { viewerRef } = useGlobal(); - const { id, integration, signals, diaIDs } = range; + const { id, integration, signals, diaIDs, from, to } = range; const assignmentData = useAssignmentData(); const assignmentRange = useAssignment(id); const highlightRange = useHighlight( @@ -60,7 +58,7 @@ function Range({ { type: HighlightEventSource.RANGE, extra: { id } }, ); - const { scaleX } = useScaleChecked(); + const scaleX = useScaleX(); const dispatch = useDispatch(); const isBlockedByEditing = @@ -105,15 +103,12 @@ function Range({ } } - const from = scaleX()(range.from); - const to = scaleX()(range.to); - const isNotSignal = !checkRangeKind(range); const isHighlighted = isBlockedByEditing || highlightRange.isActive || assignmentRange.isActive; const isAssigned = isRangeAssigned(range); - const isResizeingActive = useResizerStatus('rangePicking'); + const isResizingActive = useResizerStatus('rangePicking'); return ( - {({ x1, x2 }, isActive) => { const width = x2 - x1; @@ -161,14 +154,14 @@ function Range({ ); }} - + {showMultiplicityTrees && ( )} unAssignHandler()} /> diff --git a/src/component/elements/ResizerWithScale.tsx b/src/component/elements/ResizerWithScale.tsx new file mode 100644 index 000000000..e18308877 --- /dev/null +++ b/src/component/elements/ResizerWithScale.tsx @@ -0,0 +1,41 @@ +import { useEffect, useState } from 'react'; + +import { useScaleX } from '../1d/utilities/scale'; +import { useGlobal } from '../context/GlobalContext'; + +import SVGResizer, { ResizerProps } from './resizer/SVGResizer'; + +interface ResizerWithScaleProps { + disabled: boolean; + from: number; + to: number; + onEnd: ResizerProps['onEnd']; + children: ResizerProps['children']; +} + +export function ResizerWithScale(props: ResizerWithScaleProps) { + const { from, to, onEnd, disabled, children } = props; + const { viewerRef } = useGlobal(); + const scaleX = useScaleX(); + const x2 = scaleX()(from); + const x1 = scaleX()(to); + const [position, setPosition] = useState({ x1, x2 }); + + useEffect(() => { + const x2 = scaleX()(from); + const x1 = scaleX()(to); + setPosition({ x1, x2 }); + }, [from, scaleX, to]); + + return ( + setPosition(p)} + > + {children} + + ); +} diff --git a/src/component/elements/draggable/SVGDraggable.tsx b/src/component/elements/draggable/SVGDraggable.tsx index d60dce890..c9ca898b3 100644 --- a/src/component/elements/draggable/SVGDraggable.tsx +++ b/src/component/elements/draggable/SVGDraggable.tsx @@ -1,4 +1,4 @@ -import { useEffect, ReactFragment } from 'react'; +import { ReactFragment } from 'react'; import useDraggable, { Position } from './useDraggable'; @@ -13,7 +13,7 @@ type PositionChangeHandler = (data: Position) => void; export interface DraggableProps { children?: ChildType | ((x1: number, x2: number) => ChildType); - initialPosition?: Position; + position?: Position; width: number; height: number; onStart?: PositionChangeHandler; @@ -26,7 +26,7 @@ export interface DraggableProps { export default function SVGDraggable(props: DraggableProps) { const { children, - initialPosition = { x: 0, y: 0 }, + position: cPosition = { x: 0, y: 0 }, width, height, onStart, @@ -36,43 +36,32 @@ export default function SVGDraggable(props: DraggableProps) { dragHandleClassName, } = props; - const { - position: { - value: { x, y }, - action, + const { onPointerDown } = useDraggable({ + position: cPosition, + onChange: (dragEvent) => { + const { action, position } = dragEvent; + switch (action) { + case 'start': + onStart?.(position); + break; + case 'move': + onMove?.(position); + break; + case 'end': + onEnd?.(position); + break; + default: + break; + } }, - onPointerDown, - } = useDraggable({ - position: initialPosition, parentElement, dragHandleClassName, }); - useEffect(() => { - const position: Position = { - x, - y, - }; - - switch (action) { - case 'start': - onStart?.(position); - break; - case 'move': - onMove?.(position); - break; - case 'end': - onEnd?.(position); - break; - default: - break; - } - }, [action, onEnd, onMove, onStart, x, y]); - return ( diff --git a/src/component/elements/draggable/useDraggable.ts b/src/component/elements/draggable/useDraggable.ts index caff6c38b..311c513f9 100644 --- a/src/component/elements/draggable/useDraggable.ts +++ b/src/component/elements/draggable/useDraggable.ts @@ -1,22 +1,28 @@ -import { useCallback, useMemo, useRef, useState } from 'react'; +import { useRef } from 'react'; export interface Position { x: number; y: number; } +interface DragChangeCb { + position: Position; + action: Action; + isActive: boolean; +} + interface UseDraggable { position: Position; parentElement?: HTMLElement | null; fromEdge?: boolean; dragHandleClassName?: string; + onChange: (dragEvent: DragChangeCb) => void; } + export interface Draggable { onPointerDown: ( event: React.PointerEvent, ) => void; - position: { value: Position; action: Action }; - isActive: boolean; } type Action = 'start' | 'move' | 'end' | null; @@ -26,91 +32,75 @@ export default function useDraggable(props: UseDraggable): Draggable { parentElement, fromEdge = false, dragHandleClassName, + onChange, } = props; const isActive = useRef(false); const positionRef = useRef({ x, y }); - const [position, setPosition] = useState<{ value: Position; action: Action }>( - { - value: { x, y }, - action: null, - }, - ); - const onPointerDown = useCallback( - (e: React.PointerEvent) => { - e.stopPropagation(); - isActive.current = true; - const eventTarget = e.currentTarget as HTMLElement; + function onPointerDown(e: React.PointerEvent) { + e.stopPropagation(); + isActive.current = true; + const eventTarget = e.currentTarget as HTMLElement; + positionRef.current = { x, y }; - const classes = - (e.target as HTMLElement).getAttribute('class')?.split(' ') || []; - if ( - (dragHandleClassName && classes.includes(dragHandleClassName)) || - !dragHandleClassName - ) { - const _parentElement = parentElement || eventTarget?.parentElement; - if (_parentElement) { - const parentBounding = _parentElement.getBoundingClientRect(); - const currentBounding = eventTarget?.getBoundingClientRect(); - const startPosition: Position = { - x: - parentBounding.x + - (!fromEdge ? e.clientX - currentBounding.x : 0), - y: - parentBounding.y + - (!fromEdge ? e.clientY - currentBounding.y : 0), - }; + const classes = + (e.target as HTMLElement).getAttribute('class')?.split(' ') || []; + if ( + (dragHandleClassName && classes.includes(dragHandleClassName)) || + !dragHandleClassName + ) { + const _parentElement = parentElement || eventTarget?.parentElement; + if (_parentElement) { + const parentBounding = _parentElement.getBoundingClientRect(); + const currentBounding = eventTarget?.getBoundingClientRect(); + const startPosition: Position = { + x: parentBounding.x + (!fromEdge ? e.clientX - currentBounding.x : 0), + y: parentBounding.y + (!fromEdge ? e.clientY - currentBounding.y : 0), + }; - if (parentBounding) { - positionRef.current = startPosition; - } - setPosition((prevPortion) => ({ ...prevPortion, action: 'start' })); + if (parentBounding) { + positionRef.current = startPosition; } - - window.addEventListener('pointermove', moveCallback); - window.addEventListener('pointerup', upCallback); + onChange({ position: { x, y }, action: 'start', isActive: true }); } - function upCallback(e: PointerEvent) { - e.stopPropagation(); - if (isActive.current) { - setPosition({ - value: { - x: e.clientX - positionRef.current.x, - y: e.clientY - positionRef.current.y, - }, - action: 'end', - }); - isActive.current = false; - } + window.addEventListener('pointermove', moveCallback); + window.addEventListener('pointerup', upCallback); + } - window.removeEventListener('pointermove', moveCallback); - window.removeEventListener('pointerup', upCallback); + function upCallback(e: PointerEvent) { + e.stopPropagation(); + if (isActive.current) { + onChange({ + position: { + x: e.clientX - positionRef.current.x, + y: e.clientY - positionRef.current.y, + }, + action: 'end', + isActive: false, + }); + isActive.current = false; } - function moveCallback(e: PointerEvent) { - e.stopPropagation(); - if (isActive.current) { - setPosition({ - value: { - x: e.clientX - positionRef.current.x, - y: e.clientY - positionRef.current.y, - }, - action: 'move', - }); - } + window.removeEventListener('pointermove', moveCallback); + window.removeEventListener('pointerup', upCallback); + } + function moveCallback(e: PointerEvent) { + e.stopPropagation(); + + if (isActive.current) { + onChange({ + position: { + x: e.clientX - positionRef.current.x, + y: e.clientY - positionRef.current.y, + }, + action: 'move', + isActive: true, + }); } - }, - [dragHandleClassName, fromEdge, parentElement], - ); + } + } - return useMemo( - () => ({ - onPointerDown, - position, - isActive: isActive.current, - }), - [onPointerDown, position], - ); + return { onPointerDown }; } diff --git a/src/component/elements/resizer/DivResizer.tsx b/src/component/elements/resizer/DivResizer.tsx deleted file mode 100644 index 6429b1531..000000000 --- a/src/component/elements/resizer/DivResizer.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/** @jsxImportSource @emotion/react */ -import { css } from '@emotion/react'; -import { CSSProperties } from 'react'; - -import { Draggable } from '../draggable/useDraggable'; - -import { ResizerProps, Position } from './Resizer'; -import useResizer from './useResizer'; - -const anchorStyle: CSSProperties = { - marginLeft: '5px', - width: '2px', - height: '100%', - pointerEvents: 'none', - position: 'relative', -}; -const styles = { - container: (position: number) => css` - position: absolute; - height: 100%; - width: 10px; - left: -5px; - cursor: e-resize; - transform: translateX(${position}px); - user-select: none; - z-index: 99999999; - - &:hover { - div { - background-color: red; - } - } - `, - - content: (left: Draggable, right: Draggable, prevPosition: Position) => { - const width = prevPosition.x2 - prevPosition.x1; - - const baseCss = css` - position: absolute; - width: ${width}px; - overflow: hidden; - `; - if (right.position.action === 'move' || left.position.action === 'move') { - const scale = (right.position.value.x - left.position.value.x) / width; - return [ - baseCss, - css` - transform: translateX(${left.position.value.x}px) scaleX(${scale}); - transform-origin: left center; - `, - ]; - } else { - return css([ - baseCss, - css` - transform: translateX(${left.position.value.x}px); - `, - ]); - } - }, -}; - -export default function DivResizer(props: ResizerProps) { - const { children, disabled } = props; - const { left, right, prevPosition, currentPosition, isActive } = - useResizer(props); - - return ( - <> - {!disabled && ( -
-
-
- )} -
- {typeof children === 'function' - ? children?.(currentPosition, isActive) - : children} -
- {!disabled && ( -
-
-
- )} - - ); -} diff --git a/src/component/elements/resizer/Resizer.tsx b/src/component/elements/resizer/Resizer.tsx deleted file mode 100644 index 112367e78..000000000 --- a/src/component/elements/resizer/Resizer.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import DivResizer from './DivResizer'; -import SVGResizer from './SVGResizer'; - -export interface Position { - x1: number; - x2: number; -} - -type ChildType = React.ReactElement[] | React.ReactElement | boolean | null; - -export interface ResizerProps { - children?: ChildType | ((position: Position, isActive: boolean) => ChildType); - initialPosition?: Position; - position?: Position; - onStart?: PositionChangeHandler; - onMove?: PositionChangeHandler; - onEnd?: PositionChangeHandler; - tag?: 'div' | 'svg'; - parentElement?: HTMLElement | null; - dragHandleClassName?: string; - disabled?: boolean; -} - -type PositionChangeHandler = (data: Position) => void; - -export default function Resizer(props: ResizerProps) { - const { tag = 'div', ...resProps } = props; - if (tag === 'div') { - return ; - } else { - return ; - } -} diff --git a/src/component/elements/resizer/SVGResizer.tsx b/src/component/elements/resizer/SVGResizer.tsx index 07575f694..49e09600b 100644 --- a/src/component/elements/resizer/SVGResizer.tsx +++ b/src/component/elements/resizer/SVGResizer.tsx @@ -1,8 +1,8 @@ +/* eslint-disable react/no-unused-prop-types */ /** @jsxImportSource @emotion/react */ import { css } from '@emotion/react'; import { CSSProperties } from 'react'; -import { ResizerProps } from './Resizer'; import useResizer from './useResizer'; const style: Record<'anchor' | 'innerContainer', CSSProperties> = { @@ -33,22 +33,40 @@ const styles = { `, }; +export interface Position { + x1: number; + x2: number; +} + +type ChildType = React.ReactElement[] | React.ReactElement | boolean | null; + +export interface ResizerProps { + children?: ChildType | ((position: Position, isActive: boolean) => ChildType); + position: Position; + onStart?: PositionChangeHandler; + onMove?: PositionChangeHandler; + onEnd?: PositionChangeHandler; + parentElement?: HTMLElement | null; + dragHandleClassName?: string; + disabled?: boolean; +} + +type PositionChangeHandler = (data: Position) => void; + export default function SVGResizer(props: ResizerProps) { - const { children, disabled } = props; - const { left, right, currentPosition, isActive } = useResizer(props); + const { children, disabled, position } = props; + const { left, right, isActive } = useResizer(props); return ( - - {typeof children === 'function' - ? children(currentPosition, isActive) - : children} + + {typeof children === 'function' ? children(position, isActive) : children} {!disabled && ( <> {' '} )} diff --git a/src/component/elements/resizer/useResizer.tsx b/src/component/elements/resizer/useResizer.tsx index 430fa85a2..38b5d884a 100644 --- a/src/component/elements/resizer/useResizer.tsx +++ b/src/component/elements/resizer/useResizer.tsx @@ -1,31 +1,27 @@ -import { useEffect, useRef } from 'react'; +import { useRef } from 'react'; import useDraggable, { Draggable } from '../draggable/useDraggable'; -import { ResizerProps, Position } from './Resizer'; +import { ResizerProps, Position } from './SVGResizer'; interface UseResizer { right: Draggable; left: Draggable; - prevPosition: Position; - currentPosition: Position; isActive: boolean; } export default function useResizer(props: ResizerProps): UseResizer { const { - initialPosition = { x1: 10, x2: 40 }, + position = { x1: 0, x2: 0 }, onStart, onMove, onEnd, parentElement, } = props; - const currentPosition = useRef<{ x1: number; x2: number }>(initialPosition); - const prevPosition = useRef<{ x1: number; x2: number }>(initialPosition); const activeRef = useRef(false); - const triggerEvent = useRef((position: Position, status: string | null) => { + function triggerEvent(position: Position, status: string | null) { switch (status) { case 'start': onStart?.(position); @@ -35,7 +31,6 @@ export default function useResizer(props: ResizerProps): UseResizer { onMove?.(position); break; case 'end': - prevPosition.current = position; activeRef.current = false; onEnd?.(position); @@ -43,55 +38,44 @@ export default function useResizer(props: ResizerProps): UseResizer { default: break; } - }); + } const right = useDraggable({ - position: { x: initialPosition.x2, y: 0 }, + position: { x: position.x2, y: 0 }, parentElement, fromEdge: true, + onChange: (dragEvent) => { + const { + action, + position: { x }, + } = dragEvent; + const resizerBoundaries: Position = { + x1: position.x1, + x2: x, + }; + triggerEvent(resizerBoundaries, action); + }, }); const left = useDraggable({ - position: { x: initialPosition.x1, y: 0 }, + position: { x: position.x1, y: 0 }, parentElement, fromEdge: true, + onChange(dragEvent) { + const { + action, + position: { x }, + } = dragEvent; + const resizerBoundaries: Position = { + x1: x, + x2: position.x2, + }; + triggerEvent(resizerBoundaries, action); + }, }); - useEffect(() => { - currentPosition.current = { - x1: left.position.value.x, - x2: right.position.value.x, - }; - }, [left.position.value.x, right.position.value.x]); - - useEffect(() => { - const { - value: { x }, - action, - } = left.position; - const position: Position = { - x1: x, - x2: currentPosition.current.x2, - }; - triggerEvent.current(position, action); - }, [left.position]); - - useEffect(() => { - const { - value: { x }, - action, - } = right.position; - const position: Position = { - x1: currentPosition.current.x1, - x2: x, - }; - triggerEvent.current(position, action); - }, [right.position]); - return { left, right, - prevPosition: prevPosition.current, - currentPosition: currentPosition.current, isActive: activeRef.current, }; }