mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Merge branch 'master' into mtolmacs/fix/small-elbow-routing
This commit is contained in:
commit
8d7ffa21d1
28 changed files with 1286 additions and 62 deletions
|
@ -32,6 +32,12 @@
|
||||||
"name": "jotai",
|
"name": "jotai",
|
||||||
"message": "Do not import from \"jotai\" directly. Use our app-specific modules (\"editor-jotai\" or \"app-jotai\")."
|
"message": "Do not import from \"jotai\" directly. Use our app-specific modules (\"editor-jotai\" or \"app-jotai\")."
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"react/jsx-no-target-blank": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowReferrer": true
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ export const AIComponents = ({
|
||||||
</br>
|
</br>
|
||||||
<div>You can also try <a href="${
|
<div>You can also try <a href="${
|
||||||
import.meta.env.VITE_APP_PLUS_LP
|
import.meta.env.VITE_APP_PLUS_LP
|
||||||
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=d2c" target="_blank" rel="noreferrer noopener">Excalidraw+</a> to get more requests.</div>
|
}/plus?utm_source=excalidraw&utm_medium=app&utm_content=d2c" target="_blank" rel="noopener">Excalidraw+</a> to get more requests.</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>`,
|
</html>`,
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const EncryptedIcon = () => {
|
||||||
className="encrypted-icon tooltip"
|
className="encrypted-icon tooltip"
|
||||||
href="https://plus.excalidraw.com/blog/end-to-end-encryption"
|
href="https://plus.excalidraw.com/blog/end-to-end-encryption"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener"
|
||||||
aria-label={t("encrypted.link")}
|
aria-label={t("encrypted.link")}
|
||||||
>
|
>
|
||||||
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const ExcalidrawPlusAppLink = () => {
|
||||||
import.meta.env.VITE_APP_PLUS_APP
|
import.meta.env.VITE_APP_PLUS_APP
|
||||||
}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
|
}?utm_source=excalidraw&utm_medium=app&utm_content=signedInUserRedirectButton#excalidraw-redirect`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
className="plus-button"
|
className="plus-button"
|
||||||
>
|
>
|
||||||
Go to Excalidraw+
|
Go to Excalidraw+
|
||||||
|
|
|
@ -198,7 +198,7 @@ exports[`Test MobileMenu > should initialize with welcome screen and hide once u
|
||||||
<a
|
<a
|
||||||
class="welcome-screen-menu-item "
|
class="welcome-screen-menu-item "
|
||||||
href="undefined/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
href="undefined/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -119,6 +119,7 @@ export const CLASSES = {
|
||||||
SHAPE_ACTIONS_MENU: "App-menu__left",
|
SHAPE_ACTIONS_MENU: "App-menu__left",
|
||||||
ZOOM_ACTIONS: "zoom-actions",
|
ZOOM_ACTIONS: "zoom-actions",
|
||||||
SEARCH_MENU_INPUT_WRAPPER: "layer-ui__search-inputWrapper",
|
SEARCH_MENU_INPUT_WRAPPER: "layer-ui__search-inputWrapper",
|
||||||
|
CONVERT_ELEMENT_TYPE_POPUP: "ConvertElementTypePopup",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CJK_HAND_DRAWN_FALLBACK_FONT = "Xiaolai";
|
export const CJK_HAND_DRAWN_FALLBACK_FONT = "Xiaolai";
|
||||||
|
|
|
@ -87,7 +87,6 @@ import type {
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
OrderedExcalidrawElement,
|
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
FixedPoint,
|
FixedPoint,
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
|
@ -716,29 +715,32 @@ const calculateFocusAndGap = (
|
||||||
|
|
||||||
// Supports translating, rotating and scaling `changedElement` with bound
|
// Supports translating, rotating and scaling `changedElement` with bound
|
||||||
// linear elements.
|
// linear elements.
|
||||||
// Because scaling involves moving the focus points as well, it is
|
|
||||||
// done before the `changedElement` is updated, and the `newSize` is passed
|
|
||||||
// in explicitly.
|
|
||||||
export const updateBoundElements = (
|
export const updateBoundElements = (
|
||||||
changedElement: NonDeletedExcalidrawElement,
|
changedElement: NonDeletedExcalidrawElement,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
options?: {
|
options?: {
|
||||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||||
newSize?: { width: number; height: number };
|
newSize?: { width: number; height: number };
|
||||||
changedElements?: Map<string, OrderedExcalidrawElement>;
|
changedElements?: Map<string, ExcalidrawElement>;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
|
if (!isBindableElement(changedElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { newSize, simultaneouslyUpdated } = options ?? {};
|
const { newSize, simultaneouslyUpdated } = options ?? {};
|
||||||
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
||||||
simultaneouslyUpdated,
|
simultaneouslyUpdated,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isBindableElement(changedElement)) {
|
let elementsMap: ElementsMap = scene.getNonDeletedElementsMap();
|
||||||
return;
|
if (options?.changedElements) {
|
||||||
|
elementsMap = new Map(elementsMap) as typeof elementsMap;
|
||||||
|
options.changedElements.forEach((element) => {
|
||||||
|
elementsMap.set(element.id, element);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
|
||||||
|
|
||||||
boundElementsVisitor(elementsMap, changedElement, (element) => {
|
boundElementsVisitor(elementsMap, changedElement, (element) => {
|
||||||
if (!isLinearElement(element) || element.isDeleted) {
|
if (!isLinearElement(element) || element.isDeleted) {
|
||||||
return;
|
return;
|
||||||
|
@ -842,6 +844,25 @@ export const updateBoundElements = (
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateBindings = (
|
||||||
|
latestElement: ExcalidrawElement,
|
||||||
|
scene: Scene,
|
||||||
|
options?: {
|
||||||
|
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||||
|
newSize?: { width: number; height: number };
|
||||||
|
zoom?: AppState["zoom"];
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
if (isLinearElement(latestElement)) {
|
||||||
|
bindOrUnbindLinearElements([latestElement], true, [], scene, options?.zoom);
|
||||||
|
} else {
|
||||||
|
updateBoundElements(latestElement, scene, {
|
||||||
|
...options,
|
||||||
|
changedElements: new Map([[latestElement.id, latestElement]]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const doesNeedUpdate = (
|
const doesNeedUpdate = (
|
||||||
boundElement: NonDeleted<ExcalidrawLinearElement>,
|
boundElement: NonDeleted<ExcalidrawLinearElement>,
|
||||||
changedElement: ExcalidrawBindableElement,
|
changedElement: ExcalidrawBindableElement,
|
||||||
|
|
|
@ -44,7 +44,6 @@ import type {
|
||||||
ExcalidrawIframeElement,
|
ExcalidrawIframeElement,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
FixedSegment,
|
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
@ -478,7 +477,7 @@ export const newArrowElement = <T extends boolean>(
|
||||||
endArrowhead?: Arrowhead | null;
|
endArrowhead?: Arrowhead | null;
|
||||||
points?: ExcalidrawArrowElement["points"];
|
points?: ExcalidrawArrowElement["points"];
|
||||||
elbowed?: T;
|
elbowed?: T;
|
||||||
fixedSegments?: FixedSegment[] | null;
|
fixedSegments?: ExcalidrawElbowArrowElement["fixedSegments"] | null;
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): T extends true
|
): T extends true
|
||||||
? NonDeleted<ExcalidrawElbowArrowElement>
|
? NonDeleted<ExcalidrawElbowArrowElement>
|
||||||
|
|
|
@ -119,6 +119,20 @@ export const isElbowArrow = (
|
||||||
return isArrowElement(element) && element.elbowed;
|
return isArrowElement(element) && element.elbowed;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isSharpArrow = (
|
||||||
|
element?: ExcalidrawElement,
|
||||||
|
): element is ExcalidrawArrowElement => {
|
||||||
|
return isArrowElement(element) && !element.elbowed && !element.roundness;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isCurvedArrow = (
|
||||||
|
element?: ExcalidrawElement,
|
||||||
|
): element is ExcalidrawArrowElement => {
|
||||||
|
return (
|
||||||
|
isArrowElement(element) && !element.elbowed && element.roundness !== null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const isLinearElementType = (
|
export const isLinearElementType = (
|
||||||
elementType: ElementOrToolType,
|
elementType: ElementOrToolType,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
|
@ -271,6 +285,10 @@ export const isBoundToContainer = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isArrowBoundToElement = (element: ExcalidrawArrowElement) => {
|
||||||
|
return !!element.startBinding || !!element.endBinding;
|
||||||
|
};
|
||||||
|
|
||||||
export const isUsingAdaptiveRadius = (type: string) =>
|
export const isUsingAdaptiveRadius = (type: string) =>
|
||||||
type === "rectangle" ||
|
type === "rectangle" ||
|
||||||
type === "embeddable" ||
|
type === "embeddable" ||
|
||||||
|
|
|
@ -412,3 +412,11 @@ export type NonDeletedSceneElementsMap = Map<
|
||||||
export type ElementsMapOrArray =
|
export type ElementsMapOrArray =
|
||||||
| readonly ExcalidrawElement[]
|
| readonly ExcalidrawElement[]
|
||||||
| Readonly<ElementsMap>;
|
| Readonly<ElementsMap>;
|
||||||
|
|
||||||
|
export type ConvertibleGenericTypes = "rectangle" | "diamond" | "ellipse";
|
||||||
|
export type ConvertibleLinearTypes =
|
||||||
|
| "line"
|
||||||
|
| "sharpArrow"
|
||||||
|
| "curvedArrow"
|
||||||
|
| "elbowArrow";
|
||||||
|
export type ConvertibleTypes = ConvertibleGenericTypes | ConvertibleLinearTypes;
|
||||||
|
|
34
packages/excalidraw/actions/actionToggleShapeSwitch.tsx
Normal file
34
packages/excalidraw/actions/actionToggleShapeSwitch.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getConversionTypeFromElements,
|
||||||
|
convertElementTypePopupAtom,
|
||||||
|
} from "../components/ConvertElementTypePopup";
|
||||||
|
import { editorJotaiStore } from "../editor-jotai";
|
||||||
|
import { CaptureUpdateAction } from "../store";
|
||||||
|
|
||||||
|
import { register } from "./register";
|
||||||
|
|
||||||
|
export const actionToggleShapeSwitch = register({
|
||||||
|
name: "toggleShapeSwitch",
|
||||||
|
label: "labels.shapeSwitch",
|
||||||
|
icon: () => null,
|
||||||
|
viewMode: true,
|
||||||
|
trackEvent: {
|
||||||
|
category: "shape_switch",
|
||||||
|
action: "toggle",
|
||||||
|
},
|
||||||
|
keywords: ["change", "switch", "swap"],
|
||||||
|
perform(elements, appState, _, app) {
|
||||||
|
editorJotaiStore.set(convertElementTypePopupAtom, {
|
||||||
|
type: "panel",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
captureUpdate: CaptureUpdateAction.NEVER,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
checked: (appState) => appState.gridModeEnabled,
|
||||||
|
predicate: (elements, appState, props) =>
|
||||||
|
getConversionTypeFromElements(elements as ExcalidrawElement[]) !== null,
|
||||||
|
});
|
|
@ -140,7 +140,8 @@ export type ActionName =
|
||||||
| "linkToElement"
|
| "linkToElement"
|
||||||
| "cropEditor"
|
| "cropEditor"
|
||||||
| "wrapSelectionInFrame"
|
| "wrapSelectionInFrame"
|
||||||
| "toggleLassoTool";
|
| "toggleLassoTool"
|
||||||
|
| "toggleShapeSwitch";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
|
@ -195,7 +196,8 @@ export interface Action {
|
||||||
| "menu"
|
| "menu"
|
||||||
| "collab"
|
| "collab"
|
||||||
| "hyperlink"
|
| "hyperlink"
|
||||||
| "search_menu";
|
| "search_menu"
|
||||||
|
| "shape_switch";
|
||||||
action?: string;
|
action?: string;
|
||||||
predicate?: (
|
predicate?: (
|
||||||
appState: Readonly<AppState>,
|
appState: Readonly<AppState>,
|
||||||
|
|
|
@ -100,6 +100,7 @@ import {
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
type EXPORT_IMAGE_TYPES,
|
type EXPORT_IMAGE_TYPES,
|
||||||
randomInteger,
|
randomInteger,
|
||||||
|
CLASSES,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -431,7 +432,7 @@ import {
|
||||||
} from "../components/hyperlink/Hyperlink";
|
} from "../components/hyperlink/Hyperlink";
|
||||||
|
|
||||||
import { Fonts } from "../fonts";
|
import { Fonts } from "../fonts";
|
||||||
import { editorJotaiStore } from "../editor-jotai";
|
import { editorJotaiStore, type WritableAtom } from "../editor-jotai";
|
||||||
import { ImageSceneDataError } from "../errors";
|
import { ImageSceneDataError } from "../errors";
|
||||||
import {
|
import {
|
||||||
getSnapLinesAtPointer,
|
getSnapLinesAtPointer,
|
||||||
|
@ -467,6 +468,12 @@ import { LassoTrail } from "../lasso";
|
||||||
|
|
||||||
import { EraserTrail } from "../eraser";
|
import { EraserTrail } from "../eraser";
|
||||||
|
|
||||||
|
import ConvertElementTypePopup, {
|
||||||
|
getConversionTypeFromElements,
|
||||||
|
convertElementTypePopupAtom,
|
||||||
|
convertElementTypes,
|
||||||
|
} from "./ConvertElementTypePopup";
|
||||||
|
|
||||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||||
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
|
import { ContextMenu, CONTEXT_MENU_SEPARATOR } from "./ContextMenu";
|
||||||
|
@ -498,7 +505,6 @@ import type { ExportedElements } from "../data";
|
||||||
import type { ContextMenuItems } from "./ContextMenu";
|
import type { ContextMenuItems } from "./ContextMenu";
|
||||||
import type { FileSystemHandle } from "../data/filesystem";
|
import type { FileSystemHandle } from "../data/filesystem";
|
||||||
import type { ExcalidrawElementSkeleton } from "../data/transform";
|
import type { ExcalidrawElementSkeleton } from "../data/transform";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AppClassProperties,
|
AppClassProperties,
|
||||||
AppProps,
|
AppProps,
|
||||||
|
@ -815,6 +821,15 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateEditorAtom = <Value, Args extends unknown[], Result>(
|
||||||
|
atom: WritableAtom<Value, Args, Result>,
|
||||||
|
...args: Args
|
||||||
|
): Result => {
|
||||||
|
const result = editorJotaiStore.set(atom, ...args);
|
||||||
|
this.triggerRender();
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
private onWindowMessage(event: MessageEvent) {
|
private onWindowMessage(event: MessageEvent) {
|
||||||
if (
|
if (
|
||||||
event.origin !== "https://player.vimeo.com" &&
|
event.origin !== "https://player.vimeo.com" &&
|
||||||
|
@ -1583,6 +1598,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
const firstSelectedElement = selectedElements[0];
|
const firstSelectedElement = selectedElements[0];
|
||||||
|
|
||||||
|
const showShapeSwitchPanel =
|
||||||
|
editorJotaiStore.get(convertElementTypePopupAtom)?.type === "panel";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx("excalidraw excalidraw-container", {
|
className={clsx("excalidraw excalidraw-container", {
|
||||||
|
@ -1857,6 +1875,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{this.renderFrameNames()}
|
{this.renderFrameNames()}
|
||||||
|
{showShapeSwitchPanel && (
|
||||||
|
<ConvertElementTypePopup app={this} />
|
||||||
|
)}
|
||||||
</ExcalidrawActionManagerContext.Provider>
|
</ExcalidrawActionManagerContext.Provider>
|
||||||
{this.renderEmbeddables()}
|
{this.renderEmbeddables()}
|
||||||
</ExcalidrawElementsContext.Provider>
|
</ExcalidrawElementsContext.Provider>
|
||||||
|
@ -2138,7 +2159,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private openEyeDropper = ({ type }: { type: "stroke" | "background" }) => {
|
private openEyeDropper = ({ type }: { type: "stroke" | "background" }) => {
|
||||||
editorJotaiStore.set(activeEyeDropperAtom, {
|
this.updateEditorAtom(activeEyeDropperAtom, {
|
||||||
swapPreviewOnAlt: true,
|
swapPreviewOnAlt: true,
|
||||||
colorPickerType:
|
colorPickerType:
|
||||||
type === "stroke" ? "elementStroke" : "elementBackground",
|
type === "stroke" ? "elementStroke" : "elementBackground",
|
||||||
|
@ -4157,6 +4178,40 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shape switching
|
||||||
|
if (event.key === KEYS.ESCAPE) {
|
||||||
|
this.updateEditorAtom(convertElementTypePopupAtom, null);
|
||||||
|
} else if (
|
||||||
|
event.key === KEYS.TAB &&
|
||||||
|
(document.activeElement === this.excalidrawContainerRef?.current ||
|
||||||
|
document.activeElement?.classList.contains(
|
||||||
|
CLASSES.CONVERT_ELEMENT_TYPE_POPUP,
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const conversionType =
|
||||||
|
getConversionTypeFromElements(selectedElements);
|
||||||
|
|
||||||
|
if (
|
||||||
|
editorJotaiStore.get(convertElementTypePopupAtom)?.type === "panel"
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
convertElementTypes(this, {
|
||||||
|
conversionType,
|
||||||
|
direction: event.shiftKey ? "left" : "right",
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
this.store.shouldCaptureIncrement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (conversionType) {
|
||||||
|
this.updateEditorAtom(convertElementTypePopupAtom, {
|
||||||
|
type: "panel",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
event.key === KEYS.ESCAPE &&
|
event.key === KEYS.ESCAPE &&
|
||||||
this.flowChartCreator.isCreatingChart
|
this.flowChartCreator.isCreatingChart
|
||||||
|
@ -4615,7 +4670,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
event[KEYS.CTRL_OR_CMD] &&
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE)
|
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE)
|
||||||
) {
|
) {
|
||||||
editorJotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
|
this.updateEditorAtom(activeConfirmDialogAtom, "clearCanvas");
|
||||||
}
|
}
|
||||||
|
|
||||||
// eye dropper
|
// eye dropper
|
||||||
|
@ -6364,7 +6419,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
focus: false,
|
focus: false,
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
editorJotaiStore.set(searchItemInFocusAtom, null);
|
this.updateEditorAtom(searchItemInFocusAtom, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editorJotaiStore.get(convertElementTypePopupAtom)) {
|
||||||
|
this.updateEditorAtom(convertElementTypePopupAtom, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// since contextMenu options are potentially evaluated on each render,
|
// since contextMenu options are potentially evaluated on each render,
|
||||||
|
|
|
@ -11,6 +11,8 @@ import {
|
||||||
isWritableElement,
|
isWritableElement,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { actionToggleShapeSwitch } from "@excalidraw/excalidraw/actions/actionToggleShapeSwitch";
|
||||||
|
|
||||||
import type { MarkRequired } from "@excalidraw/common/utility-types";
|
import type { MarkRequired } from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -410,6 +412,14 @@ function CommandPaletteInner({
|
||||||
actionManager.executeAction(actionToggleSearchMenu);
|
actionManager.executeAction(actionToggleSearchMenu);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t("labels.shapeSwitch"),
|
||||||
|
category: DEFAULT_CATEGORIES.elements,
|
||||||
|
icon: boltIcon,
|
||||||
|
perform: () => {
|
||||||
|
actionManager.executeAction(actionToggleShapeSwitch);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t("labels.changeStroke"),
|
label: t("labels.changeStroke"),
|
||||||
keywords: ["color", "outline"],
|
keywords: ["color", "outline"],
|
||||||
|
|
18
packages/excalidraw/components/ConvertElementTypePopup.scss
Normal file
18
packages/excalidraw/components/ConvertElementTypePopup.scss
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
@import "../css//variables.module.scss";
|
||||||
|
|
||||||
|
.excalidraw {
|
||||||
|
.ConvertElementTypePopup {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: var(--island-bg-color);
|
||||||
|
box-shadow: var(--shadow-island);
|
||||||
|
padding: 0.5rem;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1047
packages/excalidraw/components/ConvertElementTypePopup.tsx
Normal file
1047
packages/excalidraw/components/ConvertElementTypePopup.tsx
Normal file
File diff suppressed because it is too large
Load diff
|
@ -21,7 +21,7 @@ const Header = () => (
|
||||||
className="HelpDialog__btn"
|
className="HelpDialog__btn"
|
||||||
href="https://docs.excalidraw.com"
|
href="https://docs.excalidraw.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener"
|
||||||
>
|
>
|
||||||
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
||||||
{t("helpDialog.documentation")}
|
{t("helpDialog.documentation")}
|
||||||
|
@ -30,7 +30,7 @@ const Header = () => (
|
||||||
className="HelpDialog__btn"
|
className="HelpDialog__btn"
|
||||||
href="https://plus.excalidraw.com/blog"
|
href="https://plus.excalidraw.com/blog"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener"
|
||||||
>
|
>
|
||||||
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
<div className="HelpDialog__link-icon">{ExternalLinkIcon}</div>
|
||||||
{t("helpDialog.blog")}
|
{t("helpDialog.blog")}
|
||||||
|
@ -247,6 +247,11 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||||
label={t("toolBar.link")}
|
label={t("toolBar.link")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+K")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+K")]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("toolBar.convertElementType")}
|
||||||
|
shortcuts={["Tab", "Shift+Tab"]}
|
||||||
|
isOr={true}
|
||||||
|
/>
|
||||||
</ShortcutIsland>
|
</ShortcutIsland>
|
||||||
<ShortcutIsland
|
<ShortcutIsland
|
||||||
className="HelpDialog__island--view"
|
className="HelpDialog__island--view"
|
||||||
|
|
|
@ -389,7 +389,7 @@ const PublishLibrary = ({
|
||||||
<a
|
<a
|
||||||
href="https://libraries.excalidraw.com"
|
href="https://libraries.excalidraw.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener"
|
||||||
>
|
>
|
||||||
{el}
|
{el}
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -11,8 +11,10 @@ import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
import { angleIcon } from "../icons";
|
import { angleIcon } from "../icons";
|
||||||
|
|
||||||
|
import { updateBindings } from "../../../element/src/binding";
|
||||||
|
|
||||||
import DragInput from "./DragInput";
|
import DragInput from "./DragInput";
|
||||||
import { getStepSizedValue, isPropertyEditable, updateBindings } from "./utils";
|
import { getStepSizedValue, isPropertyEditable } from "./utils";
|
||||||
|
|
||||||
import type { DragInputCallbackType } from "./DragInput";
|
import type { DragInputCallbackType } from "./DragInput";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
import { pointFrom, pointRotateRads } from "@excalidraw/math";
|
||||||
|
|
||||||
import {
|
|
||||||
bindOrUnbindLinearElements,
|
|
||||||
updateBoundElements,
|
|
||||||
} from "@excalidraw/element/binding";
|
|
||||||
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
import { getBoundTextElement } from "@excalidraw/element/textElement";
|
||||||
import {
|
import {
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
isLinearElement,
|
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "@excalidraw/element/typeChecks";
|
} from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
|
@ -27,6 +22,8 @@ import type {
|
||||||
|
|
||||||
import type Scene from "@excalidraw/element/Scene";
|
import type Scene from "@excalidraw/element/Scene";
|
||||||
|
|
||||||
|
import { updateBindings } from "../../../element/src/binding";
|
||||||
|
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
|
|
||||||
export type StatsInputProperty =
|
export type StatsInputProperty =
|
||||||
|
@ -194,19 +191,3 @@ export const getAtomicUnits = (
|
||||||
});
|
});
|
||||||
return _atomicUnits;
|
return _atomicUnits;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateBindings = (
|
|
||||||
latestElement: ExcalidrawElement,
|
|
||||||
scene: Scene,
|
|
||||||
options?: {
|
|
||||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
|
||||||
newSize?: { width: number; height: number };
|
|
||||||
zoom?: AppState["zoom"];
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
if (isLinearElement(latestElement)) {
|
|
||||||
bindOrUnbindLinearElements([latestElement], true, [], scene, options?.zoom);
|
|
||||||
} else {
|
|
||||||
updateBoundElements(latestElement, scene, options);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ const DropdownMenuItemLink = ({
|
||||||
onSelect,
|
onSelect,
|
||||||
className = "",
|
className = "",
|
||||||
selected,
|
selected,
|
||||||
rel = "noreferrer",
|
rel = "noopener",
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
href: string;
|
href: string;
|
||||||
|
@ -31,11 +31,12 @@ const DropdownMenuItemLink = ({
|
||||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// eslint-disable-next-line react/jsx-no-target-blank
|
||||||
<a
|
<a
|
||||||
{...rest}
|
{...rest}
|
||||||
href={href}
|
href={href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel={rel || "noopener"}
|
||||||
className={getDropdownMenuItemClassName(className, selected)}
|
className={getDropdownMenuItemClassName(className, selected)}
|
||||||
title={rest.title ?? rest["aria-label"]}
|
title={rest.title ?? rest["aria-label"]}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
|
|
@ -78,7 +78,7 @@ const WelcomeScreenMenuItemLink = ({
|
||||||
className={`welcome-screen-menu-item ${className}`}
|
className={`welcome-screen-menu-item ${className}`}
|
||||||
href={href}
|
href={href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
>
|
>
|
||||||
<WelcomeScreenMenuItemContent icon={icon} shortcut={shortcut}>
|
<WelcomeScreenMenuItemContent icon={icon} shortcut={shortcut}>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { bumpVersion } from "@excalidraw/element/mutateElement";
|
||||||
import { getContainerElement } from "@excalidraw/element/textElement";
|
import { getContainerElement } from "@excalidraw/element/textElement";
|
||||||
import { detectLineHeight } from "@excalidraw/element/textMeasurements";
|
import { detectLineHeight } from "@excalidraw/element/textMeasurements";
|
||||||
import {
|
import {
|
||||||
|
isArrowBoundToElement,
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
isElbowArrow,
|
isElbowArrow,
|
||||||
isFixedPointBinding,
|
isFixedPointBinding,
|
||||||
|
@ -594,8 +595,7 @@ export const restoreElements = (
|
||||||
return restoredElements.map((element) => {
|
return restoredElements.map((element) => {
|
||||||
if (
|
if (
|
||||||
isElbowArrow(element) &&
|
isElbowArrow(element) &&
|
||||||
element.startBinding == null &&
|
!isArrowBoundToElement(element) &&
|
||||||
element.endBinding == null &&
|
|
||||||
!validateElbowPoints(element.points)
|
!validateElbowPoints(element.points)
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { atom, createStore, type PrimitiveAtom } from "jotai";
|
import {
|
||||||
|
atom,
|
||||||
|
createStore,
|
||||||
|
type PrimitiveAtom,
|
||||||
|
type WritableAtom,
|
||||||
|
} from "jotai";
|
||||||
import { createIsolation } from "jotai-scope";
|
import { createIsolation } from "jotai-scope";
|
||||||
|
|
||||||
const jotai = createIsolation();
|
const jotai = createIsolation();
|
||||||
|
|
||||||
export { atom, PrimitiveAtom };
|
export { atom, PrimitiveAtom, WritableAtom };
|
||||||
export const { useAtom, useSetAtom, useAtomValue, useStore } = jotai;
|
export const { useAtom, useSetAtom, useAtomValue, useStore } = jotai;
|
||||||
export const EditorJotaiProvider: ReturnType<
|
export const EditorJotaiProvider: ReturnType<
|
||||||
typeof createIsolation
|
typeof createIsolation
|
||||||
|
|
|
@ -165,7 +165,9 @@
|
||||||
"unCroppedDimension": "Uncropped dimension",
|
"unCroppedDimension": "Uncropped dimension",
|
||||||
"copyElementLink": "Copy link to object",
|
"copyElementLink": "Copy link to object",
|
||||||
"linkToElement": "Link to object",
|
"linkToElement": "Link to object",
|
||||||
"wrapSelectionInFrame": "Wrap selection in frame"
|
"wrapSelectionInFrame": "Wrap selection in frame",
|
||||||
|
"tab": "Tab",
|
||||||
|
"shapeSwitch": "Switch shape"
|
||||||
},
|
},
|
||||||
"elementLink": {
|
"elementLink": {
|
||||||
"title": "Link to object",
|
"title": "Link to object",
|
||||||
|
@ -296,7 +298,8 @@
|
||||||
"laser": "Laser pointer",
|
"laser": "Laser pointer",
|
||||||
"hand": "Hand (panning tool)",
|
"hand": "Hand (panning tool)",
|
||||||
"extraTools": "More tools",
|
"extraTools": "More tools",
|
||||||
"mermaidToExcalidraw": "Mermaid to Excalidraw"
|
"mermaidToExcalidraw": "Mermaid to Excalidraw",
|
||||||
|
"convertElementType": "Toggle shape type"
|
||||||
},
|
},
|
||||||
"element": {
|
"element": {
|
||||||
"rectangle": "Rectangle",
|
"rectangle": "Rectangle",
|
||||||
|
|
|
@ -21,7 +21,7 @@ exports[`<Excalidraw/> > <MainMenu/> > should render main menu with host menu it
|
||||||
<a
|
<a
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
href="blog.excalidaw.com"
|
href="blog.excalidaw.com"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -392,7 +392,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
aria-label="GitHub"
|
aria-label="GitHub"
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
href="https://github.com/excalidraw/excalidraw"
|
href="https://github.com/excalidraw/excalidraw"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="GitHub"
|
title="GitHub"
|
||||||
>
|
>
|
||||||
|
@ -426,7 +426,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
aria-label="X"
|
aria-label="X"
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
href="https://x.com/excalidraw"
|
href="https://x.com/excalidraw"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="X"
|
title="X"
|
||||||
>
|
>
|
||||||
|
@ -472,7 +472,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
||||||
aria-label="Discord"
|
aria-label="Discord"
|
||||||
class="dropdown-menu-item dropdown-menu-item-base"
|
class="dropdown-menu-item dropdown-menu-item-base"
|
||||||
href="https://discord.gg/UexuTaE"
|
href="https://discord.gg/UexuTaE"
|
||||||
rel="noreferrer"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
title="Discord"
|
title="Discord"
|
||||||
>
|
>
|
||||||
|
|
|
@ -714,6 +714,7 @@ export type AppClassProperties = {
|
||||||
excalidrawContainerValue: App["excalidrawContainerValue"];
|
excalidrawContainerValue: App["excalidrawContainerValue"];
|
||||||
|
|
||||||
onPointerUpEmitter: App["onPointerUpEmitter"];
|
onPointerUpEmitter: App["onPointerUpEmitter"];
|
||||||
|
updateEditorAtom: App["updateEditorAtom"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PointerDownState = Readonly<{
|
export type PointerDownState = Readonly<{
|
||||||
|
|
|
@ -80,6 +80,8 @@ const getTransform = (
|
||||||
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SubmitHandler = () => void;
|
||||||
|
|
||||||
export const textWysiwyg = ({
|
export const textWysiwyg = ({
|
||||||
id,
|
id,
|
||||||
onChange,
|
onChange,
|
||||||
|
@ -106,7 +108,7 @@ export const textWysiwyg = ({
|
||||||
excalidrawContainer: HTMLDivElement | null;
|
excalidrawContainer: HTMLDivElement | null;
|
||||||
app: App;
|
app: App;
|
||||||
autoSelect?: boolean;
|
autoSelect?: boolean;
|
||||||
}) => {
|
}): SubmitHandler => {
|
||||||
const textPropertiesUpdated = (
|
const textPropertiesUpdated = (
|
||||||
updatedTextElement: ExcalidrawTextElement,
|
updatedTextElement: ExcalidrawTextElement,
|
||||||
editable: HTMLTextAreaElement,
|
editable: HTMLTextAreaElement,
|
||||||
|
@ -186,7 +188,6 @@ export const textWysiwyg = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
maxWidth = getBoundTextMaxWidth(container, updatedTextElement);
|
maxWidth = getBoundTextMaxWidth(container, updatedTextElement);
|
||||||
|
|
||||||
maxHeight = getBoundTextMaxHeight(
|
maxHeight = getBoundTextMaxHeight(
|
||||||
container,
|
container,
|
||||||
updatedTextElement as ExcalidrawTextElementWithContainer,
|
updatedTextElement as ExcalidrawTextElementWithContainer,
|
||||||
|
@ -735,4 +736,6 @@ export const textWysiwyg = ({
|
||||||
excalidrawContainer
|
excalidrawContainer
|
||||||
?.querySelector(".excalidraw-textEditorContainer")!
|
?.querySelector(".excalidraw-textEditorContainer")!
|
||||||
.appendChild(editable);
|
.appendChild(editable);
|
||||||
|
|
||||||
|
return handleSubmit;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue