Files
excalidraw/src/components/DragInput.tsx
T
2023-04-18 16:26:01 +08:00

236 lines
6.8 KiB
TypeScript

import throttle from "lodash.throttle";
import { useEffect, useMemo, useRef } from "react";
import { EVENT } from "../constants";
import { getTransformHandles } from "../element";
import { mutateElement } from "../element/mutateElement";
import { resizeSingleElement } from "../element/resizeElements";
import { ExcalidrawElement } from "../element/types";
import { KEYS } from "../keys";
import { degreeToRadian, radianToDegree, rotatePoint } from "../math";
import Scene from "../scene/Scene";
import { AppState, Point } from "../types";
import { arrayToMap } from "../utils";
const shouldKeepAspectRatio = (element: ExcalidrawElement) => {
return element.type === "image";
};
type AdjustableProperty = "width" | "height" | "angle" | "x" | "y";
interface DragInputProps {
label: string | React.ReactNode;
property: AdjustableProperty;
element: ExcalidrawElement;
zoom: AppState["zoom"];
}
const DragInput = ({ label, property, element, zoom }: DragInputProps) => {
const inputRef = useRef<HTMLInputElement>(null);
const labelRef = useRef<HTMLDivElement>(null);
const originalElementsMap = useMemo(
() => arrayToMap(Scene.getScene(element)?.getNonDeletedElements() ?? []),
[element],
);
const handleChange = useMemo(
() =>
(
initialValue: number,
delta: number,
source: "pointerMove" | "keyDown",
pointerOffset?: number,
) => {
if (inputRef.current) {
const keepAspectRatio = shouldKeepAspectRatio(element);
if (
(property === "width" || property === "height") &&
source === "pointerMove" &&
pointerOffset
) {
const handles = getTransformHandles(element, zoom, "mouse");
let referencePoint: Point | undefined;
let handleDirection: "e" | "s" | "se" | undefined;
if (keepAspectRatio && handles.se) {
referencePoint = [handles.se[0], handles.se[1]];
handleDirection = "se";
} else if (property === "width" && handles.e) {
referencePoint = [handles.e[0], handles.e[1]];
handleDirection = "e";
} else if (property === "height" && handles.s) {
referencePoint = [handles.s[0], handles.s[1]];
handleDirection = "s";
}
if (referencePoint !== undefined && handleDirection !== undefined) {
const pointerRotated = rotatePoint(
[
referencePoint[0] +
(property === "width" ? pointerOffset : 0),
referencePoint[1] +
(property === "height" ? pointerOffset : 0),
],
referencePoint,
element.angle,
);
resizeSingleElement(
originalElementsMap,
keepAspectRatio,
element,
handleDirection,
false,
pointerRotated[0],
pointerRotated[1],
);
}
} else if (
source === "keyDown" ||
(source === "pointerMove" &&
property !== "width" &&
property !== "height")
) {
const incVal = Math.round(
Math.sign(delta) * Math.pow(Math.abs(delta) / 10, 1.6),
);
let newVal = initialValue + incVal;
newVal =
property === "angle"
? // so the degree converted from radian is an integer
degreeToRadian(
Math.round(
radianToDegree(
degreeToRadian(
Math.sign(newVal % 360) === -1
? (newVal % 360) + 360
: newVal % 360,
),
),
),
)
: Math.round(newVal);
mutateElement(element, {
[property]: newVal,
});
}
}
},
[element, property, zoom, originalElementsMap],
);
const hangleChangeThrottled = useMemo(() => {
return throttle(handleChange, 16);
}, [handleChange]);
useEffect(() => {
const value =
Math.round(
property === "angle"
? radianToDegree(element[property]) * 100
: element[property] * 100,
) / 100;
if (inputRef.current) {
inputRef.current.value = String(value);
}
}, [element, element.version, element.versionNonce, property]);
useEffect(() => {
hangleChangeThrottled.cancel();
});
return (
<label className="color-input-container">
<div
className="color-picker-hash"
ref={labelRef}
style={{
width: "20px",
}}
onPointerDown={(event) => {
if (inputRef.current) {
const startPosition = event.clientX;
let startValue = Number(inputRef.current.value);
if (isNaN(startValue)) {
startValue = 0;
}
let lastPointerRef: {
x: number;
y: number;
} | null = null;
document.body.classList.add("dragResize");
const onPointerMove = (event: PointerEvent) => {
if (lastPointerRef) {
hangleChangeThrottled(
startValue,
Math.ceil(event.clientX - startPosition),
"pointerMove",
event.clientX - lastPointerRef.x,
);
}
lastPointerRef = {
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,
);
lastPointerRef = null;
document.body.classList.remove("dragResize");
},
false,
);
}
}}
onPointerEnter={() => {
if (labelRef.current) {
labelRef.current.style.cursor = "ew-resize";
}
}}
>
{label}
</div>
<input
className="color-picker-input"
style={{
width: "66px",
fontSize: "12px",
}}
autoComplete="off"
spellCheck="false"
onKeyDown={(event) => {
const value = Number(event.target.value);
if (isNaN(value)) {
return;
}
if (event.key === KEYS.ENTER) {
handleChange(value, 0, "keyDown");
}
}}
ref={inputRef}
></input>
</label>
);
};
export default DragInput;