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