2024-06-04 23:06:27 +08:00

209 lines
5.9 KiB
TypeScript

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<HTMLInputElement>(null);
const labelRef = useRef<HTMLDivElement>(null);
const cbThrottled = useMemo(() => {
return throttle(dragInputCallback, 16);
}, [dragInputCallback]);
const [inputValue, setInputValue] = useState(value.toString());
useEffect(() => {
setInputValue(value.toString());
}, [value]);
return (
<div className={clsx("drag-input-container", !editable && "disabled")}>
<div
className="drag-input-label"
ref={labelRef}
onPointerDown={(event) => {
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<string, ExcalidrawElement> | 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}
</div>
<input
className="drag-input"
autoComplete="off"
spellCheck="false"
onKeyDown={(event) => {
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}
></input>
</div>
);
};
export default StatsDragInput;