mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
switch multi
This commit is contained in:
parent
230a339c7b
commit
41a4dadaaf
5 changed files with 279 additions and 190 deletions
|
@ -392,6 +392,7 @@ import {
|
||||||
} from "../element/textMeasurements";
|
} from "../element/textMeasurements";
|
||||||
|
|
||||||
import ShapeSwitch, {
|
import ShapeSwitch, {
|
||||||
|
adjustBoundTextSize,
|
||||||
GENERIC_SWITCHABLE_SHAPES,
|
GENERIC_SWITCHABLE_SHAPES,
|
||||||
LINEAR_SWITCHABLE_SHAPES,
|
LINEAR_SWITCHABLE_SHAPES,
|
||||||
shapeSwitchAtom,
|
shapeSwitchAtom,
|
||||||
|
@ -446,6 +447,8 @@ import type {
|
||||||
MagicGenerationData,
|
MagicGenerationData,
|
||||||
ExcalidrawNonSelectionElement,
|
ExcalidrawNonSelectionElement,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
|
GenericSwitchableToolType,
|
||||||
|
LinearSwitchableToolType,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import type {
|
import type {
|
||||||
RenderInteractiveSceneCallback,
|
RenderInteractiveSceneCallback,
|
||||||
|
@ -573,7 +576,6 @@ const gesture: Gesture = {
|
||||||
initialDistance: null,
|
initialDistance: null,
|
||||||
initialScale: null,
|
initialScale: null,
|
||||||
};
|
};
|
||||||
let textWysiwygSubmitHandler: (() => void) | null = null;
|
|
||||||
|
|
||||||
class App extends React.Component<AppProps, AppState> {
|
class App extends React.Component<AppProps, AppState> {
|
||||||
canvas: AppClassProperties["canvas"];
|
canvas: AppClassProperties["canvas"];
|
||||||
|
@ -4108,28 +4110,24 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstElement = selectedElements[0];
|
const genericSwitchable = isGenericSwitchableElement(selectedElements);
|
||||||
const isGenericSwitchable =
|
const linearSwitchable = isLinearSwitchableElement(selectedElements);
|
||||||
firstElement && isGenericSwitchableElement(firstElement);
|
|
||||||
const isLinearSwitchable =
|
if (genericSwitchable || linearSwitchable) {
|
||||||
firstElement && isLinearSwitchableElement(firstElement);
|
const firstElement = selectedElements[0];
|
||||||
|
|
||||||
if (
|
|
||||||
selectedElements.length === 1 &&
|
|
||||||
(isGenericSwitchable || isLinearSwitchable)
|
|
||||||
) {
|
|
||||||
if (event.key === KEYS.ESCAPE) {
|
if (event.key === KEYS.ESCAPE) {
|
||||||
editorJotaiStore.set(shapeSwitchAtom, null);
|
editorJotaiStore.set(shapeSwitchAtom, null);
|
||||||
} else if (event.key === KEYS.SLASH || event.key === KEYS.TAB) {
|
} else if (event.key === KEYS.SLASH || event.key === KEYS.TAB) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (editorJotaiStore.get(shapeSwitchAtom)?.type === "panel") {
|
if (editorJotaiStore.get(shapeSwitchAtom)?.type === "panel") {
|
||||||
const index = isGenericSwitchable
|
const index = genericSwitchable
|
||||||
? GENERIC_SWITCHABLE_SHAPES.indexOf(selectedElements[0].type)
|
? GENERIC_SWITCHABLE_SHAPES.indexOf(selectedElements[0].type)
|
||||||
: LINEAR_SWITCHABLE_SHAPES.indexOf(selectedElements[0].type);
|
: LINEAR_SWITCHABLE_SHAPES.indexOf(selectedElements[0].type);
|
||||||
|
|
||||||
const nextType = (
|
const nextType = (
|
||||||
isGenericSwitchable
|
genericSwitchable
|
||||||
? GENERIC_SWITCHABLE_SHAPES[
|
? GENERIC_SWITCHABLE_SHAPES[
|
||||||
(index + 1) % GENERIC_SWITCHABLE_SHAPES.length
|
(index + 1) % GENERIC_SWITCHABLE_SHAPES.length
|
||||||
]
|
]
|
||||||
|
@ -4145,16 +4143,20 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
type: "panel",
|
type: "panel",
|
||||||
});
|
});
|
||||||
if (!editorJotaiStore.get(shapeSwitchFontSizeAtom)) {
|
if (!editorJotaiStore.get(shapeSwitchFontSizeAtom)) {
|
||||||
|
selectedElements.forEach((element) => {
|
||||||
const boundText = getBoundTextElement(
|
const boundText = getBoundTextElement(
|
||||||
firstElement,
|
element,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
if (boundText && isGenericSwitchable) {
|
if (boundText && genericSwitchable && firstElement) {
|
||||||
editorJotaiStore.set(shapeSwitchFontSizeAtom, {
|
editorJotaiStore.set(shapeSwitchFontSizeAtom, {
|
||||||
|
[element.id]: {
|
||||||
fontSize: boundText.fontSize,
|
fontSize: boundText.fontSize,
|
||||||
elementType: firstElement.type,
|
elementType: element.type as GenericSwitchableToolType,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4819,90 +4821,98 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
const firstElement = selectedElements[0];
|
const selectedElementIds = selectedElements.reduce(
|
||||||
|
(acc, element) => ({ ...acc, [element.id]: true }),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
firstElement &&
|
isGenericSwitchableElement(selectedElements) &&
|
||||||
selectedElements.length === 1 &&
|
|
||||||
isGenericSwitchableElement(firstElement) &&
|
|
||||||
isGenericSwitchableToolType(tool.type)
|
isGenericSwitchableToolType(tool.type)
|
||||||
) {
|
) {
|
||||||
ShapeCache.delete(firstElement);
|
selectedElements.forEach((element) => {
|
||||||
|
ShapeCache.delete(element);
|
||||||
|
|
||||||
mutateElement(firstElement, {
|
mutateElement(
|
||||||
type: tool.type,
|
element,
|
||||||
|
{
|
||||||
|
type: tool.type as GenericSwitchableToolType,
|
||||||
roundness:
|
roundness:
|
||||||
tool.type === "diamond" && firstElement.roundness
|
tool.type === "diamond" && element.roundness
|
||||||
? {
|
? {
|
||||||
type: isUsingAdaptiveRadius(tool.type)
|
type: isUsingAdaptiveRadius(tool.type)
|
||||||
? ROUNDNESS.ADAPTIVE_RADIUS
|
? ROUNDNESS.ADAPTIVE_RADIUS
|
||||||
: ROUNDNESS.PROPORTIONAL_RADIUS,
|
: ROUNDNESS.PROPORTIONAL_RADIUS,
|
||||||
value: ROUNDNESS.PROPORTIONAL_RADIUS,
|
value: ROUNDNESS.PROPORTIONAL_RADIUS,
|
||||||
}
|
}
|
||||||
: firstElement.roundness,
|
: element.roundness,
|
||||||
});
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
const boundText = getBoundTextElement(
|
const boundText = getBoundTextElement(
|
||||||
firstElement,
|
element,
|
||||||
this.scene.getNonDeletedElementsMap(),
|
this.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
if (boundText) {
|
if (boundText) {
|
||||||
if (
|
if (
|
||||||
editorJotaiStore.get(shapeSwitchFontSizeAtom)?.elementType ===
|
editorJotaiStore.get(shapeSwitchFontSizeAtom)?.[element.id]
|
||||||
tool.type
|
?.elementType === tool.type
|
||||||
) {
|
) {
|
||||||
mutateElement(
|
mutateElement(
|
||||||
boundText,
|
boundText,
|
||||||
{
|
{
|
||||||
fontSize:
|
fontSize:
|
||||||
editorJotaiStore.get(shapeSwitchFontSizeAtom)?.fontSize ??
|
editorJotaiStore.get(shapeSwitchFontSizeAtom)?.[element.id]
|
||||||
boundText.fontSize,
|
?.fontSize ?? boundText.fontSize,
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.startTextEditing({
|
adjustBoundTextSize(
|
||||||
sceneX: firstElement.x + firstElement.width / 2,
|
element,
|
||||||
sceneY: firstElement.y + firstElement.height / 2,
|
boundText,
|
||||||
container: firstElement as ExcalidrawTextContainer,
|
this.scene.getNonDeletedElementsMap(),
|
||||||
keepContainerDimensions: true,
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => {
|
||||||
selectedElementIds: {
|
return {
|
||||||
[firstElement.id]: true,
|
selectedElementIds,
|
||||||
},
|
|
||||||
activeTool: updateActiveTool(prevState, { type: "selection" }),
|
activeTool: updateActiveTool(prevState, { type: "selection" }),
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
textWysiwygSubmitHandler?.();
|
|
||||||
|
|
||||||
this.store.shouldCaptureIncrement();
|
this.store.shouldCaptureIncrement();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
firstElement &&
|
isLinearSwitchableElement(selectedElements) &&
|
||||||
selectedElements.length === 1 &&
|
|
||||||
isLinearSwitchableElement(firstElement) &&
|
|
||||||
isLinearSwitchableToolType(tool.type)
|
isLinearSwitchableToolType(tool.type)
|
||||||
) {
|
) {
|
||||||
ShapeCache.delete(firstElement);
|
selectedElements.forEach((element) => {
|
||||||
|
ShapeCache.delete(element);
|
||||||
|
|
||||||
mutateElement(firstElement as ExcalidrawLinearElement, {
|
mutateElement(
|
||||||
type: tool.type,
|
element as ExcalidrawLinearElement,
|
||||||
|
{
|
||||||
|
type: tool.type as LinearSwitchableToolType,
|
||||||
startArrowhead: null,
|
startArrowhead: null,
|
||||||
endArrowhead: tool.type === "arrow" ? "arrow" : null,
|
endArrowhead: tool.type === "arrow" ? "arrow" : null,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
const firstElement = selectedElements[0];
|
||||||
|
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
selectedElementIds: {
|
selectedElementIds,
|
||||||
[firstElement.id]: true,
|
selectedLinearElement:
|
||||||
},
|
selectedElements.length === 1
|
||||||
selectedLinearElement: new LinearElementEditor(
|
? new LinearElementEditor(firstElement as ExcalidrawLinearElement)
|
||||||
firstElement as ExcalidrawLinearElement,
|
: null,
|
||||||
),
|
|
||||||
activeTool: updateActiveTool(prevState, { type: "selection" }),
|
activeTool: updateActiveTool(prevState, { type: "selection" }),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -5037,7 +5047,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
textWysiwygSubmitHandler = textWysiwyg({
|
textWysiwyg({
|
||||||
id: element.id,
|
id: element.id,
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
getViewportCoords: (x, y) => {
|
getViewportCoords: (x, y) => {
|
||||||
|
@ -5060,7 +5070,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
|
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
|
||||||
textWysiwygSubmitHandler = null;
|
|
||||||
const isDeleted = !nextOriginalText.trim();
|
const isDeleted = !nextOriginalText.trim();
|
||||||
updateElement(nextOriginalText, isDeleted);
|
updateElement(nextOriginalText, isDeleted);
|
||||||
// select the created text element only if submitting via keyboard
|
// select the created text element only if submitting via keyboard
|
||||||
|
|
|
@ -6,15 +6,24 @@ import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
||||||
|
|
||||||
import { atom, useAtom } from "../editor-jotai";
|
import { atom, useAtom } from "../editor-jotai";
|
||||||
|
|
||||||
import { getElementAbsoluteCoords } from "../element";
|
import { getElementAbsoluteCoords, refreshTextDimensions } from "../element";
|
||||||
import { sceneCoordsToViewportCoords } from "../utils";
|
import { getFontString, sceneCoordsToViewportCoords } from "../utils";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { isArrowElement, isLinearElement } from "../element/typeChecks";
|
import { isArrowElement, isLinearElement } from "../element/typeChecks";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
|
||||||
import "./ShapeSwitch.scss";
|
import {
|
||||||
|
computeBoundTextPosition,
|
||||||
|
getBoundTextMaxHeight,
|
||||||
|
getBoundTextMaxWidth,
|
||||||
|
} from "../element/textElement";
|
||||||
|
import { wrapText } from "../element/textWrapping";
|
||||||
|
import { measureText } from "../element/textMeasurements";
|
||||||
|
import { mutateElement } from "../element/mutateElement";
|
||||||
|
import { getCommonBoundingBox } from "../element/bounds";
|
||||||
|
|
||||||
|
import { ToolButton } from "./ToolButton";
|
||||||
import {
|
import {
|
||||||
ArrowIcon,
|
ArrowIcon,
|
||||||
DiamondIcon,
|
DiamondIcon,
|
||||||
|
@ -22,10 +31,17 @@ import {
|
||||||
LineIcon,
|
LineIcon,
|
||||||
RectangleIcon,
|
RectangleIcon,
|
||||||
} from "./icons";
|
} from "./icons";
|
||||||
import { ToolButton } from "./ToolButton";
|
|
||||||
|
import "./ShapeSwitch.scss";
|
||||||
|
|
||||||
import type App from "./App";
|
import type App from "./App";
|
||||||
import type { ExcalidrawElement } from "../element/types";
|
import type {
|
||||||
|
ElementsMap,
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawTextContainer,
|
||||||
|
ExcalidrawTextElementWithContainer,
|
||||||
|
GenericSwitchableToolType,
|
||||||
|
} from "../element/types";
|
||||||
import type { ToolType } from "../types";
|
import type { ToolType } from "../types";
|
||||||
|
|
||||||
const GAP_HORIZONTAL = 8;
|
const GAP_HORIZONTAL = 8;
|
||||||
|
@ -46,8 +62,10 @@ export const shapeSwitchAtom = atom<
|
||||||
| null
|
| null
|
||||||
>(null);
|
>(null);
|
||||||
export const shapeSwitchFontSizeAtom = atom<{
|
export const shapeSwitchFontSizeAtom = atom<{
|
||||||
|
[id: string]: {
|
||||||
fontSize: number;
|
fontSize: number;
|
||||||
elementType: "rectangle" | "diamond" | "ellipse";
|
elementType: GenericSwitchableToolType;
|
||||||
|
};
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const ShapeSwitch = ({ app }: { app: App }) => {
|
const ShapeSwitch = ({ app }: { app: App }) => {
|
||||||
|
@ -64,22 +82,22 @@ const ShapeSwitch = ({ app }: { app: App }) => {
|
||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene.getNonDeletedElementsMap(),
|
||||||
app.state,
|
app.state,
|
||||||
);
|
);
|
||||||
const firstElement = selectedElements[0];
|
|
||||||
|
|
||||||
const isSingleSelected = firstElement && selectedElements.length === 1;
|
|
||||||
|
|
||||||
// clear if hint target no longer matches
|
// clear if hint target no longer matches
|
||||||
if (shapeSwitch.type === "hint" && firstElement?.id !== shapeSwitch.id) {
|
if (
|
||||||
|
shapeSwitch.type === "hint" &&
|
||||||
|
selectedElements?.[0]?.id !== shapeSwitch.id
|
||||||
|
) {
|
||||||
setShapeSwitch(null);
|
setShapeSwitch(null);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSingleSelected) {
|
if (selectedElements.length === 0) {
|
||||||
setShapeSwitch(null);
|
setShapeSwitch(null);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = { app, element: firstElement };
|
const props = { app, elements: selectedElements };
|
||||||
|
|
||||||
switch (shapeSwitch.type) {
|
switch (shapeSwitch.type) {
|
||||||
case "hint":
|
case "hint":
|
||||||
|
@ -91,7 +109,13 @@ const ShapeSwitch = ({ app }: { app: App }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Hint = ({ app, element }: { app: App; element: ExcalidrawElement }) => {
|
const Hint = ({
|
||||||
|
app,
|
||||||
|
elements,
|
||||||
|
}: {
|
||||||
|
app: App;
|
||||||
|
elements: ExcalidrawElement[];
|
||||||
|
}) => {
|
||||||
const [, setShapeSwitch] = useAtom(shapeSwitchAtom);
|
const [, setShapeSwitch] = useAtom(shapeSwitchAtom);
|
||||||
const hintRef = useRef<HTMLDivElement>(null);
|
const hintRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
@ -114,14 +138,14 @@ const Hint = ({ app, element }: { app: App; element: ExcalidrawElement }) => {
|
||||||
}, [setShapeSwitch]);
|
}, [setShapeSwitch]);
|
||||||
|
|
||||||
const [x1, y1, , , cx, cy] = getElementAbsoluteCoords(
|
const [x1, y1, , , cx, cy] = getElementAbsoluteCoords(
|
||||||
element,
|
elements[0],
|
||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const rotatedTopLeft = pointRotateRads(
|
const rotatedTopLeft = pointRotateRads(
|
||||||
pointFrom(x1, y1),
|
pointFrom(x1, y1),
|
||||||
pointFrom(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
elements[0].angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { x, y } = sceneCoordsToViewportCoords(
|
const { x, y } = sceneCoordsToViewportCoords(
|
||||||
|
@ -135,7 +159,7 @@ const Hint = ({ app, element }: { app: App; element: ExcalidrawElement }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={hintRef}
|
ref={hintRef}
|
||||||
key={element.id}
|
// key={element.id}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
bottom: `${
|
bottom: `${
|
||||||
|
@ -158,17 +182,35 @@ const Hint = ({ app, element }: { app: App; element: ExcalidrawElement }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Panel = ({ app, element }: { app: App; element: ExcalidrawElement }) => {
|
const Panel = ({
|
||||||
const [x1, , , y2, cx, cy] = getElementAbsoluteCoords(
|
app,
|
||||||
element,
|
elements,
|
||||||
|
}: {
|
||||||
|
app: App;
|
||||||
|
elements: ExcalidrawElement[];
|
||||||
|
}) => {
|
||||||
|
let [x1, y2, cx, cy] = [0, 0, 0, 0];
|
||||||
|
let rotatedBottomLeft = [0, 0];
|
||||||
|
|
||||||
|
if (elements.length === 1) {
|
||||||
|
[x1, , , y2, cx, cy] = getElementAbsoluteCoords(
|
||||||
|
elements[0],
|
||||||
app.scene.getNonDeletedElementsMap(),
|
app.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const rotatedBottomLeft = pointRotateRads(
|
rotatedBottomLeft = pointRotateRads(
|
||||||
pointFrom(x1, y2),
|
pointFrom(x1, y2),
|
||||||
pointFrom(cx, cy),
|
pointFrom(cx, cy),
|
||||||
element.angle,
|
elements[0].angle,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
const { minX, maxY, midX, midY } = getCommonBoundingBox(elements);
|
||||||
|
x1 = minX;
|
||||||
|
y2 = maxY;
|
||||||
|
cx = midX;
|
||||||
|
cy = midY;
|
||||||
|
rotatedBottomLeft = pointFrom(x1, y2);
|
||||||
|
}
|
||||||
|
|
||||||
const { x, y } = sceneCoordsToViewportCoords(
|
const { x, y } = sceneCoordsToViewportCoords(
|
||||||
{
|
{
|
||||||
|
@ -178,7 +220,7 @@ const Panel = ({ app, element }: { app: App; element: ExcalidrawElement }) => {
|
||||||
app.state,
|
app.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const SHAPES: [string, string, ReactNode][] = isLinearElement(element)
|
const SHAPES: [string, string, ReactNode][] = isLinearElement(elements[0])
|
||||||
? [
|
? [
|
||||||
["arrow", "5", ArrowIcon],
|
["arrow", "5", ArrowIcon],
|
||||||
["line", "6", LineIcon],
|
["line", "6", LineIcon],
|
||||||
|
@ -203,18 +245,22 @@ const Panel = ({ app, element }: { app: App; element: ExcalidrawElement }) => {
|
||||||
>
|
>
|
||||||
{SHAPES.map(([type, shortcut, icon]) => {
|
{SHAPES.map(([type, shortcut, icon]) => {
|
||||||
const isSelected =
|
const isSelected =
|
||||||
type === element.type ||
|
type === elements[0].type ||
|
||||||
(isArrowElement(element) && element.elbowed && type === "elbow") ||
|
(isArrowElement(elements[0]) &&
|
||||||
(isArrowElement(element) && element.roundness && type === "curve") ||
|
elements[0].elbowed &&
|
||||||
(isArrowElement(element) &&
|
type === "elbow") ||
|
||||||
!element.elbowed &&
|
(isArrowElement(elements[0]) &&
|
||||||
!element.roundness &&
|
elements[0].roundness &&
|
||||||
|
type === "curve") ||
|
||||||
|
(isArrowElement(elements[0]) &&
|
||||||
|
!elements[0].elbowed &&
|
||||||
|
!elements[0].roundness &&
|
||||||
type === "straight");
|
type === "straight");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
className="Shape"
|
className="Shape"
|
||||||
key={`${element.version}_${type}`}
|
key={`${elements[0].version}_${type}`}
|
||||||
type="radio"
|
type="radio"
|
||||||
icon={icon}
|
icon={icon}
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
|
@ -241,4 +287,77 @@ const Panel = ({ app, element }: { app: App; element: ExcalidrawElement }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const adjustBoundTextSize = (
|
||||||
|
container: ExcalidrawTextContainer,
|
||||||
|
boundText: ExcalidrawTextElementWithContainer,
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
) => {
|
||||||
|
const maxWidth = getBoundTextMaxWidth(container, boundText);
|
||||||
|
const maxHeight = getBoundTextMaxHeight(container, boundText);
|
||||||
|
|
||||||
|
const wrappedText = wrapText(
|
||||||
|
boundText.text,
|
||||||
|
getFontString(boundText),
|
||||||
|
maxWidth,
|
||||||
|
);
|
||||||
|
|
||||||
|
let metrics = measureText(
|
||||||
|
wrappedText,
|
||||||
|
getFontString(boundText),
|
||||||
|
boundText.lineHeight,
|
||||||
|
);
|
||||||
|
|
||||||
|
let nextFontSize = boundText.fontSize;
|
||||||
|
while (
|
||||||
|
(metrics.width > maxWidth || metrics.height > maxHeight) &&
|
||||||
|
nextFontSize > 0
|
||||||
|
) {
|
||||||
|
nextFontSize -= 1;
|
||||||
|
const _updatedTextElement = {
|
||||||
|
...boundText,
|
||||||
|
fontSize: nextFontSize,
|
||||||
|
};
|
||||||
|
metrics = measureText(
|
||||||
|
boundText.text,
|
||||||
|
getFontString(_updatedTextElement),
|
||||||
|
boundText.lineHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
mutateElement(
|
||||||
|
boundText,
|
||||||
|
{
|
||||||
|
fontSize: nextFontSize,
|
||||||
|
width: metrics.width,
|
||||||
|
height: metrics.height,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { x, y } = computeBoundTextPosition(container, boundText, elementsMap);
|
||||||
|
|
||||||
|
mutateElement(
|
||||||
|
boundText,
|
||||||
|
{
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
mutateElement(
|
||||||
|
boundText,
|
||||||
|
{
|
||||||
|
...refreshTextDimensions(
|
||||||
|
boundText,
|
||||||
|
container,
|
||||||
|
elementsMap,
|
||||||
|
boundText.originalText,
|
||||||
|
),
|
||||||
|
containerId: container.id,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default ShapeSwitch;
|
export default ShapeSwitch;
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
import { parseClipboard } from "../clipboard";
|
import { parseClipboard } from "../clipboard";
|
||||||
import { CLASSES, POINTER_BUTTON } from "../constants";
|
import { CLASSES, POINTER_BUTTON } from "../constants";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import Scene from "../scene/Scene";
|
|
||||||
import {
|
import {
|
||||||
isWritableElement,
|
isWritableElement,
|
||||||
getFontString,
|
getFontString,
|
||||||
|
@ -35,7 +34,7 @@ import {
|
||||||
computeBoundTextPosition,
|
computeBoundTextPosition,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { getTextWidth, measureText } from "./textMeasurements";
|
import { getTextWidth } from "./textMeasurements";
|
||||||
import { normalizeText } from "./textMeasurements";
|
import { normalizeText } from "./textMeasurements";
|
||||||
import { wrapText } from "./textWrapping";
|
import { wrapText } from "./textWrapping";
|
||||||
import {
|
import {
|
||||||
|
@ -86,7 +85,6 @@ export const textWysiwyg = ({
|
||||||
excalidrawContainer,
|
excalidrawContainer,
|
||||||
app,
|
app,
|
||||||
autoSelect = true,
|
autoSelect = true,
|
||||||
keepContainerDimensions = false,
|
|
||||||
}: {
|
}: {
|
||||||
id: ExcalidrawElement["id"];
|
id: ExcalidrawElement["id"];
|
||||||
/**
|
/**
|
||||||
|
@ -127,8 +125,7 @@ export const textWysiwyg = ({
|
||||||
|
|
||||||
const updateWysiwygStyle = () => {
|
const updateWysiwygStyle = () => {
|
||||||
const appState = app.state;
|
const appState = app.state;
|
||||||
const updatedTextElement =
|
const updatedTextElement = app.scene.getElement<ExcalidrawTextElement>(id);
|
||||||
Scene.getScene(element)?.getElement<ExcalidrawTextElement>(id);
|
|
||||||
|
|
||||||
if (!updatedTextElement) {
|
if (!updatedTextElement) {
|
||||||
return;
|
return;
|
||||||
|
@ -190,48 +187,6 @@ export const textWysiwyg = ({
|
||||||
updatedTextElement as ExcalidrawTextElementWithContainer,
|
updatedTextElement as ExcalidrawTextElementWithContainer,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (keepContainerDimensions) {
|
|
||||||
const wrappedText = wrapText(
|
|
||||||
updatedTextElement.text,
|
|
||||||
getFontString(updatedTextElement),
|
|
||||||
maxWidth,
|
|
||||||
);
|
|
||||||
|
|
||||||
let metrics = measureText(
|
|
||||||
wrappedText,
|
|
||||||
getFontString(updatedTextElement),
|
|
||||||
updatedTextElement.lineHeight,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (width > maxWidth || height > maxHeight) {
|
|
||||||
let nextFontSize = updatedTextElement.fontSize;
|
|
||||||
while (
|
|
||||||
(metrics.width > maxWidth || metrics.height > maxHeight) &&
|
|
||||||
nextFontSize > 0
|
|
||||||
) {
|
|
||||||
nextFontSize -= 1;
|
|
||||||
const _updatedTextElement = {
|
|
||||||
...updatedTextElement,
|
|
||||||
fontSize: nextFontSize,
|
|
||||||
};
|
|
||||||
metrics = measureText(
|
|
||||||
updatedTextElement.text,
|
|
||||||
getFontString(_updatedTextElement),
|
|
||||||
updatedTextElement.lineHeight,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
mutateElement(
|
|
||||||
updatedTextElement,
|
|
||||||
{ fontSize: nextFontSize },
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
width = metrics.width;
|
|
||||||
height = metrics.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
// autogrow container height if text exceeds
|
// autogrow container height if text exceeds
|
||||||
if (!isArrowElement(container) && height > maxHeight) {
|
if (!isArrowElement(container) && height > maxHeight) {
|
||||||
const targetContainerHeight = computeContainerDimensionForBoundText(
|
const targetContainerHeight = computeContainerDimensionForBoundText(
|
||||||
|
@ -578,7 +533,7 @@ export const textWysiwyg = ({
|
||||||
// it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the
|
// it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the
|
||||||
// wysiwyg on update
|
// wysiwyg on update
|
||||||
cleanup();
|
cleanup();
|
||||||
const updateElement = Scene.getScene(element)?.getElement(
|
const updateElement = app.scene.getElement(
|
||||||
element.id,
|
element.id,
|
||||||
) as ExcalidrawTextElement;
|
) as ExcalidrawTextElement;
|
||||||
if (!updateElement) {
|
if (!updateElement) {
|
||||||
|
|
|
@ -30,6 +30,8 @@ import type {
|
||||||
ExcalidrawRectangleElement,
|
ExcalidrawRectangleElement,
|
||||||
ExcalidrawEllipseElement,
|
ExcalidrawEllipseElement,
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
|
GenericSwitchableToolType,
|
||||||
|
LinearSwitchableToolType,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const isInitializedImageElement = (
|
export const isInitializedImageElement = (
|
||||||
|
@ -341,22 +343,25 @@ export const isBounds = (box: unknown): box is Bounds =>
|
||||||
typeof box[2] === "number" &&
|
typeof box[2] === "number" &&
|
||||||
typeof box[3] === "number";
|
typeof box[3] === "number";
|
||||||
|
|
||||||
|
type NonEmptyArray<T> = [T, ...T[]];
|
||||||
|
|
||||||
type ExcalidrawGenericSwitchableElement =
|
type ExcalidrawGenericSwitchableElement =
|
||||||
| ExcalidrawRectangleElement
|
| ExcalidrawRectangleElement
|
||||||
| ExcalidrawEllipseElement
|
| ExcalidrawEllipseElement
|
||||||
| ExcalidrawDiamondElement;
|
| ExcalidrawDiamondElement;
|
||||||
|
|
||||||
type GenericSwitchableToolType = "rectangle" | "ellipse" | "diamond";
|
|
||||||
|
|
||||||
type LinearSwitchableToolType = "arrow" | "line";
|
|
||||||
|
|
||||||
export const isGenericSwitchableElement = (
|
export const isGenericSwitchableElement = (
|
||||||
element: ExcalidrawElement,
|
elements: ExcalidrawElement[],
|
||||||
): element is ExcalidrawGenericSwitchableElement => {
|
): elements is NonEmptyArray<ExcalidrawGenericSwitchableElement> => {
|
||||||
|
if (elements.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const firstType = elements[0].type;
|
||||||
return (
|
return (
|
||||||
element.type === "rectangle" ||
|
(firstType === "rectangle" ||
|
||||||
element.type === "ellipse" ||
|
firstType === "ellipse" ||
|
||||||
element.type === "diamond"
|
firstType === "diamond") &&
|
||||||
|
elements.every((element) => element.type === firstType)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -367,18 +372,16 @@ export const isGenericSwitchableToolType = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isLinearSwitchableElement = (
|
export const isLinearSwitchableElement = (
|
||||||
element: ExcalidrawElement,
|
elements: ExcalidrawElement[],
|
||||||
): element is ExcalidrawLinearElement => {
|
): elements is NonEmptyArray<ExcalidrawLinearElement> => {
|
||||||
if (element.type === "arrow" || element.type === "line") {
|
if (elements.length === 0) {
|
||||||
if (
|
|
||||||
(!element.boundElements || element.boundElements.length === 0) &&
|
|
||||||
!element.startBinding &&
|
|
||||||
!element.endBinding
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
const firstType = elements[0].type;
|
||||||
|
return (
|
||||||
|
(firstType === "arrow" || firstType === "line") &&
|
||||||
|
elements.every((element) => element.type === firstType)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isLinearSwitchableToolType = (
|
export const isLinearSwitchableToolType = (
|
||||||
|
|
|
@ -411,3 +411,6 @@ export type NonDeletedSceneElementsMap = Map<
|
||||||
export type ElementsMapOrArray =
|
export type ElementsMapOrArray =
|
||||||
| readonly ExcalidrawElement[]
|
| readonly ExcalidrawElement[]
|
||||||
| Readonly<ElementsMap>;
|
| Readonly<ElementsMap>;
|
||||||
|
|
||||||
|
export type GenericSwitchableToolType = "rectangle" | "ellipse" | "diamond";
|
||||||
|
export type LinearSwitchableToolType = "line" | "arrow";
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue