wip: drag input
parent
80b9fd18b9
commit
6e577d1308
@ -0,0 +1,235 @@
|
||||
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;
|
Loading…
Reference in New Issue