refactor to include dimension and step size
parent
0a529bd2ed
commit
0987c5b770
@ -1,270 +0,0 @@
|
|||||||
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 type { ElementsMap, ExcalidrawElement } from "../element/types";
|
|
||||||
import { KEYS } from "../keys";
|
|
||||||
import { degreeToRadian, radianToDegree } from "../math";
|
|
||||||
import type { AppState, Point } from "../types";
|
|
||||||
import { deepCopyElement } from "../element/newElement";
|
|
||||||
|
|
||||||
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;
|
|
||||||
elementsMap: ElementsMap;
|
|
||||||
zoom: AppState["zoom"];
|
|
||||||
}
|
|
||||||
|
|
||||||
const DragInput = ({
|
|
||||||
label,
|
|
||||||
property,
|
|
||||||
element,
|
|
||||||
elementsMap,
|
|
||||||
zoom,
|
|
||||||
}: DragInputProps) => {
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const labelRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const originalElement = useRef<ExcalidrawElement>();
|
|
||||||
const accumulatedDimensionChange = useRef(0);
|
|
||||||
|
|
||||||
const handleChange = useMemo(
|
|
||||||
() =>
|
|
||||||
(
|
|
||||||
initialValue: number,
|
|
||||||
delta: number,
|
|
||||||
source: "pointerMove" | "keyDown",
|
|
||||||
pointerOffset?: number,
|
|
||||||
) => {
|
|
||||||
if (inputRef.current && originalElement.current) {
|
|
||||||
const keepAspectRatio = shouldKeepAspectRatio(element);
|
|
||||||
|
|
||||||
if (
|
|
||||||
(property === "width" || property === "height") &&
|
|
||||||
source === "pointerMove" &&
|
|
||||||
pointerOffset
|
|
||||||
) {
|
|
||||||
const handles = getTransformHandles(
|
|
||||||
originalElement.current,
|
|
||||||
zoom,
|
|
||||||
elementsMap,
|
|
||||||
"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) {
|
|
||||||
accumulatedDimensionChange.current += pointerOffset;
|
|
||||||
|
|
||||||
const pointer: Point = [
|
|
||||||
referencePoint[0] +
|
|
||||||
(property === "width"
|
|
||||||
? accumulatedDimensionChange.current
|
|
||||||
: 0),
|
|
||||||
referencePoint[1] +
|
|
||||||
(property === "height"
|
|
||||||
? accumulatedDimensionChange.current
|
|
||||||
: 0),
|
|
||||||
];
|
|
||||||
|
|
||||||
resizeSingleElement(
|
|
||||||
elementsMap,
|
|
||||||
keepAspectRatio,
|
|
||||||
element,
|
|
||||||
elementsMap,
|
|
||||||
handleDirection,
|
|
||||||
false,
|
|
||||||
pointer[0],
|
|
||||||
pointer[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,
|
|
||||||
});
|
|
||||||
originalElement.current = deepCopyElement(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[element, property, zoom, elementsMap],
|
|
||||||
);
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
accumulatedDimensionChange.current = 0;
|
|
||||||
originalElement.current = undefined;
|
|
||||||
}, [element.id]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<label className="color-input-container">
|
|
||||||
<div
|
|
||||||
className="color-picker-hash"
|
|
||||||
ref={labelRef}
|
|
||||||
style={{
|
|
||||||
width: "20px",
|
|
||||||
}}
|
|
||||||
onPointerDown={(event) => {
|
|
||||||
if (!originalElement.current) {
|
|
||||||
originalElement.current = deepCopyElement(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!accumulatedDimensionChange.current) {
|
|
||||||
accumulatedDimensionChange.current = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onPointerUp={(event) => {
|
|
||||||
accumulatedDimensionChange.current = 0;
|
|
||||||
originalElement.current = undefined;
|
|
||||||
}}
|
|
||||||
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 eventTarget = event.target;
|
|
||||||
|
|
||||||
if (eventTarget instanceof HTMLInputElement) {
|
|
||||||
const value = Number(eventTarget.value);
|
|
||||||
if (isNaN(value)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.key === KEYS.ENTER) {
|
|
||||||
handleChange(value, 0, "keyDown");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
ref={inputRef}
|
|
||||||
></input>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DragInput;
|
|
@ -0,0 +1,61 @@
|
|||||||
|
import { mutateElement } from "../../element/mutateElement";
|
||||||
|
import type { ExcalidrawElement } from "../../element/types";
|
||||||
|
import { degreeToRadian, radianToDegree } from "../../math";
|
||||||
|
import DragInput from "./DragInput";
|
||||||
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
|
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
|
|
||||||
|
interface AngleProps {
|
||||||
|
element: ExcalidrawElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STEP_SIZE = 15;
|
||||||
|
|
||||||
|
const Angle = ({ element }: AngleProps) => {
|
||||||
|
const handleDegreeChange: DragInputCallbackType = (
|
||||||
|
accumulatedChange,
|
||||||
|
instantChange,
|
||||||
|
stateAtStart,
|
||||||
|
shouldKeepAspectRatio,
|
||||||
|
shouldChangeByStepSize,
|
||||||
|
nextValue,
|
||||||
|
) => {
|
||||||
|
if (nextValue !== undefined) {
|
||||||
|
const nextAngle = degreeToRadian(nextValue);
|
||||||
|
mutateElement(element, {
|
||||||
|
angle: nextAngle,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stateAtStart) {
|
||||||
|
const originalAngleInDegrees =
|
||||||
|
Math.round(radianToDegree(stateAtStart.angle) * 100) / 100;
|
||||||
|
const changeInDegrees = Math.round(accumulatedChange);
|
||||||
|
let nextAngleInDegrees = (originalAngleInDegrees + changeInDegrees) % 360;
|
||||||
|
if (shouldChangeByStepSize) {
|
||||||
|
nextAngleInDegrees = getStepSizedValue(nextAngleInDegrees, STEP_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
mutateElement(element, {
|
||||||
|
angle: degreeToRadian(
|
||||||
|
nextAngleInDegrees < 0
|
||||||
|
? nextAngleInDegrees + 360
|
||||||
|
: nextAngleInDegrees,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DragInput
|
||||||
|
label="A"
|
||||||
|
value={Math.round(radianToDegree(element.angle) * 100) / 100}
|
||||||
|
element={element}
|
||||||
|
dragInputCallback={handleDegreeChange}
|
||||||
|
editable={isPropertyEditable(element, "angle")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Angle;
|
@ -0,0 +1,160 @@
|
|||||||
|
import type { ExcalidrawElement } from "../../element/types";
|
||||||
|
import DragInput from "./DragInput";
|
||||||
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
|
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
|
import { mutateElement } from "../../element/mutateElement";
|
||||||
|
|
||||||
|
interface DimensionDragInputProps {
|
||||||
|
property: "width" | "height";
|
||||||
|
element: ExcalidrawElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STEP_SIZE = 10;
|
||||||
|
const _shouldKeepAspectRatio = (element: ExcalidrawElement) => {
|
||||||
|
return element.type === "image";
|
||||||
|
};
|
||||||
|
|
||||||
|
const newOrigin = (
|
||||||
|
x1: number,
|
||||||
|
y1: number,
|
||||||
|
w1: number,
|
||||||
|
h1: number,
|
||||||
|
w2: number,
|
||||||
|
h2: number,
|
||||||
|
angle: number,
|
||||||
|
) => {
|
||||||
|
/**
|
||||||
|
* The formula below is the result of solving
|
||||||
|
* rotate(x1, y1, cx1, cy1, angle) = rotate(x2, y2, cx2, cy2, angle)
|
||||||
|
* where rotate is the function defined in math.ts
|
||||||
|
*
|
||||||
|
* This is so that the new origin (x2, y2),
|
||||||
|
* when rotated against the new center (cx2, cy2),
|
||||||
|
* coincides with (x1, y1) rotated against (cx1, cy1)
|
||||||
|
*
|
||||||
|
* The reason for doing this computation is so the element's top left corner
|
||||||
|
* on the canvas remains fixed after any changes in its dimension.
|
||||||
|
*/
|
||||||
|
|
||||||
|
return {
|
||||||
|
x:
|
||||||
|
x1 +
|
||||||
|
(w1 - w2) / 2 +
|
||||||
|
((w2 - w1) / 2) * Math.cos(angle) +
|
||||||
|
((h1 - h2) / 2) * Math.sin(angle),
|
||||||
|
y:
|
||||||
|
y1 +
|
||||||
|
(h1 - h2) / 2 +
|
||||||
|
((w2 - w1) / 2) * Math.sin(angle) +
|
||||||
|
((h2 - h1) / 2) * Math.cos(angle),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const DimensionDragInput = ({ property, element }: DimensionDragInputProps) => {
|
||||||
|
const handleDimensionChange: DragInputCallbackType = (
|
||||||
|
accumulatedChange,
|
||||||
|
instantChange,
|
||||||
|
stateAtStart,
|
||||||
|
shouldKeepAspectRatio,
|
||||||
|
shouldChangeByStepSize,
|
||||||
|
nextValue,
|
||||||
|
) => {
|
||||||
|
if (stateAtStart) {
|
||||||
|
const keepAspectRatio =
|
||||||
|
shouldKeepAspectRatio || _shouldKeepAspectRatio(element);
|
||||||
|
const aspectRatio = stateAtStart.width / stateAtStart.height;
|
||||||
|
|
||||||
|
if (nextValue !== undefined) {
|
||||||
|
const nextWidth = Math.max(
|
||||||
|
property === "width"
|
||||||
|
? nextValue
|
||||||
|
: keepAspectRatio
|
||||||
|
? nextValue * aspectRatio
|
||||||
|
: stateAtStart.width,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const nextHeight = Math.max(
|
||||||
|
property === "height"
|
||||||
|
? nextValue
|
||||||
|
: keepAspectRatio
|
||||||
|
? nextValue / aspectRatio
|
||||||
|
: stateAtStart.height,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
mutateElement(element, {
|
||||||
|
...newOrigin(
|
||||||
|
element.x,
|
||||||
|
element.y,
|
||||||
|
element.width,
|
||||||
|
element.height,
|
||||||
|
nextWidth,
|
||||||
|
nextHeight,
|
||||||
|
element.angle,
|
||||||
|
),
|
||||||
|
width: nextWidth,
|
||||||
|
height: nextHeight,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const changeInWidth = property === "width" ? accumulatedChange : 0;
|
||||||
|
const changeInHeight = property === "height" ? accumulatedChange : 0;
|
||||||
|
|
||||||
|
let nextWidth = Math.max(0, stateAtStart.width + changeInWidth);
|
||||||
|
if (property === "width") {
|
||||||
|
if (shouldChangeByStepSize) {
|
||||||
|
nextWidth = getStepSizedValue(nextWidth, STEP_SIZE);
|
||||||
|
} else {
|
||||||
|
nextWidth = Math.round(nextWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextHeight = Math.max(0, stateAtStart.height + changeInHeight);
|
||||||
|
if (property === "height") {
|
||||||
|
if (shouldChangeByStepSize) {
|
||||||
|
nextHeight = getStepSizedValue(nextHeight, STEP_SIZE);
|
||||||
|
} else {
|
||||||
|
nextHeight = Math.round(nextHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keepAspectRatio) {
|
||||||
|
if (property === "width") {
|
||||||
|
nextHeight = Math.round((nextWidth / aspectRatio) * 100) / 100;
|
||||||
|
} else {
|
||||||
|
nextWidth = Math.round(nextHeight * aspectRatio * 100) / 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutateElement(element, {
|
||||||
|
width: nextWidth,
|
||||||
|
height: nextHeight,
|
||||||
|
...newOrigin(
|
||||||
|
stateAtStart.x,
|
||||||
|
stateAtStart.y,
|
||||||
|
stateAtStart.width,
|
||||||
|
stateAtStart.height,
|
||||||
|
nextWidth,
|
||||||
|
nextHeight,
|
||||||
|
stateAtStart.angle,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DragInput
|
||||||
|
label={property === "width" ? "W" : "H"}
|
||||||
|
element={element}
|
||||||
|
dragInputCallback={handleDimensionChange}
|
||||||
|
value={
|
||||||
|
Math.round(
|
||||||
|
(property === "width" ? element.width : element.height) * 100,
|
||||||
|
) / 100
|
||||||
|
}
|
||||||
|
editable={isPropertyEditable(element, property)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DimensionDragInput;
|
@ -0,0 +1,75 @@
|
|||||||
|
.excalidraw {
|
||||||
|
.drag-input-container {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
box-shadow: 0 0 0 1px var(--color-primary-darkest);
|
||||||
|
border-radius: var(--border-radius-lg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-input-label {
|
||||||
|
height: var(--default-button-size);
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0.5rem 0.5rem 0.5rem 0.75rem;
|
||||||
|
border: 1px solid var(--default-border-color);
|
||||||
|
border-right: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
:root[dir="ltr"] & {
|
||||||
|
border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[dir="rtl"] & {
|
||||||
|
border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0;
|
||||||
|
border-right: 1px solid var(--default-border-color);
|
||||||
|
border-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
color: var(--input-label-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
height: var(--default-button-size);
|
||||||
|
border: 1px solid var(--default-border-color);
|
||||||
|
border-left: 0;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
|
||||||
|
:root[dir="ltr"] & {
|
||||||
|
border-radius: 0 var(--border-radius-lg) var(--border-radius-lg) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[dir="rtl"] & {
|
||||||
|
border-radius: var(--border-radius-lg) 0 0 var(--border-radius-lg);
|
||||||
|
border-left: 1px solid var(--default-border-color);
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
padding: 0.5rem;
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,176 @@
|
|||||||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import throttle from "lodash.throttle";
|
||||||
|
import { EVENT } from "../../constants";
|
||||||
|
import { KEYS } from "../../keys";
|
||||||
|
import type { ExcalidrawElement } from "../../element/types";
|
||||||
|
import { deepCopyElement } from "../../element/newElement";
|
||||||
|
|
||||||
|
import "./DragInput.scss";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
export type DragInputCallbackType = (
|
||||||
|
accumulatedChange: number,
|
||||||
|
instantChange: number,
|
||||||
|
stateAtStart: ExcalidrawElement,
|
||||||
|
shouldKeepAspectRatio: boolean,
|
||||||
|
shouldChangeByStepSize: boolean,
|
||||||
|
nextValue?: number,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
interface StatsDragInputProps {
|
||||||
|
label: string | React.ReactNode;
|
||||||
|
value: number;
|
||||||
|
element: ExcalidrawElement;
|
||||||
|
editable?: boolean;
|
||||||
|
shouldKeepAspectRatio?: boolean;
|
||||||
|
dragInputCallback: DragInputCallbackType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatsDragInput = ({
|
||||||
|
label,
|
||||||
|
dragInputCallback,
|
||||||
|
value,
|
||||||
|
element,
|
||||||
|
editable = true,
|
||||||
|
shouldKeepAspectRatio,
|
||||||
|
}: StatsDragInputProps) => {
|
||||||
|
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 accumulatedChange: number | null = null;
|
||||||
|
|
||||||
|
document.body.classList.add("dragResize");
|
||||||
|
|
||||||
|
const onPointerMove = (event: PointerEvent) => {
|
||||||
|
if (!stateAtStart) {
|
||||||
|
stateAtStart = deepCopyElement(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accumulatedChange) {
|
||||||
|
accumulatedChange = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastPointer && stateAtStart && accumulatedChange !== null) {
|
||||||
|
const instantChange = event.clientX - lastPointer.x;
|
||||||
|
accumulatedChange += instantChange;
|
||||||
|
|
||||||
|
cbThrottled(
|
||||||
|
accumulatedChange,
|
||||||
|
instantChange,
|
||||||
|
stateAtStart,
|
||||||
|
shouldKeepAspectRatio!!,
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
lastPointer = null;
|
||||||
|
accumulatedChange = null;
|
||||||
|
stateAtStart = 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(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
element,
|
||||||
|
shouldKeepAspectRatio!!,
|
||||||
|
false,
|
||||||
|
v,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
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;
|
@ -0,0 +1,23 @@
|
|||||||
|
import { isFrameLikeElement, isTextElement } from "../../element/typeChecks";
|
||||||
|
import type { ExcalidrawElement } from "../../element/types";
|
||||||
|
|
||||||
|
export const isPropertyEditable = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
property: keyof ExcalidrawElement,
|
||||||
|
) => {
|
||||||
|
if (property === "height" && isTextElement(element)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (property === "width" && isTextElement(element)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (property === "angle" && isFrameLikeElement(element)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStepSizedValue = (value: number, stepSize: number) => {
|
||||||
|
const v = value + stepSize / 2;
|
||||||
|
return v - (v % stepSize);
|
||||||
|
};
|
Loading…
Reference in New Issue