mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: support Stats bound text fontSize
editing (#8187)
This commit is contained in:
parent
ba8c09d529
commit
abbeed3d5f
5 changed files with 215 additions and 150 deletions
|
@ -15,33 +15,42 @@ import "./DragInput.scss";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
import { cloneJSON } from "../../utils";
|
import { cloneJSON } from "../../utils";
|
||||||
|
|
||||||
export type DragInputCallbackType<T extends StatsInputProperty> = (props: {
|
export type DragInputCallbackType<
|
||||||
|
P extends StatsInputProperty,
|
||||||
|
E = ExcalidrawElement,
|
||||||
|
> = (props: {
|
||||||
accumulatedChange: number;
|
accumulatedChange: number;
|
||||||
instantChange: number;
|
instantChange: number;
|
||||||
originalElements: readonly ExcalidrawElement[];
|
originalElements: readonly E[];
|
||||||
originalElementsMap: ElementsMap;
|
originalElementsMap: ElementsMap;
|
||||||
shouldKeepAspectRatio: boolean;
|
shouldKeepAspectRatio: boolean;
|
||||||
shouldChangeByStepSize: boolean;
|
shouldChangeByStepSize: boolean;
|
||||||
nextValue?: number;
|
nextValue?: number;
|
||||||
property: T;
|
property: P;
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
originalAppState: AppState;
|
originalAppState: AppState;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
|
||||||
interface StatsDragInputProps<T extends StatsInputProperty> {
|
interface StatsDragInputProps<
|
||||||
|
T extends StatsInputProperty,
|
||||||
|
E = ExcalidrawElement,
|
||||||
|
> {
|
||||||
label: string | React.ReactNode;
|
label: string | React.ReactNode;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
value: number | "Mixed";
|
value: number | "Mixed";
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly E[];
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
shouldKeepAspectRatio?: boolean;
|
shouldKeepAspectRatio?: boolean;
|
||||||
dragInputCallback: DragInputCallbackType<T>;
|
dragInputCallback: DragInputCallbackType<T, E>;
|
||||||
property: T;
|
property: T;
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatsDragInput = <T extends StatsInputProperty>({
|
const StatsDragInput = <
|
||||||
|
T extends StatsInputProperty,
|
||||||
|
E extends ExcalidrawElement = ExcalidrawElement,
|
||||||
|
>({
|
||||||
label,
|
label,
|
||||||
icon,
|
icon,
|
||||||
dragInputCallback,
|
dragInputCallback,
|
||||||
|
@ -52,7 +61,7 @@ const StatsDragInput = <T extends StatsInputProperty>({
|
||||||
property,
|
property,
|
||||||
scene,
|
scene,
|
||||||
appState,
|
appState,
|
||||||
}: StatsDragInputProps<T>) => {
|
}: StatsDragInputProps<T, E>) => {
|
||||||
const app = useApp();
|
const app = useApp();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const labelRef = useRef<HTMLDivElement>(null);
|
const labelRef = useRef<HTMLDivElement>(null);
|
||||||
|
@ -61,7 +70,7 @@ const StatsDragInput = <T extends StatsInputProperty>({
|
||||||
|
|
||||||
const stateRef = useRef<{
|
const stateRef = useRef<{
|
||||||
originalAppState: AppState;
|
originalAppState: AppState;
|
||||||
originalElements: readonly ExcalidrawElement[];
|
originalElements: readonly E[];
|
||||||
lastUpdatedValue: string;
|
lastUpdatedValue: string;
|
||||||
updatePending: boolean;
|
updatePending: boolean;
|
||||||
}>(null!);
|
}>(null!);
|
||||||
|
@ -82,7 +91,7 @@ const StatsDragInput = <T extends StatsInputProperty>({
|
||||||
|
|
||||||
const handleInputValue = (
|
const handleInputValue = (
|
||||||
updatedValue: string,
|
updatedValue: string,
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly E[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
if (!stateRef.current.updatePending) {
|
if (!stateRef.current.updatePending) {
|
||||||
|
@ -173,9 +182,18 @@ const StatsDragInput = <T extends StatsInputProperty>({
|
||||||
y: number;
|
y: number;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
|
|
||||||
let originalElements: ExcalidrawElement[] | null = null;
|
|
||||||
let originalElementsMap: Map<string, ExcalidrawElement> | null =
|
let originalElementsMap: Map<string, ExcalidrawElement> | null =
|
||||||
null;
|
app.scene
|
||||||
|
.getNonDeletedElements()
|
||||||
|
.reduce((acc: ElementsMap, element) => {
|
||||||
|
acc.set(element.id, deepCopyElement(element));
|
||||||
|
return acc;
|
||||||
|
}, new Map());
|
||||||
|
|
||||||
|
let originalElements: readonly E[] | null = elements.map(
|
||||||
|
(element) => originalElementsMap!.get(element.id) as E,
|
||||||
|
);
|
||||||
|
|
||||||
const originalAppState: AppState = cloneJSON(appState);
|
const originalAppState: AppState = cloneJSON(appState);
|
||||||
|
|
||||||
let accumulatedChange: number | null = null;
|
let accumulatedChange: number | null = null;
|
||||||
|
@ -183,21 +201,6 @@ const StatsDragInput = <T extends StatsInputProperty>({
|
||||||
document.body.classList.add("excalidraw-cursor-resize");
|
document.body.classList.add("excalidraw-cursor-resize");
|
||||||
|
|
||||||
const onPointerMove = (event: PointerEvent) => {
|
const onPointerMove = (event: PointerEvent) => {
|
||||||
if (!originalElementsMap) {
|
|
||||||
originalElementsMap = app.scene
|
|
||||||
.getNonDeletedElements()
|
|
||||||
.reduce((acc, element) => {
|
|
||||||
acc.set(element.id, deepCopyElement(element));
|
|
||||||
return acc;
|
|
||||||
}, new Map() as ElementsMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!originalElements) {
|
|
||||||
originalElements = elements.map(
|
|
||||||
(element) => originalElementsMap!.get(element.id)!,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!accumulatedChange) {
|
if (!accumulatedChange) {
|
||||||
accumulatedChange = 0;
|
accumulatedChange = 0;
|
||||||
}
|
}
|
||||||
|
@ -205,6 +208,7 @@ const StatsDragInput = <T extends StatsInputProperty>({
|
||||||
if (
|
if (
|
||||||
lastPointer &&
|
lastPointer &&
|
||||||
originalElementsMap !== null &&
|
originalElementsMap !== null &&
|
||||||
|
originalElements !== null &&
|
||||||
accumulatedChange !== null
|
accumulatedChange !== null
|
||||||
) {
|
) {
|
||||||
const instantChange = event.clientX - lastPointer.x;
|
const instantChange = event.clientX - lastPointer.x;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import type { ExcalidrawTextElement } from "../../element/types";
|
import type {
|
||||||
import { refreshTextDimensions } from "../../element/newElement";
|
ExcalidrawElement,
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
} from "../../element/types";
|
||||||
import StatsDragInput from "./DragInput";
|
import StatsDragInput from "./DragInput";
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import { mutateElement } from "../../element/mutateElement";
|
import { mutateElement } from "../../element/mutateElement";
|
||||||
|
@ -7,10 +9,12 @@ import { getStepSizedValue } from "./utils";
|
||||||
import { fontSizeIcon } from "../icons";
|
import { fontSizeIcon } from "../icons";
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
import { isTextElement } from "../../element";
|
import { isTextElement, redrawTextBoundingBox } from "../../element";
|
||||||
|
import { hasBoundTextElement } from "../../element/typeChecks";
|
||||||
|
import { getBoundTextElement } from "../../element/textElement";
|
||||||
|
|
||||||
interface FontSizeProps {
|
interface FontSizeProps {
|
||||||
element: ExcalidrawTextElement;
|
element: ExcalidrawElement;
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
property: "fontSize";
|
property: "fontSize";
|
||||||
|
@ -20,7 +24,8 @@ const MIN_FONT_SIZE = 4;
|
||||||
const STEP_SIZE = 4;
|
const STEP_SIZE = 4;
|
||||||
|
|
||||||
const handleFontSizeChange: DragInputCallbackType<
|
const handleFontSizeChange: DragInputCallbackType<
|
||||||
FontSizeProps["property"]
|
FontSizeProps["property"],
|
||||||
|
ExcalidrawTextElement
|
||||||
> = ({
|
> = ({
|
||||||
accumulatedChange,
|
accumulatedChange,
|
||||||
originalElements,
|
originalElements,
|
||||||
|
@ -36,50 +41,52 @@ const handleFontSizeChange: DragInputCallbackType<
|
||||||
if (!latestElement || !isTextElement(latestElement)) {
|
if (!latestElement || !isTextElement(latestElement)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let nextFontSize;
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
const nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
|
nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
|
||||||
|
} else if (origElement.type === "text") {
|
||||||
const newElement = {
|
|
||||||
...latestElement,
|
|
||||||
fontSize: nextFontSize,
|
|
||||||
};
|
|
||||||
const updates = refreshTextDimensions(newElement, null, elementsMap);
|
|
||||||
mutateElement(latestElement, {
|
|
||||||
...updates,
|
|
||||||
fontSize: nextFontSize,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (origElement.type === "text") {
|
|
||||||
const originalFontSize = Math.round(origElement.fontSize);
|
const originalFontSize = Math.round(origElement.fontSize);
|
||||||
const changeInFontSize = Math.round(accumulatedChange);
|
const changeInFontSize = Math.round(accumulatedChange);
|
||||||
let nextFontSize = Math.max(
|
nextFontSize = Math.max(
|
||||||
originalFontSize + changeInFontSize,
|
originalFontSize + changeInFontSize,
|
||||||
MIN_FONT_SIZE,
|
MIN_FONT_SIZE,
|
||||||
);
|
);
|
||||||
if (shouldChangeByStepSize) {
|
if (shouldChangeByStepSize) {
|
||||||
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
|
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
|
||||||
}
|
}
|
||||||
const newElement = {
|
}
|
||||||
...latestElement,
|
|
||||||
fontSize: nextFontSize,
|
if (nextFontSize) {
|
||||||
};
|
|
||||||
const updates = refreshTextDimensions(newElement, null, elementsMap);
|
|
||||||
mutateElement(latestElement, {
|
mutateElement(latestElement, {
|
||||||
...updates,
|
|
||||||
fontSize: nextFontSize,
|
fontSize: nextFontSize,
|
||||||
});
|
});
|
||||||
|
redrawTextBoundingBox(
|
||||||
|
latestElement,
|
||||||
|
scene.getContainerElement(latestElement),
|
||||||
|
scene.getNonDeletedElementsMap(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const FontSize = ({ element, scene, appState, property }: FontSizeProps) => {
|
const FontSize = ({ element, scene, appState, property }: FontSizeProps) => {
|
||||||
|
const _element = isTextElement(element)
|
||||||
|
? element
|
||||||
|
: hasBoundTextElement(element)
|
||||||
|
? getBoundTextElement(element, scene.getNonDeletedElementsMap())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!_element) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatsDragInput
|
<StatsDragInput
|
||||||
label="F"
|
label="F"
|
||||||
value={Math.round(element.fontSize * 10) / 10}
|
value={Math.round(_element.fontSize * 10) / 10}
|
||||||
elements={[element]}
|
elements={[_element]}
|
||||||
dragInputCallback={handleFontSizeChange}
|
dragInputCallback={handleFontSizeChange}
|
||||||
icon={fontSizeIcon}
|
icon={fontSizeIcon}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { isTextElement, refreshTextDimensions } from "../../element";
|
import { isTextElement, redrawTextBoundingBox } from "../../element";
|
||||||
import { mutateElement } from "../../element/mutateElement";
|
import { mutateElement } from "../../element/mutateElement";
|
||||||
import { isBoundToContainer } from "../../element/typeChecks";
|
import { hasBoundTextElement } from "../../element/typeChecks";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
|
NonDeletedSceneElementsMap,
|
||||||
} from "../../element/types";
|
} from "../../element/types";
|
||||||
import { isInGroup } from "../../groups";
|
import { isInGroup } from "../../groups";
|
||||||
import type Scene from "../../scene/Scene";
|
import type Scene from "../../scene/Scene";
|
||||||
|
@ -12,10 +13,12 @@ import StatsDragInput from "./DragInput";
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import { getStepSizedValue } from "./utils";
|
import { getStepSizedValue } from "./utils";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
import { getBoundTextElement } from "../../element/textElement";
|
||||||
|
|
||||||
interface MultiFontSizeProps {
|
interface MultiFontSizeProps {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
|
elementsMap: NonDeletedSceneElementsMap;
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
property: "fontSize";
|
property: "fontSize";
|
||||||
}
|
}
|
||||||
|
@ -25,14 +28,34 @@ const STEP_SIZE = 4;
|
||||||
|
|
||||||
const getApplicableTextElements = (
|
const getApplicableTextElements = (
|
||||||
elements: readonly (ExcalidrawElement | undefined)[],
|
elements: readonly (ExcalidrawElement | undefined)[],
|
||||||
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
) =>
|
) =>
|
||||||
elements.filter(
|
elements.reduce(
|
||||||
(el) =>
|
(acc: ExcalidrawTextElement[], el) => {
|
||||||
el && !isInGroup(el) && isTextElement(el) && !isBoundToContainer(el),
|
if (!el || isInGroup(el)) {
|
||||||
) as ExcalidrawTextElement[];
|
return acc;
|
||||||
|
}
|
||||||
|
if (isTextElement(el)) {
|
||||||
|
acc.push(el);
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
if (hasBoundTextElement(el)) {
|
||||||
|
const boundTextElement = getBoundTextElement(el, elementsMap);
|
||||||
|
if (boundTextElement) {
|
||||||
|
acc.push(boundTextElement);
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const handleFontSizeChange: DragInputCallbackType<
|
const handleFontSizeChange: DragInputCallbackType<
|
||||||
MultiFontSizeProps["property"]
|
MultiFontSizeProps["property"],
|
||||||
|
ExcalidrawTextElement
|
||||||
> = ({
|
> = ({
|
||||||
accumulatedChange,
|
accumulatedChange,
|
||||||
originalElements,
|
originalElements,
|
||||||
|
@ -41,41 +64,35 @@ const handleFontSizeChange: DragInputCallbackType<
|
||||||
scene,
|
scene,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const latestTextElements = getApplicableTextElements(
|
const latestTextElements = originalElements.map((el) =>
|
||||||
originalElements.map((el) => elementsMap.get(el.id)),
|
elementsMap.get(el.id),
|
||||||
);
|
) as ExcalidrawTextElement[];
|
||||||
|
|
||||||
|
let nextFontSize;
|
||||||
|
|
||||||
if (nextValue) {
|
if (nextValue) {
|
||||||
const nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
|
nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
|
||||||
|
|
||||||
for (const textElement of latestTextElements.map((el) =>
|
for (const textElement of latestTextElements) {
|
||||||
elementsMap.get(el.id),
|
|
||||||
)) {
|
|
||||||
if (!textElement || !isTextElement(textElement)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const newElement = {
|
|
||||||
...textElement,
|
|
||||||
fontSize: nextFontSize,
|
|
||||||
};
|
|
||||||
const updates = refreshTextDimensions(newElement, null, elementsMap);
|
|
||||||
mutateElement(
|
mutateElement(
|
||||||
textElement,
|
textElement,
|
||||||
{
|
{
|
||||||
...updates,
|
|
||||||
fontSize: nextFontSize,
|
fontSize: nextFontSize,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
redrawTextBoundingBox(
|
||||||
|
textElement,
|
||||||
|
scene.getContainerElement(textElement),
|
||||||
|
elementsMap,
|
||||||
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.triggerUpdate();
|
scene.triggerUpdate();
|
||||||
return;
|
} else {
|
||||||
}
|
const originalTextElements = originalElements as ExcalidrawTextElement[];
|
||||||
|
|
||||||
const originalTextElements = originalElements.filter(
|
|
||||||
(el) => !isInGroup(el) && isTextElement(el) && !isBoundToContainer(el),
|
|
||||||
) as ExcalidrawTextElement[];
|
|
||||||
|
|
||||||
for (let i = 0; i < latestTextElements.length; i++) {
|
for (let i = 0; i < latestTextElements.length; i++) {
|
||||||
const latestElement = latestTextElements[i];
|
const latestElement = latestTextElements[i];
|
||||||
|
@ -90,22 +107,24 @@ const handleFontSizeChange: DragInputCallbackType<
|
||||||
if (shouldChangeByStepSize) {
|
if (shouldChangeByStepSize) {
|
||||||
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
|
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE);
|
||||||
}
|
}
|
||||||
const newElement = {
|
|
||||||
...latestElement,
|
|
||||||
fontSize: nextFontSize,
|
|
||||||
};
|
|
||||||
const updates = refreshTextDimensions(newElement, null, elementsMap);
|
|
||||||
mutateElement(
|
mutateElement(
|
||||||
latestElement,
|
latestElement,
|
||||||
{
|
{
|
||||||
...updates,
|
|
||||||
fontSize: nextFontSize,
|
fontSize: nextFontSize,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
redrawTextBoundingBox(
|
||||||
|
latestElement,
|
||||||
|
scene.getContainerElement(latestElement),
|
||||||
|
elementsMap,
|
||||||
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
scene.triggerUpdate();
|
scene.triggerUpdate();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const MultiFontSize = ({
|
const MultiFontSize = ({
|
||||||
|
@ -113,8 +132,14 @@ const MultiFontSize = ({
|
||||||
scene,
|
scene,
|
||||||
appState,
|
appState,
|
||||||
property,
|
property,
|
||||||
|
elementsMap,
|
||||||
}: MultiFontSizeProps) => {
|
}: MultiFontSizeProps) => {
|
||||||
const latestTextElements = getApplicableTextElements(elements);
|
const latestTextElements = getApplicableTextElements(elements, elementsMap);
|
||||||
|
|
||||||
|
if (!latestTextElements.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const fontSizes = latestTextElements.map(
|
const fontSizes = latestTextElements.map(
|
||||||
(textEl) => Math.round(textEl.fontSize * 10) / 10,
|
(textEl) => Math.round(textEl.fontSize * 10) / 10,
|
||||||
);
|
);
|
||||||
|
@ -125,7 +150,7 @@ const MultiFontSize = ({
|
||||||
<StatsDragInput
|
<StatsDragInput
|
||||||
label="F"
|
label="F"
|
||||||
icon={fontSizeIcon}
|
icon={fontSizeIcon}
|
||||||
elements={elements}
|
elements={latestTextElements}
|
||||||
dragInputCallback={handleFontSizeChange}
|
dragInputCallback={handleFontSizeChange}
|
||||||
value={value}
|
value={value}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
|
|
|
@ -21,7 +21,6 @@ import type Scene from "../../scene/Scene";
|
||||||
import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App";
|
import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App";
|
||||||
import { getAtomicUnits } from "./utils";
|
import { getAtomicUnits } from "./utils";
|
||||||
import { STATS_PANELS } from "../../constants";
|
import { STATS_PANELS } from "../../constants";
|
||||||
import { isTextElement } from "../../element";
|
|
||||||
|
|
||||||
interface StatsProps {
|
interface StatsProps {
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
|
@ -216,14 +215,12 @@ export const StatsInner = memo(
|
||||||
scene={scene}
|
scene={scene}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
{singleElement.type === "text" && (
|
|
||||||
<FontSize
|
<FontSize
|
||||||
property="fontSize"
|
property="fontSize"
|
||||||
element={singleElement}
|
element={singleElement}
|
||||||
scene={scene}
|
scene={scene}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -278,14 +275,13 @@ export const StatsInner = memo(
|
||||||
scene={scene}
|
scene={scene}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
{multipleElements.some((el) => isTextElement(el)) && (
|
|
||||||
<MultiFontSize
|
<MultiFontSize
|
||||||
property="fontSize"
|
property="fontSize"
|
||||||
elements={multipleElements}
|
elements={multipleElements}
|
||||||
scene={scene}
|
scene={scene}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
|
elementsMap={elementsMap}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import * as StaticScene from "../../renderer/staticScene";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
import { reseed } from "../../random";
|
import { reseed } from "../../random";
|
||||||
import { setDateTimeForTests } from "../../utils";
|
import { setDateTimeForTests } from "../../utils";
|
||||||
import { Excalidraw } from "../..";
|
import { Excalidraw, mutateElement } from "../..";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
@ -37,10 +37,14 @@ const editInput = (input: HTMLInputElement, value: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatsProperty = (label: string) => {
|
const getStatsProperty = (label: string) => {
|
||||||
|
const elementStats = UI.queryStats()?.querySelector("#elementStats");
|
||||||
|
|
||||||
if (elementStats) {
|
if (elementStats) {
|
||||||
const properties = elementStats?.querySelector(".statsItem");
|
const properties = elementStats?.querySelector(".statsItem");
|
||||||
return properties?.querySelector?.(
|
return (
|
||||||
|
properties?.querySelector?.(
|
||||||
`.drag-input-container[data-testid="${label}"]`,
|
`.drag-input-container[data-testid="${label}"]`,
|
||||||
|
) || null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +61,7 @@ const testInputProperty = (
|
||||||
const input = getStatsProperty(label)?.querySelector(
|
const input = getStatsProperty(label)?.querySelector(
|
||||||
".drag-input",
|
".drag-input",
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(input).not.toBeNull();
|
expect(input).toBeDefined();
|
||||||
expect(input.value).toBe(initialValue.toString());
|
expect(input.value).toBe(initialValue.toString());
|
||||||
editInput(input, String(nextValue));
|
editInput(input, String(nextValue));
|
||||||
if (property === "angle") {
|
if (property === "angle") {
|
||||||
|
@ -131,8 +135,8 @@ describe("stats for a generic element", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should open stats", () => {
|
it("should open stats", () => {
|
||||||
expect(stats).not.toBeNull();
|
expect(stats).toBeDefined();
|
||||||
expect(elementStats).not.toBeNull();
|
expect(elementStats).toBeDefined();
|
||||||
|
|
||||||
// title
|
// title
|
||||||
const title = elementStats?.querySelector("h3");
|
const title = elementStats?.querySelector("h3");
|
||||||
|
@ -140,18 +144,18 @@ describe("stats for a generic element", () => {
|
||||||
|
|
||||||
// element type
|
// element type
|
||||||
const elementType = elementStats?.querySelector(".elementType");
|
const elementType = elementStats?.querySelector(".elementType");
|
||||||
expect(elementType).not.toBeNull();
|
expect(elementType).toBeDefined();
|
||||||
expect(elementType?.lastChild?.nodeValue).toBe(t("element.rectangle"));
|
expect(elementType?.lastChild?.nodeValue).toBe(t("element.rectangle"));
|
||||||
|
|
||||||
// properties
|
// properties
|
||||||
const properties = elementStats?.querySelector(".statsItem");
|
const properties = elementStats?.querySelector(".statsItem");
|
||||||
expect(properties?.childNodes).not.toBeNull();
|
expect(properties?.childNodes).toBeDefined();
|
||||||
["X", "Y", "W", "H", "A"].forEach((label) => () => {
|
["X", "Y", "W", "H", "A"].forEach((label) => () => {
|
||||||
expect(
|
expect(
|
||||||
properties?.querySelector?.(
|
properties?.querySelector?.(
|
||||||
`.drag-input-container[data-testid="${label}"]`,
|
`.drag-input-container[data-testid="${label}"]`,
|
||||||
),
|
),
|
||||||
).not.toBeNull();
|
).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -174,7 +178,7 @@ describe("stats for a generic element", () => {
|
||||||
const input = getStatsProperty("W")?.querySelector(
|
const input = getStatsProperty("W")?.querySelector(
|
||||||
".drag-input",
|
".drag-input",
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(input).not.toBeNull();
|
expect(input).toBeDefined();
|
||||||
expect(input.value).toBe(rectangle.width.toString());
|
expect(input.value).toBe(rectangle.width.toString());
|
||||||
editInput(input, "123.123");
|
editInput(input, "123.123");
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
|
@ -333,7 +337,7 @@ describe("stats for a non-generic element", () => {
|
||||||
const input = getStatsProperty("F")?.querySelector(
|
const input = getStatsProperty("F")?.querySelector(
|
||||||
".drag-input",
|
".drag-input",
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(input).not.toBeNull();
|
expect(input).toBeDefined();
|
||||||
expect(input.value).toBe(text.fontSize.toString());
|
expect(input.value).toBe(text.fontSize.toString());
|
||||||
editInput(input, "36");
|
editInput(input, "36");
|
||||||
expect(text.fontSize).toBe(36);
|
expect(text.fontSize).toBe(36);
|
||||||
|
@ -366,7 +370,7 @@ describe("stats for a non-generic element", () => {
|
||||||
|
|
||||||
elementStats = stats?.querySelector("#elementStats");
|
elementStats = stats?.querySelector("#elementStats");
|
||||||
|
|
||||||
expect(elementStats).not.toBeNull();
|
expect(elementStats).toBeDefined();
|
||||||
|
|
||||||
// cannot change angle
|
// cannot change angle
|
||||||
const angle = getStatsProperty("A")?.querySelector(".drag-input");
|
const angle = getStatsProperty("A")?.querySelector(".drag-input");
|
||||||
|
@ -387,7 +391,7 @@ describe("stats for a non-generic element", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
elementStats = stats?.querySelector("#elementStats");
|
elementStats = stats?.querySelector("#elementStats");
|
||||||
expect(elementStats).not.toBeNull();
|
expect(elementStats).toBeDefined();
|
||||||
const widthToHeight = image.width / image.height;
|
const widthToHeight = image.width / image.height;
|
||||||
|
|
||||||
// when width or height is changed, the aspect ratio is preserved
|
// when width or height is changed, the aspect ratio is preserved
|
||||||
|
@ -399,6 +403,35 @@ describe("stats for a non-generic element", () => {
|
||||||
expect(image.height).toBe(80);
|
expect(image.height).toBe(80);
|
||||||
expect(image.width / image.height).toBe(widthToHeight);
|
expect(image.width / image.height).toBe(widthToHeight);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should display fontSize for bound text", () => {
|
||||||
|
const container = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
width: 200,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
const text = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
width: 200,
|
||||||
|
height: 100,
|
||||||
|
containerId: container.id,
|
||||||
|
fontSize: 20,
|
||||||
|
});
|
||||||
|
mutateElement(container, {
|
||||||
|
boundElements: [{ type: "text", id: text.id }],
|
||||||
|
});
|
||||||
|
h.elements = [container, text];
|
||||||
|
|
||||||
|
API.setSelectedElements([container]);
|
||||||
|
const fontSize = getStatsProperty("F")?.querySelector(
|
||||||
|
".drag-input",
|
||||||
|
) as HTMLInputElement;
|
||||||
|
expect(fontSize).toBeDefined();
|
||||||
|
|
||||||
|
editInput(fontSize, "40");
|
||||||
|
|
||||||
|
expect(text.fontSize).toBe(40);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// multiple elements
|
// multiple elements
|
||||||
|
@ -515,25 +548,25 @@ describe("stats for multiple elements", () => {
|
||||||
const width = getStatsProperty("W")?.querySelector(
|
const width = getStatsProperty("W")?.querySelector(
|
||||||
".drag-input",
|
".drag-input",
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(width).not.toBeNull();
|
expect(width).toBeDefined();
|
||||||
expect(width.value).toBe("Mixed");
|
expect(width.value).toBe("Mixed");
|
||||||
|
|
||||||
const height = getStatsProperty("H")?.querySelector(
|
const height = getStatsProperty("H")?.querySelector(
|
||||||
".drag-input",
|
".drag-input",
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(height).not.toBeNull();
|
expect(height).toBeDefined();
|
||||||
expect(height.value).toBe("Mixed");
|
expect(height.value).toBe("Mixed");
|
||||||
|
|
||||||
const angle = getStatsProperty("A")?.querySelector(
|
const angle = getStatsProperty("A")?.querySelector(
|
||||||
".drag-input",
|
".drag-input",
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(angle).not.toBeNull();
|
expect(angle).toBeDefined();
|
||||||
expect(angle.value).toBe("0");
|
expect(angle.value).toBe("0");
|
||||||
|
|
||||||
const fontSize = getStatsProperty("F")?.querySelector(
|
const fontSize = getStatsProperty("F")?.querySelector(
|
||||||
".drag-input",
|
".drag-input",
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(fontSize).not.toBeNull();
|
expect(fontSize).toBeDefined();
|
||||||
|
|
||||||
// changing width does not affect text
|
// changing width does not affect text
|
||||||
editInput(width, "200");
|
editInput(width, "200");
|
||||||
|
@ -579,7 +612,7 @@ describe("stats for multiple elements", () => {
|
||||||
".drag-input",
|
".drag-input",
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
|
|
||||||
expect(x).not.toBeNull();
|
expect(x).toBeDefined();
|
||||||
expect(Number(x.value)).toBe(x1);
|
expect(Number(x.value)).toBe(x1);
|
||||||
|
|
||||||
editInput(x, "300");
|
editInput(x, "300");
|
||||||
|
@ -592,7 +625,7 @@ describe("stats for multiple elements", () => {
|
||||||
".drag-input",
|
".drag-input",
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
|
|
||||||
expect(y).not.toBeNull();
|
expect(y).toBeDefined();
|
||||||
expect(Number(y.value)).toBe(y1);
|
expect(Number(y.value)).toBe(y1);
|
||||||
|
|
||||||
editInput(y, "200");
|
editInput(y, "200");
|
||||||
|
@ -604,13 +637,13 @@ describe("stats for multiple elements", () => {
|
||||||
const width = getStatsProperty("W")?.querySelector(
|
const width = getStatsProperty("W")?.querySelector(
|
||||||
".drag-input",
|
".drag-input",
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(width).not.toBeNull();
|
expect(width).toBeDefined();
|
||||||
expect(Number(width.value)).toBe(200);
|
expect(Number(width.value)).toBe(200);
|
||||||
|
|
||||||
const height = getStatsProperty("H")?.querySelector(
|
const height = getStatsProperty("H")?.querySelector(
|
||||||
".drag-input",
|
".drag-input",
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(height).not.toBeNull();
|
expect(height).toBeDefined();
|
||||||
expect(Number(height.value)).toBe(200);
|
expect(Number(height.value)).toBe(200);
|
||||||
|
|
||||||
editInput(width, "400");
|
editInput(width, "400");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue