mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: switch between basic shapes
This commit is contained in:
parent
70c3e921bb
commit
407d8ababb
9 changed files with 545 additions and 5 deletions
|
@ -120,6 +120,7 @@ export const getDefaultAppState = (): Omit<
|
|||
isCropping: false,
|
||||
croppingElementId: null,
|
||||
searchMatches: [],
|
||||
showShapeSwitchPanel: false,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -244,6 +245,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||
isCropping: { browser: false, export: false, server: false },
|
||||
croppingElementId: { browser: false, export: false, server: false },
|
||||
searchMatches: { browser: false, export: false, server: false },
|
||||
showShapeSwitchPanel: { browser: false, export: false, server: false },
|
||||
});
|
||||
|
||||
const _clearAppStateForStorage = <
|
||||
|
|
|
@ -166,6 +166,10 @@ import {
|
|||
isElbowArrow,
|
||||
isFlowchartNodeElement,
|
||||
isBindableElement,
|
||||
isGenericSwitchableElement,
|
||||
isGenericSwitchableToolType,
|
||||
isLinearSwitchableElement,
|
||||
isLinearSwitchableToolType,
|
||||
} from "../element/typeChecks";
|
||||
import type {
|
||||
ExcalidrawBindableElement,
|
||||
|
@ -467,6 +471,7 @@ import {
|
|||
getApproxMinLineHeight,
|
||||
getMinTextElementWidth,
|
||||
} from "../element/textMeasurements";
|
||||
import ShapeSwitch from "./ShapeSwitch";
|
||||
|
||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||
|
@ -559,6 +564,7 @@ const gesture: Gesture = {
|
|||
initialDistance: null,
|
||||
initialScale: null,
|
||||
};
|
||||
let textWysiwygSubmitHandler: (() => void) | null = null;
|
||||
|
||||
class App extends React.Component<AppProps, AppState> {
|
||||
canvas: AppClassProperties["canvas"];
|
||||
|
@ -1805,6 +1811,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
/>
|
||||
)}
|
||||
{this.renderFrameNames()}
|
||||
<ShapeSwitch app={this} />
|
||||
</ExcalidrawActionManagerContext.Provider>
|
||||
{this.renderEmbeddables()}
|
||||
</ExcalidrawElementsContext.Provider>
|
||||
|
@ -4092,6 +4099,56 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
selectedElements.length === 1 &&
|
||||
(selectedElements[0].type === "rectangle" ||
|
||||
selectedElements[0].type === "diamond" ||
|
||||
selectedElements[0].type === "ellipse" ||
|
||||
selectedElements[0].type === "arrow" ||
|
||||
selectedElements[0].type === "line")
|
||||
) {
|
||||
if (this.state.showShapeSwitchPanel && event.key === KEYS.ESCAPE) {
|
||||
this.setState({
|
||||
showShapeSwitchPanel: false,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === KEYS.SLASH) {
|
||||
if (!this.state.showShapeSwitchPanel) {
|
||||
this.setState({
|
||||
showShapeSwitchPanel: true,
|
||||
});
|
||||
} else if (
|
||||
selectedElements[0].type === "rectangle" ||
|
||||
selectedElements[0].type === "diamond" ||
|
||||
selectedElements[0].type === "ellipse"
|
||||
) {
|
||||
const index = ["rectangle", "diamond", "ellipse"].indexOf(
|
||||
selectedElements[0].type,
|
||||
);
|
||||
const nextType = ["rectangle", "diamond", "ellipse"][
|
||||
(index + 1) % 3
|
||||
] as ToolType;
|
||||
this.setActiveTool({ type: nextType });
|
||||
} else if (
|
||||
selectedElements[0].type === "arrow" ||
|
||||
selectedElements[0].type === "line"
|
||||
) {
|
||||
const index = ["arrow", "line"].indexOf(selectedElements[0].type);
|
||||
const nextType = ["arrow", "line"][(index + 1) % 2] as ToolType;
|
||||
this.setActiveTool({ type: nextType });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
showShapeSwitchPanel: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
event.key === KEYS.ESCAPE &&
|
||||
this.flowChartCreator.isCreatingChart
|
||||
|
@ -4742,6 +4799,95 @@ class App extends React.Component<AppProps, AppState> {
|
|||
...commonResets,
|
||||
};
|
||||
});
|
||||
|
||||
const selectedElements = getSelectedElements(
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
this.state,
|
||||
);
|
||||
const firstElement = selectedElements[0];
|
||||
|
||||
if (
|
||||
firstElement &&
|
||||
selectedElements.length === 1 &&
|
||||
isGenericSwitchableElement(firstElement) &&
|
||||
isGenericSwitchableToolType(tool.type)
|
||||
) {
|
||||
ShapeCache.delete(firstElement);
|
||||
|
||||
mutateElement(firstElement, {
|
||||
type: tool.type,
|
||||
roundness:
|
||||
tool.type === "diamond" && firstElement.roundness
|
||||
? {
|
||||
type: isUsingAdaptiveRadius(tool.type)
|
||||
? ROUNDNESS.ADAPTIVE_RADIUS
|
||||
: ROUNDNESS.PROPORTIONAL_RADIUS,
|
||||
value: ROUNDNESS.PROPORTIONAL_RADIUS,
|
||||
}
|
||||
: firstElement.roundness,
|
||||
});
|
||||
|
||||
this.setActiveTool({ type: "selection" });
|
||||
|
||||
if (firstElement.boundElements?.some((e) => e.type === "text")) {
|
||||
this.startTextEditing({
|
||||
sceneX: firstElement.x + firstElement.width / 2,
|
||||
sceneY: firstElement.y + firstElement.height / 2,
|
||||
container: firstElement as ExcalidrawTextContainer,
|
||||
keepContainerDimensions: true,
|
||||
});
|
||||
}
|
||||
|
||||
textWysiwygSubmitHandler?.();
|
||||
|
||||
this.setState({
|
||||
selectedElementIds: {
|
||||
[firstElement.id]: true,
|
||||
},
|
||||
});
|
||||
|
||||
this.store.shouldCaptureIncrement();
|
||||
}
|
||||
|
||||
if (
|
||||
firstElement &&
|
||||
selectedElements.length === 1 &&
|
||||
isLinearSwitchableElement(firstElement) &&
|
||||
isLinearSwitchableToolType(tool.type)
|
||||
) {
|
||||
ShapeCache.delete(firstElement);
|
||||
|
||||
mutateElement(firstElement as ExcalidrawLinearElement, {
|
||||
type: tool.type,
|
||||
startArrowhead: null,
|
||||
endArrowhead: tool.type === "arrow" ? "arrow" : null,
|
||||
});
|
||||
|
||||
this.setActiveTool({ type: "selection" });
|
||||
|
||||
if (
|
||||
firstElement.boundElements?.some((e) => e.type === "text") &&
|
||||
isArrowElement(firstElement)
|
||||
) {
|
||||
this.startTextEditing({
|
||||
sceneX: firstElement.x + firstElement.width / 2,
|
||||
sceneY: firstElement.y + firstElement.height / 2,
|
||||
container: firstElement,
|
||||
keepContainerDimensions: true,
|
||||
});
|
||||
}
|
||||
|
||||
textWysiwygSubmitHandler?.();
|
||||
|
||||
this.setState({
|
||||
selectedElementIds: {
|
||||
[firstElement.id]: true,
|
||||
},
|
||||
selectedLinearElement: new LinearElementEditor(
|
||||
firstElement as ExcalidrawLinearElement,
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
setOpenDialog = (dialogType: AppState["openDialog"]) => {
|
||||
|
@ -4843,8 +4989,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
element: ExcalidrawTextElement,
|
||||
{
|
||||
isExistingElement = false,
|
||||
keepContainerDimensions = false,
|
||||
}: {
|
||||
isExistingElement?: boolean;
|
||||
keepContainerDimensions?: boolean;
|
||||
},
|
||||
) {
|
||||
const elementsMap = this.scene.getElementsMapIncludingDeleted();
|
||||
|
@ -4871,7 +5019,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
]);
|
||||
};
|
||||
|
||||
textWysiwyg({
|
||||
textWysiwygSubmitHandler = textWysiwyg({
|
||||
id: element.id,
|
||||
canvas: this.canvas,
|
||||
getViewportCoords: (x, y) => {
|
||||
|
@ -4894,6 +5042,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
}),
|
||||
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
|
||||
textWysiwygSubmitHandler = null;
|
||||
const isDeleted = !nextOriginalText.trim();
|
||||
updateElement(nextOriginalText, isDeleted);
|
||||
// select the created text element only if submitting via keyboard
|
||||
|
@ -4949,6 +5098,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
// the text on edit anyway (and users can select-all from contextmenu
|
||||
// if needed)
|
||||
autoSelect: !this.device.isTouchScreen,
|
||||
keepContainerDimensions,
|
||||
});
|
||||
// deselect all other elements when inserting text
|
||||
this.deselectElements();
|
||||
|
@ -5181,6 +5331,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
insertAtParentCenter = true,
|
||||
container,
|
||||
autoEdit = true,
|
||||
keepContainerDimensions = false,
|
||||
}: {
|
||||
/** X position to insert text at */
|
||||
sceneX: number;
|
||||
|
@ -5190,6 +5341,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
insertAtParentCenter?: boolean;
|
||||
container?: ExcalidrawTextContainer | null;
|
||||
autoEdit?: boolean;
|
||||
keepContainerDimensions?: boolean;
|
||||
}) => {
|
||||
let shouldBindToContainer = false;
|
||||
|
||||
|
@ -5325,6 +5477,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
if (autoEdit || existingTextElement || container) {
|
||||
this.handleTextWysiwyg(element, {
|
||||
isExistingElement: !!existingTextElement,
|
||||
keepContainerDimensions,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
|
@ -8768,6 +8921,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
cursorButton: "up",
|
||||
snapLines: updateStable(prevState.snapLines, []),
|
||||
originSnapOffset: null,
|
||||
showShapeSwitchPanel: false,
|
||||
}));
|
||||
|
||||
this.lastPointerMoveCoords = null;
|
||||
|
|
47
packages/excalidraw/components/ShapeSwitch.scss
Normal file
47
packages/excalidraw/components/ShapeSwitch.scss
Normal file
|
@ -0,0 +1,47 @@
|
|||
@import "../css//variables.module.scss";
|
||||
|
||||
@keyframes disappear {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.excalidraw {
|
||||
.ShapeSwitch__Hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
background: var(--default-bg-color);
|
||||
|
||||
.key {
|
||||
background: var(--color-primary-light);
|
||||
padding: 0.4rem 0.8rem;
|
||||
font-weight: bold;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.animation {
|
||||
opacity: 1;
|
||||
animation: disappear 2s ease-out;
|
||||
}
|
||||
|
||||
.ShapeSwitch__Panel {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.2rem;
|
||||
border-radius: 0.5rem;
|
||||
background: var(--default-bg-color);
|
||||
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
199
packages/excalidraw/components/ShapeSwitch.tsx
Normal file
199
packages/excalidraw/components/ShapeSwitch.tsx
Normal file
|
@ -0,0 +1,199 @@
|
|||
import { getSelectedElements } from "../scene";
|
||||
import type App from "./App";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import {
|
||||
ArrowIcon,
|
||||
DiamondIcon,
|
||||
EllipseIcon,
|
||||
LineIcon,
|
||||
RectangleIcon,
|
||||
} from "./icons";
|
||||
import { type ReactNode, useEffect, useRef } from "react";
|
||||
import { trackEvent } from "../analytics";
|
||||
import type { ToolType } from "../types";
|
||||
import { sceneCoordsToViewportCoords } from "../utils";
|
||||
import type { ExcalidrawElement } from "../element/types";
|
||||
import "./ShapeSwitch.scss";
|
||||
import clsx from "clsx";
|
||||
import { getElementAbsoluteCoords } from "../element";
|
||||
import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
||||
import {
|
||||
isArrowElement,
|
||||
isGenericSwitchableElement,
|
||||
isLinearElement,
|
||||
isLinearSwitchableElement,
|
||||
} from "../element/typeChecks";
|
||||
import { t } from "../i18n";
|
||||
|
||||
const GAP_HORIZONTAL = 8;
|
||||
const GAP_VERTICAL = 10;
|
||||
|
||||
const ShapeSwitch = ({ app }: { app: App }) => {
|
||||
const selectedElements = getSelectedElements(
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
app.state,
|
||||
);
|
||||
const firstElement = selectedElements[0];
|
||||
|
||||
useEffect(() => {
|
||||
return app.setState({ showShapeSwitchPanel: false });
|
||||
}, [app]);
|
||||
|
||||
if (
|
||||
firstElement &&
|
||||
selectedElements.length === 1 &&
|
||||
(isGenericSwitchableElement(firstElement) ||
|
||||
isLinearSwitchableElement(firstElement))
|
||||
) {
|
||||
return app.state.showShapeSwitchPanel ? (
|
||||
<Panel app={app} element={firstElement} />
|
||||
) : (
|
||||
<Hint app={app} element={firstElement} />
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const Hint = ({ app, element }: { app: App; element: ExcalidrawElement }) => {
|
||||
const [x1, y1, , , cx, cy] = getElementAbsoluteCoords(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
const rotatedTopLeft = pointRotateRads(
|
||||
pointFrom(x1, y1),
|
||||
pointFrom(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
|
||||
const { x, y } = sceneCoordsToViewportCoords(
|
||||
{
|
||||
sceneX: rotatedTopLeft[0],
|
||||
sceneY: rotatedTopLeft[1],
|
||||
},
|
||||
app.state,
|
||||
);
|
||||
|
||||
const hintRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = () => {
|
||||
hintRef.current?.classList.remove("animation");
|
||||
};
|
||||
|
||||
if (hintRef.current) {
|
||||
hintRef.current.addEventListener("animationend", listener);
|
||||
}
|
||||
}, [element.id]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={hintRef}
|
||||
key={element.id}
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: `${
|
||||
app.state.height +
|
||||
GAP_VERTICAL * app.state.zoom.value -
|
||||
y -
|
||||
app.state.offsetTop
|
||||
}px`,
|
||||
left: `${x - app.state.offsetLeft - GAP_HORIZONTAL}px`,
|
||||
zIndex: 2,
|
||||
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
}}
|
||||
className={clsx("ShapeSwitch__Hint", "animation")}
|
||||
>
|
||||
<div className="key">/</div>
|
||||
<div className="text">{t("labels.slash")}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Panel = ({ app, element }: { app: App; element: ExcalidrawElement }) => {
|
||||
const [x1, , , y2, cx, cy] = getElementAbsoluteCoords(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
const rotatedBottomLeft = pointRotateRads(
|
||||
pointFrom(x1, y2),
|
||||
pointFrom(cx, cy),
|
||||
element.angle,
|
||||
);
|
||||
|
||||
const { x, y } = sceneCoordsToViewportCoords(
|
||||
{
|
||||
sceneX: rotatedBottomLeft[0],
|
||||
sceneY: rotatedBottomLeft[1],
|
||||
},
|
||||
app.state,
|
||||
);
|
||||
|
||||
const SHAPES: [string, string, ReactNode][] = isLinearElement(element)
|
||||
? [
|
||||
["arrow", "5", ArrowIcon],
|
||||
["line", "6", LineIcon],
|
||||
]
|
||||
: [
|
||||
["rectangle", "2", RectangleIcon],
|
||||
["diamond", "3", DiamondIcon],
|
||||
["ellipse", "4", EllipseIcon],
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: `${
|
||||
y + GAP_VERTICAL * app.state.zoom.value - app.state.offsetTop
|
||||
}px`,
|
||||
left: `${x - app.state.offsetLeft - GAP_HORIZONTAL}px`,
|
||||
zIndex: 2,
|
||||
}}
|
||||
className="ShapeSwitch__Panel"
|
||||
>
|
||||
{SHAPES.map(([type, shortcut, icon]) => {
|
||||
const isSelected =
|
||||
type === element.type ||
|
||||
(isArrowElement(element) && element.elbowed && type === "elbow") ||
|
||||
(isArrowElement(element) && element.roundness && type === "curve") ||
|
||||
(isArrowElement(element) &&
|
||||
!element.elbowed &&
|
||||
!element.roundness &&
|
||||
type === "straight");
|
||||
|
||||
return (
|
||||
<ToolButton
|
||||
className="Shape"
|
||||
key={type}
|
||||
type="radio"
|
||||
icon={icon}
|
||||
checked={isSelected}
|
||||
name="editor-current-shape"
|
||||
title={type}
|
||||
keyBindingLabel={""}
|
||||
aria-label={type}
|
||||
data-testid={`toolbar-${type}`}
|
||||
onPointerDown={({ pointerType }) => {
|
||||
if (!app.state.penDetected && pointerType === "pen") {
|
||||
app.togglePenMode(true);
|
||||
}
|
||||
}}
|
||||
onChange={() => {
|
||||
if (app.state.activeTool.type !== type) {
|
||||
trackEvent("shape-switch", type, "ui");
|
||||
}
|
||||
app.setActiveTool({ type: type as ToolType });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShapeSwitch;
|
39
packages/excalidraw/components/ShapeSwitcher.tsx
Normal file
39
packages/excalidraw/components/ShapeSwitcher.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
// render the shape switcher div on top of the canvas at the selected element's position
|
||||
|
||||
import { THEME } from "../constants";
|
||||
import type { ExcalidrawElement } from "../element/types";
|
||||
import type { AppState } from "../types";
|
||||
import { sceneCoordsToViewportCoords } from "../utils";
|
||||
|
||||
const ShapeSwitcher = ({
|
||||
appState,
|
||||
element,
|
||||
}: {
|
||||
appState: AppState;
|
||||
element: ExcalidrawElement;
|
||||
elements: readonly ExcalidrawElement[];
|
||||
}) => {
|
||||
const isDarkTheme = appState.theme === THEME.DARK;
|
||||
const { x, y } = sceneCoordsToViewportCoords(
|
||||
{
|
||||
sceneX: element.x,
|
||||
sceneY: element.y,
|
||||
},
|
||||
appState,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: `${appState.height - y + appState.offsetTop}px`,
|
||||
left: `${x - appState.offsetLeft}px`,
|
||||
backgroundColor: isDarkTheme
|
||||
? "rgba(0, 0, 0, 0.9)"
|
||||
: "rgba(255, 255, 255, 0.9)",
|
||||
}}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShapeSwitcher;
|
|
@ -48,7 +48,7 @@ import {
|
|||
originalContainerCache,
|
||||
updateOriginalContainerCache,
|
||||
} from "./containerCache";
|
||||
import { getTextWidth } from "./textMeasurements";
|
||||
import { getTextWidth, measureText } from "./textMeasurements";
|
||||
import { normalizeText } from "./textMeasurements";
|
||||
|
||||
const getTransform = (
|
||||
|
@ -72,6 +72,8 @@ const getTransform = (
|
|||
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
||||
};
|
||||
|
||||
type SubmitHandler = () => void;
|
||||
|
||||
export const textWysiwyg = ({
|
||||
id,
|
||||
onChange,
|
||||
|
@ -82,6 +84,7 @@ export const textWysiwyg = ({
|
|||
excalidrawContainer,
|
||||
app,
|
||||
autoSelect = true,
|
||||
keepContainerDimensions = false,
|
||||
}: {
|
||||
id: ExcalidrawElement["id"];
|
||||
/**
|
||||
|
@ -98,7 +101,8 @@ export const textWysiwyg = ({
|
|||
excalidrawContainer: HTMLDivElement | null;
|
||||
app: App;
|
||||
autoSelect?: boolean;
|
||||
}) => {
|
||||
keepContainerDimensions?: boolean;
|
||||
}): SubmitHandler => {
|
||||
const textPropertiesUpdated = (
|
||||
updatedTextElement: ExcalidrawTextElement,
|
||||
editable: HTMLTextAreaElement,
|
||||
|
@ -179,12 +183,53 @@ export const textWysiwyg = ({
|
|||
}
|
||||
|
||||
maxWidth = getBoundTextMaxWidth(container, updatedTextElement);
|
||||
|
||||
maxHeight = getBoundTextMaxHeight(
|
||||
container,
|
||||
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
|
||||
if (!isArrowElement(container) && height > maxHeight) {
|
||||
const targetContainerHeight = computeContainerDimensionForBoundText(
|
||||
|
@ -727,4 +772,6 @@ export const textWysiwyg = ({
|
|||
excalidrawContainer
|
||||
?.querySelector(".excalidraw-textEditorContainer")!
|
||||
.appendChild(editable);
|
||||
|
||||
return handleSubmit;
|
||||
};
|
||||
|
|
|
@ -26,6 +26,9 @@ import type {
|
|||
PointBinding,
|
||||
FixedPointBinding,
|
||||
ExcalidrawFlowchartNodeElement,
|
||||
ExcalidrawRectangleElement,
|
||||
ExcalidrawEllipseElement,
|
||||
ExcalidrawDiamondElement,
|
||||
} from "./types";
|
||||
|
||||
export const isInitializedImageElement = (
|
||||
|
@ -336,3 +339,49 @@ export const isBounds = (box: unknown): box is Bounds =>
|
|||
typeof box[1] === "number" &&
|
||||
typeof box[2] === "number" &&
|
||||
typeof box[3] === "number";
|
||||
|
||||
type ExcalidrawGenericSwitchableElement =
|
||||
| ExcalidrawRectangleElement
|
||||
| ExcalidrawEllipseElement
|
||||
| ExcalidrawDiamondElement;
|
||||
|
||||
type GenericSwitchableToolType = "rectangle" | "ellipse" | "diamond";
|
||||
|
||||
type LinearSwitchableToolType = "arrow" | "line";
|
||||
|
||||
export const isGenericSwitchableElement = (
|
||||
element: ExcalidrawElement,
|
||||
): element is ExcalidrawGenericSwitchableElement => {
|
||||
return (
|
||||
element.type === "rectangle" ||
|
||||
element.type === "ellipse" ||
|
||||
element.type === "diamond"
|
||||
);
|
||||
};
|
||||
|
||||
export const isGenericSwitchableToolType = (
|
||||
type: string,
|
||||
): type is GenericSwitchableToolType => {
|
||||
return type === "rectangle" || type === "ellipse" || type === "diamond";
|
||||
};
|
||||
|
||||
export const isLinearSwitchableElement = (
|
||||
element: ExcalidrawElement,
|
||||
): element is ExcalidrawLinearElement => {
|
||||
if (element.type === "arrow" || element.type === "line") {
|
||||
if (
|
||||
(!element.boundElements || element.boundElements.length === 0) &&
|
||||
!element.startBinding &&
|
||||
!element.endBinding
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isLinearSwitchableToolType = (
|
||||
type: string,
|
||||
): type is LinearSwitchableToolType => {
|
||||
return type === "arrow" || type === "line";
|
||||
};
|
||||
|
|
|
@ -165,7 +165,8 @@
|
|||
"unCroppedDimension": "Uncropped dimension",
|
||||
"copyElementLink": "Copy link to object",
|
||||
"linkToElement": "Link to object",
|
||||
"wrapSelectionInFrame": "Wrap selection in frame"
|
||||
"wrapSelectionInFrame": "Wrap selection in frame",
|
||||
"slash": "Slash"
|
||||
},
|
||||
"elementLink": {
|
||||
"title": "Link to object",
|
||||
|
|
|
@ -410,6 +410,8 @@ export interface AppState {
|
|||
croppingElementId: ExcalidrawElement["id"] | null;
|
||||
|
||||
searchMatches: readonly SearchMatch[];
|
||||
|
||||
showShapeSwitchPanel: boolean;
|
||||
}
|
||||
|
||||
type SearchMatch = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue