import { useEffect, useMemo, useRef, useState } from "react"; import throttle from "lodash.throttle"; import { EVENT } from "../../constants"; import { KEYS } from "../../keys"; import type { ElementsMap, ExcalidrawElement } from "../../element/types"; import { deepCopyElement } from "../../element/newElement"; import "./DragInput.scss"; import clsx from "clsx"; import { useApp } from "../App"; export type DragInputCallbackType = ({ accumulatedChange, instantChange, stateAtStart, originalElementsMap, shouldKeepAspectRatio, shouldChangeByStepSize, nextValue, }: { accumulatedChange: number; instantChange: number; stateAtStart: ExcalidrawElement[]; originalElementsMap: ElementsMap; shouldKeepAspectRatio: boolean; shouldChangeByStepSize: boolean; nextValue?: number; }) => void; interface StatsDragInputProps { label: string | React.ReactNode; value: number; elements: ExcalidrawElement[]; editable?: boolean; shouldKeepAspectRatio?: boolean; dragInputCallback: DragInputCallbackType; } const StatsDragInput = ({ label, dragInputCallback, value, elements, editable = true, shouldKeepAspectRatio, }: StatsDragInputProps) => { const app = useApp(); const inputRef = useRef(null); const labelRef = useRef(null); const cbThrottled = useMemo(() => { return throttle(dragInputCallback, 16); }, [dragInputCallback]); const [inputValue, setInputValue] = useState(value.toString()); useEffect(() => { setInputValue(value.toString()); }, [value]); return (
{ if (inputRef.current && editable) { let startValue = Number(inputRef.current.value); if (isNaN(startValue)) { startValue = 0; } let lastPointer: { x: number; y: number; } | null = null; let stateAtStart: ExcalidrawElement[] | null = null; let originalElementsMap: Map | null = null; let accumulatedChange: number | null = null; document.body.classList.add("dragResize"); const onPointerMove = (event: PointerEvent) => { if (!stateAtStart) { stateAtStart = elements.map((element) => deepCopyElement(element), ); } if (!originalElementsMap) { originalElementsMap = app.scene .getNonDeletedElements() .reduce((acc, element) => { acc.set(element.id, deepCopyElement(element)); return acc; }, new Map() as ElementsMap); } if (!accumulatedChange) { accumulatedChange = 0; } if (lastPointer && stateAtStart && accumulatedChange !== null) { const instantChange = event.clientX - lastPointer.x; accumulatedChange += instantChange; cbThrottled({ accumulatedChange, instantChange, stateAtStart, originalElementsMap, shouldKeepAspectRatio: shouldKeepAspectRatio!!, shouldChangeByStepSize: event.shiftKey, }); } lastPointer = { x: event.clientX, y: event.clientY, }; }; window.addEventListener(EVENT.POINTER_MOVE, onPointerMove, false); window.addEventListener( EVENT.POINTER_UP, () => { window.removeEventListener( EVENT.POINTER_MOVE, onPointerMove, false, ); app.store.shouldCaptureIncrement(); lastPointer = null; accumulatedChange = null; stateAtStart = null; originalElementsMap = null; document.body.classList.remove("dragResize"); }, false, ); } }} onPointerEnter={() => { if (labelRef.current) { labelRef.current.style.cursor = "ew-resize"; } }} > {label}
{ if (editable) { const eventTarget = event.target; if ( eventTarget instanceof HTMLInputElement && event.key === KEYS.ENTER ) { const v = Number(eventTarget.value); if (isNaN(v)) { setInputValue(value.toString()); return; } dragInputCallback({ accumulatedChange: 0, instantChange: 0, stateAtStart: elements, originalElementsMap: app.scene.getNonDeletedElementsMap(), shouldKeepAspectRatio: shouldKeepAspectRatio!!, shouldChangeByStepSize: false, nextValue: v, }); app.store.shouldCaptureIncrement(); eventTarget.blur(); } } }} ref={inputRef} value={inputValue} onChange={(event) => { const eventTarget = event.target; if (eventTarget instanceof HTMLInputElement) { setInputValue(event.target.value); } }} onBlur={() => { if (!inputValue) { setInputValue(value.toString()); } }} disabled={!editable} >
); }; export default StatsDragInput;