mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feats: 为 snow-shot 提供自定义功能
This commit is contained in:
parent
58f7d33d80
commit
27e0350fa7
21 changed files with 940 additions and 478 deletions
|
@ -848,6 +848,13 @@ const ExcalidrawWrapper = () => {
|
||||||
handleKeyboardGlobally={true}
|
handleKeyboardGlobally={true}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
theme={editorTheme}
|
theme={editorTheme}
|
||||||
|
customOptions={{
|
||||||
|
disableKeyEvents: true,
|
||||||
|
// hideMainToolbar: true,
|
||||||
|
// hideMenu: true,
|
||||||
|
// hideFooter: true,
|
||||||
|
hideContextMenu: true,
|
||||||
|
}}
|
||||||
renderTopRightUI={(isMobile) => {
|
renderTopRightUI={(isMobile) => {
|
||||||
if (isMobile || !collabAPI || isCollabDisabled) {
|
if (isMobile || !collabAPI || isCollabDisabled) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -78,8 +78,8 @@
|
||||||
"autorelease": "node scripts/autorelease.js",
|
"autorelease": "node scripts/autorelease.js",
|
||||||
"prerelease:excalidraw": "node scripts/prerelease.js",
|
"prerelease:excalidraw": "node scripts/prerelease.js",
|
||||||
"release:excalidraw": "node scripts/release.js",
|
"release:excalidraw": "node scripts/release.js",
|
||||||
"rm:build": "rm -rf excalidraw-app/{build,dist,dev-dist} && rm -rf packages/*/{dist,build} && rm -rf examples/*/{build,dist}",
|
"rm:build": "rimraf -rfexcalidraw-app/{build,dist,dev-dist} && rimraf -rfpackages/*/{dist,build} && rimraf -rfexamples/*/{build,dist}",
|
||||||
"rm:node_modules": "rm -rf node_modules && rm -rf excalidraw-app/node_modules && rm -rf packages/*/node_modules",
|
"rm:node_modules": "rimraf -rfnode_modules && rimraf -rfexcalidraw-app/node_modules && rimraf -rfpackages/*/node_modules",
|
||||||
"clean-install": "yarn rm:node_modules && yarn install"
|
"clean-install": "yarn rm:node_modules && yarn install"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||||
"repository": "https://github.com/excalidraw/excalidraw",
|
"repository": "https://github.com/excalidraw/excalidraw",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"gen:types": "rm -rf types && tsc",
|
"gen:types": "rimraf -rf types && tsc",
|
||||||
"build:esm": "rm -rf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
"build:esm": "rimraf -rf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
import type { KeyboardModifiersObject } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { isDarwin } from "./constants";
|
import { isDarwin } from "./constants";
|
||||||
|
|
||||||
import type { ValueOf } from "./utility-types";
|
import type { ValueOf } from "./utility-types";
|
||||||
|
|
||||||
|
|
||||||
export const CODES = {
|
export const CODES = {
|
||||||
EQUAL: "Equal",
|
EQUAL: "Equal",
|
||||||
MINUS: "Minus",
|
MINUS: "Minus",
|
||||||
|
@ -140,12 +143,60 @@ export const isArrowKey = (key: string) =>
|
||||||
key === KEYS.ARROW_DOWN ||
|
key === KEYS.ARROW_DOWN ||
|
||||||
key === KEYS.ARROW_UP;
|
key === KEYS.ARROW_UP;
|
||||||
|
|
||||||
export const shouldResizeFromCenter = (event: MouseEvent | KeyboardEvent) =>
|
const shouldResizeFromCenterDefault = (event: MouseEvent | KeyboardEvent) =>
|
||||||
event.altKey;
|
event.altKey;
|
||||||
|
|
||||||
export const shouldMaintainAspectRatio = (event: MouseEvent | KeyboardEvent) =>
|
const shouldMaintainAspectRatioDefault = (event: MouseEvent | KeyboardEvent) =>
|
||||||
event.shiftKey;
|
event.shiftKey;
|
||||||
|
|
||||||
|
const shouldRotateWithDiscreteAngleDefault = (
|
||||||
|
event: MouseEvent | KeyboardEvent | React.PointerEvent<HTMLCanvasElement>,
|
||||||
|
) => event.shiftKey;
|
||||||
|
|
||||||
|
const shouldSnappingDefault = (event: KeyboardModifiersObject) =>
|
||||||
|
event[KEYS.CTRL_OR_CMD];
|
||||||
|
|
||||||
|
let shouldResizeFromCenterFunction = shouldResizeFromCenterDefault;
|
||||||
|
let shouldMaintainAspectRatioFunction = shouldMaintainAspectRatioDefault;
|
||||||
|
let shouldRotateWithDiscreteAngleFunction =
|
||||||
|
shouldRotateWithDiscreteAngleDefault;
|
||||||
|
let shouldSnappingFunction = shouldSnappingDefault;
|
||||||
|
|
||||||
|
export const setShouldResizeFromCenter = (
|
||||||
|
shouldResizeFromCenter: (event: MouseEvent | KeyboardEvent) => boolean,
|
||||||
|
) => {
|
||||||
|
shouldResizeFromCenterFunction = shouldResizeFromCenter;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setShouldMaintainAspectRatio = (
|
||||||
|
shouldMaintainAspectRatio: (event: MouseEvent | KeyboardEvent) => boolean,
|
||||||
|
) => {
|
||||||
|
shouldMaintainAspectRatioFunction = shouldMaintainAspectRatio;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setShouldRotateWithDiscreteAngle = (
|
||||||
|
shouldRotateWithDiscreteAngle: (
|
||||||
|
event: MouseEvent | KeyboardEvent | React.PointerEvent<HTMLCanvasElement>,
|
||||||
|
) => boolean,
|
||||||
|
) => {
|
||||||
|
shouldRotateWithDiscreteAngleFunction = shouldRotateWithDiscreteAngle;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setShouldSnapping = (
|
||||||
|
shouldSnapping: (event: KeyboardModifiersObject) => boolean,
|
||||||
|
) => {
|
||||||
|
shouldSnappingFunction = shouldSnapping;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const shouldResizeFromCenter = (event: MouseEvent | KeyboardEvent) =>
|
||||||
|
shouldResizeFromCenterFunction(event);
|
||||||
|
|
||||||
|
export const shouldMaintainAspectRatio = (event: MouseEvent | KeyboardEvent) =>
|
||||||
|
shouldMaintainAspectRatioFunction(event);
|
||||||
|
|
||||||
export const shouldRotateWithDiscreteAngle = (
|
export const shouldRotateWithDiscreteAngle = (
|
||||||
event: MouseEvent | KeyboardEvent | React.PointerEvent<HTMLCanvasElement>,
|
event: MouseEvent | KeyboardEvent | React.PointerEvent<HTMLCanvasElement>,
|
||||||
) => event.shiftKey;
|
) => shouldRotateWithDiscreteAngleFunction(event);
|
||||||
|
|
||||||
|
export const shouldSnapping = (event: KeyboardModifiersObject) =>
|
||||||
|
shouldSnappingFunction(event);
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||||
"repository": "https://github.com/excalidraw/excalidraw",
|
"repository": "https://github.com/excalidraw/excalidraw",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"gen:types": "rm -rf types && tsc",
|
"gen:types": "rimraf -rf types && tsc",
|
||||||
"build:esm": "rm -rf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
"build:esm": "rimraf -rf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { pointFrom } from "@excalidraw/math";
|
import { pointFrom } from "@excalidraw/math";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
||||||
|
@ -137,7 +137,12 @@ import { CaptureUpdateAction } from "../store";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
import type { CaptureUpdateActionType } from "../store";
|
import type { CaptureUpdateActionType } from "../store";
|
||||||
import type { AppClassProperties, AppState, Primitive } from "../types";
|
import {
|
||||||
|
ExcalidrawPropsCustomOptionsContext,
|
||||||
|
type AppClassProperties,
|
||||||
|
type AppState,
|
||||||
|
type Primitive,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
||||||
|
|
||||||
|
@ -322,28 +327,35 @@ export const actionChangeStrokeColor = register({
|
||||||
: CaptureUpdateAction.EVENTUALLY,
|
: CaptureUpdateAction.EVENTUALLY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, appProps }) => (
|
PanelComponent: ({ elements, appState, updateData, appProps }) => {
|
||||||
<>
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
<h3 aria-hidden="true">{t("labels.stroke")}</h3>
|
|
||||||
<ColorPicker
|
return (
|
||||||
topPicks={DEFAULT_ELEMENT_STROKE_PICKS}
|
<>
|
||||||
palette={DEFAULT_ELEMENT_STROKE_COLOR_PALETTE}
|
<h3 aria-hidden="true">{t("labels.stroke")}</h3>
|
||||||
type="elementStroke"
|
<ColorPicker
|
||||||
label={t("labels.stroke")}
|
topPicks={
|
||||||
color={getFormValue(
|
customOptions?.pickerRenders?.elementStrokeColors ??
|
||||||
elements,
|
DEFAULT_ELEMENT_STROKE_PICKS
|
||||||
appState,
|
}
|
||||||
(element) => element.strokeColor,
|
palette={DEFAULT_ELEMENT_STROKE_COLOR_PALETTE}
|
||||||
true,
|
type="elementStroke"
|
||||||
appState.currentItemStrokeColor,
|
label={t("labels.stroke")}
|
||||||
)}
|
color={getFormValue(
|
||||||
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
elements,
|
||||||
elements={elements}
|
appState,
|
||||||
appState={appState}
|
(element) => element.strokeColor,
|
||||||
updateData={updateData}
|
true,
|
||||||
/>
|
appState.currentItemStrokeColor,
|
||||||
</>
|
)}
|
||||||
),
|
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
||||||
|
elements={elements}
|
||||||
|
appState={appState}
|
||||||
|
updateData={updateData}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeBackgroundColor = register({
|
export const actionChangeBackgroundColor = register({
|
||||||
|
@ -368,28 +380,37 @@ export const actionChangeBackgroundColor = register({
|
||||||
: CaptureUpdateAction.EVENTUALLY,
|
: CaptureUpdateAction.EVENTUALLY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, appProps }) => (
|
PanelComponent: ({ elements, appState, updateData, appProps }) => {
|
||||||
<>
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
<h3 aria-hidden="true">{t("labels.background")}</h3>
|
|
||||||
<ColorPicker
|
return (
|
||||||
topPicks={DEFAULT_ELEMENT_BACKGROUND_PICKS}
|
<>
|
||||||
palette={DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE}
|
<h3 aria-hidden="true">{t("labels.background")}</h3>
|
||||||
type="elementBackground"
|
<ColorPicker
|
||||||
label={t("labels.background")}
|
topPicks={
|
||||||
color={getFormValue(
|
customOptions?.pickerRenders?.elementBackgroundColors ??
|
||||||
elements,
|
DEFAULT_ELEMENT_BACKGROUND_PICKS
|
||||||
appState,
|
}
|
||||||
(element) => element.backgroundColor,
|
palette={DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE}
|
||||||
true,
|
type="elementBackground"
|
||||||
appState.currentItemBackgroundColor,
|
label={t("labels.background")}
|
||||||
)}
|
color={getFormValue(
|
||||||
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
|
elements,
|
||||||
elements={elements}
|
appState,
|
||||||
appState={appState}
|
(element) => element.backgroundColor,
|
||||||
updateData={updateData}
|
true,
|
||||||
/>
|
appState.currentItemBackgroundColor,
|
||||||
</>
|
)}
|
||||||
),
|
onChange={(color) =>
|
||||||
|
updateData({ currentItemBackgroundColor: color })
|
||||||
|
}
|
||||||
|
elements={elements}
|
||||||
|
appState={appState}
|
||||||
|
updateData={updateData}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeFillStyle = register({
|
export const actionChangeFillStyle = register({
|
||||||
|
|
|
@ -7,6 +7,8 @@ import {
|
||||||
moveAllRight,
|
moveAllRight,
|
||||||
} from "@excalidraw/element/zindex";
|
} from "@excalidraw/element/zindex";
|
||||||
|
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BringForwardIcon,
|
BringForwardIcon,
|
||||||
BringToFrontIcon,
|
BringToFrontIcon,
|
||||||
|
@ -16,6 +18,8 @@ import {
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { CaptureUpdateAction } from "../store";
|
import { CaptureUpdateAction } from "../store";
|
||||||
|
|
||||||
|
import { ExcalidrawPropsCustomOptionsContext } from "../types";
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
export const actionSendBackward = register({
|
export const actionSendBackward = register({
|
||||||
|
@ -36,16 +40,29 @@ export const actionSendBackward = register({
|
||||||
event[KEYS.CTRL_OR_CMD] &&
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
!event.shiftKey &&
|
!event.shiftKey &&
|
||||||
event.code === CODES.BRACKET_LEFT,
|
event.code === CODES.BRACKET_LEFT,
|
||||||
PanelComponent: ({ updateData, appState }) => (
|
PanelComponent: ({ updateData, appState }) => {
|
||||||
<button
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
type="button"
|
|
||||||
className="zIndexButton"
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
onClick={() => updateData(null)}
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
title={`${t("labels.sendBackward")} — ${getShortcutKey("CtrlOrCmd+[")}`}
|
onClick: () => updateData(null),
|
||||||
>
|
title: `${t("labels.sendBackward")}`,
|
||||||
{SendBackwardIcon}
|
children: SendBackwardIcon,
|
||||||
</button>
|
name: "sendBackward",
|
||||||
),
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="zIndexButton"
|
||||||
|
onClick={() => updateData(null)}
|
||||||
|
title={`${t("labels.sendBackward")} — ${getShortcutKey("CtrlOrCmd+[")}`}
|
||||||
|
>
|
||||||
|
{SendBackwardIcon}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionBringForward = register({
|
export const actionBringForward = register({
|
||||||
|
@ -66,16 +83,29 @@ export const actionBringForward = register({
|
||||||
event[KEYS.CTRL_OR_CMD] &&
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
!event.shiftKey &&
|
!event.shiftKey &&
|
||||||
event.code === CODES.BRACKET_RIGHT,
|
event.code === CODES.BRACKET_RIGHT,
|
||||||
PanelComponent: ({ updateData, appState }) => (
|
PanelComponent: ({ updateData, appState }) => {
|
||||||
<button
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
type="button"
|
|
||||||
className="zIndexButton"
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
onClick={() => updateData(null)}
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
title={`${t("labels.bringForward")} — ${getShortcutKey("CtrlOrCmd+]")}`}
|
onClick: () => updateData(null),
|
||||||
>
|
title: `${t("labels.bringForward")}`,
|
||||||
{BringForwardIcon}
|
children: BringForwardIcon,
|
||||||
</button>
|
name: "bringForward",
|
||||||
),
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="zIndexButton"
|
||||||
|
onClick={() => updateData(null)}
|
||||||
|
title={`${t("labels.bringForward")} — ${getShortcutKey("CtrlOrCmd+]")}`}
|
||||||
|
>
|
||||||
|
{BringForwardIcon}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionSendToBack = register({
|
export const actionSendToBack = register({
|
||||||
|
@ -99,20 +129,33 @@ export const actionSendToBack = register({
|
||||||
: event[KEYS.CTRL_OR_CMD] &&
|
: event[KEYS.CTRL_OR_CMD] &&
|
||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
event.code === CODES.BRACKET_LEFT,
|
event.code === CODES.BRACKET_LEFT,
|
||||||
PanelComponent: ({ updateData, appState }) => (
|
PanelComponent: ({ updateData, appState }) => {
|
||||||
<button
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
type="button"
|
|
||||||
className="zIndexButton"
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
onClick={() => updateData(null)}
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
title={`${t("labels.sendToBack")} — ${
|
onClick: () => updateData(null),
|
||||||
isDarwin
|
title: `${t("labels.sendToBack")}`,
|
||||||
? getShortcutKey("CtrlOrCmd+Alt+[")
|
children: SendToBackIcon,
|
||||||
: getShortcutKey("CtrlOrCmd+Shift+[")
|
name: "sendToBack",
|
||||||
}`}
|
});
|
||||||
>
|
}
|
||||||
{SendToBackIcon}
|
|
||||||
</button>
|
return (
|
||||||
),
|
<button
|
||||||
|
type="button"
|
||||||
|
className="zIndexButton"
|
||||||
|
onClick={() => updateData(null)}
|
||||||
|
title={`${t("labels.sendToBack")} — ${
|
||||||
|
isDarwin
|
||||||
|
? getShortcutKey("CtrlOrCmd+Alt+[")
|
||||||
|
: getShortcutKey("CtrlOrCmd+Shift+[")
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{SendToBackIcon}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionBringToFront = register({
|
export const actionBringToFront = register({
|
||||||
|
@ -137,18 +180,31 @@ export const actionBringToFront = register({
|
||||||
: event[KEYS.CTRL_OR_CMD] &&
|
: event[KEYS.CTRL_OR_CMD] &&
|
||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
event.code === CODES.BRACKET_RIGHT,
|
event.code === CODES.BRACKET_RIGHT,
|
||||||
PanelComponent: ({ updateData, appState }) => (
|
PanelComponent: ({ updateData, appState }) => {
|
||||||
<button
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
type="button"
|
|
||||||
className="zIndexButton"
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
onClick={(event) => updateData(null)}
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
title={`${t("labels.bringToFront")} — ${
|
onClick: () => updateData(null),
|
||||||
isDarwin
|
title: `${t("labels.bringToFront")}`,
|
||||||
? getShortcutKey("CtrlOrCmd+Alt+]")
|
children: BringToFrontIcon,
|
||||||
: getShortcutKey("CtrlOrCmd+Shift+]")
|
name: "bringToFront",
|
||||||
}`}
|
});
|
||||||
>
|
}
|
||||||
{BringToFrontIcon}
|
|
||||||
</button>
|
return (
|
||||||
),
|
<button
|
||||||
|
type="button"
|
||||||
|
className="zIndexButton"
|
||||||
|
onClick={(event) => updateData(null)}
|
||||||
|
title={`${t("labels.bringToFront")} — ${
|
||||||
|
isDarwin
|
||||||
|
? getShortcutKey("CtrlOrCmd+Alt+]")
|
||||||
|
: getShortcutKey("CtrlOrCmd+Shift+]")
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{BringToFrontIcon}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -160,7 +160,6 @@ export class ActionManager {
|
||||||
const appState = this.getAppState();
|
const appState = this.getAppState();
|
||||||
const updateData = (formState?: any) => {
|
const updateData = (formState?: any) => {
|
||||||
trackAction(action, "ui", appState, elements, this.app, formState);
|
trackAction(action, "ui", appState, elements, this.app, formState);
|
||||||
|
|
||||||
this.updater(
|
this.updater(
|
||||||
action.perform(
|
action.perform(
|
||||||
this.getElementsIncludingDeleted(),
|
this.getElementsIncludingDeleted(),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useState } from "react";
|
import { useState, useContext } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CLASSES,
|
CLASSES,
|
||||||
|
@ -46,6 +46,8 @@ import {
|
||||||
hasStrokeWidth,
|
hasStrokeWidth,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
|
|
||||||
|
import { ExcalidrawPropsCustomOptionsContext } from "../types";
|
||||||
|
|
||||||
import { SHAPES } from "./shapes";
|
import { SHAPES } from "./shapes";
|
||||||
|
|
||||||
import "./Actions.scss";
|
import "./Actions.scss";
|
||||||
|
@ -112,6 +114,8 @@ export const SelectedShapeActions = ({
|
||||||
renderAction: ActionManager["renderAction"];
|
renderAction: ActionManager["renderAction"];
|
||||||
app: AppClassProperties;
|
app: AppClassProperties;
|
||||||
}) => {
|
}) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
const targetElements = getTargetElements(elementsMap, appState);
|
const targetElements = getTargetElements(elementsMap, appState);
|
||||||
|
|
||||||
let isSingleElementBoundContainer = false;
|
let isSingleElementBoundContainer = false;
|
||||||
|
@ -212,12 +216,22 @@ export const SelectedShapeActions = ({
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.layers")}</legend>
|
<legend>{t("labels.layers")}</legend>
|
||||||
<div className="buttonList">
|
{!customOptions?.pickerRenders?.ButtonList && (
|
||||||
{renderAction("sendToBack")}
|
<div className={"buttonList"}>
|
||||||
{renderAction("sendBackward")}
|
{renderAction("sendToBack")}
|
||||||
{renderAction("bringForward")}
|
{renderAction("sendBackward")}
|
||||||
{renderAction("bringToFront")}
|
{renderAction("bringForward")}
|
||||||
</div>
|
{renderAction("bringToFront")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{customOptions?.pickerRenders?.ButtonList && (
|
||||||
|
<customOptions.pickerRenders.ButtonList>
|
||||||
|
{renderAction("sendToBack")}
|
||||||
|
{renderAction("sendBackward")}
|
||||||
|
{renderAction("bringForward")}
|
||||||
|
{renderAction("bringToFront")}
|
||||||
|
</customOptions.pickerRenders.ButtonList>
|
||||||
|
)}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
{showAlignActions && !isSingleElementBoundContainer && (
|
{showAlignActions && !isSingleElementBoundContainer && (
|
||||||
|
|
|
@ -100,6 +100,10 @@ import {
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
type EXPORT_IMAGE_TYPES,
|
type EXPORT_IMAGE_TYPES,
|
||||||
randomInteger,
|
randomInteger,
|
||||||
|
setShouldResizeFromCenter,
|
||||||
|
setShouldMaintainAspectRatio,
|
||||||
|
setShouldRotateWithDiscreteAngle,
|
||||||
|
setShouldSnapping,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -810,6 +814,26 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.actionManager.registerAction(
|
this.actionManager.registerAction(
|
||||||
createRedoAction(this.history, this.store),
|
createRedoAction(this.history, this.store),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 初始化一些配置
|
||||||
|
if (this.props.customOptions?.shouldResizeFromCenter) {
|
||||||
|
setShouldResizeFromCenter(
|
||||||
|
this.props.customOptions.shouldResizeFromCenter,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.props.customOptions?.shouldMaintainAspectRatio) {
|
||||||
|
setShouldMaintainAspectRatio(
|
||||||
|
this.props.customOptions.shouldMaintainAspectRatio,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.props.customOptions?.shouldRotateWithDiscreteAngle) {
|
||||||
|
setShouldRotateWithDiscreteAngle(
|
||||||
|
this.props.customOptions.shouldRotateWithDiscreteAngle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.props.customOptions?.shouldSnapping) {
|
||||||
|
setShouldSnapping(this.props.customOptions.shouldSnapping);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onWindowMessage(event: MessageEvent) {
|
private onWindowMessage(event: MessageEvent) {
|
||||||
|
@ -1648,209 +1672,215 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
generateLinkForSelection={
|
generateLinkForSelection={
|
||||||
this.props.generateLinkForSelection
|
this.props.generateLinkForSelection
|
||||||
}
|
}
|
||||||
|
customOptions={this.props.customOptions}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</LayerUI>
|
</LayerUI>
|
||||||
|
|
||||||
<div className="excalidraw-textEditorContainer" />
|
<div className="excalidraw-container-inner">
|
||||||
<div className="excalidraw-contextMenuContainer" />
|
<div className="excalidraw-textEditorContainer" />
|
||||||
<div className="excalidraw-eye-dropper-container" />
|
<div className="excalidraw-contextMenuContainer" />
|
||||||
<SVGLayer
|
<div className="excalidraw-eye-dropper-container" />
|
||||||
trails={[
|
<SVGLayer
|
||||||
this.laserTrails,
|
trails={[
|
||||||
this.lassoTrail,
|
this.laserTrails,
|
||||||
this.eraserTrail,
|
this.lassoTrail,
|
||||||
]}
|
this.eraserTrail,
|
||||||
/>
|
]}
|
||||||
{selectedElements.length === 1 &&
|
/>
|
||||||
this.state.openDialog?.name !==
|
{selectedElements.length === 1 &&
|
||||||
"elementLinkSelector" &&
|
this.state.openDialog?.name !==
|
||||||
this.state.showHyperlinkPopup && (
|
"elementLinkSelector" &&
|
||||||
<Hyperlink
|
this.state.showHyperlinkPopup && (
|
||||||
key={firstSelectedElement.id}
|
<Hyperlink
|
||||||
element={firstSelectedElement}
|
key={firstSelectedElement.id}
|
||||||
elementsMap={allElementsMap}
|
element={firstSelectedElement}
|
||||||
setAppState={this.setAppState}
|
elementsMap={allElementsMap}
|
||||||
onLinkOpen={this.props.onLinkOpen}
|
setAppState={this.setAppState}
|
||||||
setToast={this.setToast}
|
onLinkOpen={this.props.onLinkOpen}
|
||||||
updateEmbedValidationStatus={
|
setToast={this.setToast}
|
||||||
this.updateEmbedValidationStatus
|
updateEmbedValidationStatus={
|
||||||
}
|
this.updateEmbedValidationStatus
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{this.props.aiEnabled !== false &&
|
||||||
|
selectedElements.length === 1 &&
|
||||||
|
isMagicFrameElement(firstSelectedElement) && (
|
||||||
|
<ElementCanvasButtons
|
||||||
|
element={firstSelectedElement}
|
||||||
|
elementsMap={elementsMap}
|
||||||
|
>
|
||||||
|
<ElementCanvasButton
|
||||||
|
title={t("labels.convertToCode")}
|
||||||
|
icon={MagicIcon}
|
||||||
|
checked={false}
|
||||||
|
onChange={() =>
|
||||||
|
this.onMagicFrameGenerate(
|
||||||
|
firstSelectedElement,
|
||||||
|
"button",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ElementCanvasButtons>
|
||||||
|
)}
|
||||||
|
{selectedElements.length === 1 &&
|
||||||
|
isIframeElement(firstSelectedElement) &&
|
||||||
|
firstSelectedElement.customData?.generationData
|
||||||
|
?.status === "done" && (
|
||||||
|
<ElementCanvasButtons
|
||||||
|
element={firstSelectedElement}
|
||||||
|
elementsMap={elementsMap}
|
||||||
|
>
|
||||||
|
<ElementCanvasButton
|
||||||
|
title={t("labels.copySource")}
|
||||||
|
icon={copyIcon}
|
||||||
|
checked={false}
|
||||||
|
onChange={() =>
|
||||||
|
this.onIframeSrcCopy(firstSelectedElement)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ElementCanvasButton
|
||||||
|
title="Enter fullscreen"
|
||||||
|
icon={fullscreenIcon}
|
||||||
|
checked={false}
|
||||||
|
onChange={() => {
|
||||||
|
const iframe =
|
||||||
|
this.getHTMLIFrameElement(
|
||||||
|
firstSelectedElement,
|
||||||
|
);
|
||||||
|
if (iframe) {
|
||||||
|
try {
|
||||||
|
iframe.requestFullscreen();
|
||||||
|
this.setState({
|
||||||
|
activeEmbeddable: {
|
||||||
|
element: firstSelectedElement,
|
||||||
|
state: "active",
|
||||||
|
},
|
||||||
|
selectedElementIds: {
|
||||||
|
[firstSelectedElement.id]: true,
|
||||||
|
},
|
||||||
|
newElement: null,
|
||||||
|
selectionElement: null,
|
||||||
|
});
|
||||||
|
} catch (err: any) {
|
||||||
|
console.warn(err);
|
||||||
|
this.setState({
|
||||||
|
errorMessage:
|
||||||
|
"Couldn't enter fullscreen",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ElementCanvasButtons>
|
||||||
|
)}
|
||||||
|
{this.state.toast !== null && (
|
||||||
|
<Toast
|
||||||
|
message={this.state.toast.message}
|
||||||
|
onClose={() => this.setToast(null)}
|
||||||
|
duration={this.state.toast.duration}
|
||||||
|
closable={this.state.toast.closable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{this.props.aiEnabled !== false &&
|
{this.state.contextMenu &&
|
||||||
selectedElements.length === 1 &&
|
!this.props.customOptions?.hideContextMenu && (
|
||||||
isMagicFrameElement(firstSelectedElement) && (
|
<ContextMenu
|
||||||
<ElementCanvasButtons
|
items={this.state.contextMenu.items}
|
||||||
element={firstSelectedElement}
|
top={this.state.contextMenu.top}
|
||||||
elementsMap={elementsMap}
|
left={this.state.contextMenu.left}
|
||||||
>
|
actionManager={this.actionManager}
|
||||||
<ElementCanvasButton
|
onClose={(callback) => {
|
||||||
title={t("labels.convertToCode")}
|
this.setState({ contextMenu: null }, () => {
|
||||||
icon={MagicIcon}
|
this.focusContainer();
|
||||||
checked={false}
|
callback?.();
|
||||||
onChange={() =>
|
});
|
||||||
this.onMagicFrameGenerate(
|
|
||||||
firstSelectedElement,
|
|
||||||
"button",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ElementCanvasButtons>
|
|
||||||
)}
|
|
||||||
{selectedElements.length === 1 &&
|
|
||||||
isIframeElement(firstSelectedElement) &&
|
|
||||||
firstSelectedElement.customData?.generationData
|
|
||||||
?.status === "done" && (
|
|
||||||
<ElementCanvasButtons
|
|
||||||
element={firstSelectedElement}
|
|
||||||
elementsMap={elementsMap}
|
|
||||||
>
|
|
||||||
<ElementCanvasButton
|
|
||||||
title={t("labels.copySource")}
|
|
||||||
icon={copyIcon}
|
|
||||||
checked={false}
|
|
||||||
onChange={() =>
|
|
||||||
this.onIframeSrcCopy(firstSelectedElement)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<ElementCanvasButton
|
|
||||||
title="Enter fullscreen"
|
|
||||||
icon={fullscreenIcon}
|
|
||||||
checked={false}
|
|
||||||
onChange={() => {
|
|
||||||
const iframe =
|
|
||||||
this.getHTMLIFrameElement(
|
|
||||||
firstSelectedElement,
|
|
||||||
);
|
|
||||||
if (iframe) {
|
|
||||||
try {
|
|
||||||
iframe.requestFullscreen();
|
|
||||||
this.setState({
|
|
||||||
activeEmbeddable: {
|
|
||||||
element: firstSelectedElement,
|
|
||||||
state: "active",
|
|
||||||
},
|
|
||||||
selectedElementIds: {
|
|
||||||
[firstSelectedElement.id]: true,
|
|
||||||
},
|
|
||||||
newElement: null,
|
|
||||||
selectionElement: null,
|
|
||||||
});
|
|
||||||
} catch (err: any) {
|
|
||||||
console.warn(err);
|
|
||||||
this.setState({
|
|
||||||
errorMessage:
|
|
||||||
"Couldn't enter fullscreen",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ElementCanvasButtons>
|
)}
|
||||||
)}
|
<StaticCanvas
|
||||||
{this.state.toast !== null && (
|
canvas={this.canvas}
|
||||||
<Toast
|
|
||||||
message={this.state.toast.message}
|
|
||||||
onClose={() => this.setToast(null)}
|
|
||||||
duration={this.state.toast.duration}
|
|
||||||
closable={this.state.toast.closable}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{this.state.contextMenu && (
|
|
||||||
<ContextMenu
|
|
||||||
items={this.state.contextMenu.items}
|
|
||||||
top={this.state.contextMenu.top}
|
|
||||||
left={this.state.contextMenu.left}
|
|
||||||
actionManager={this.actionManager}
|
|
||||||
onClose={(callback) => {
|
|
||||||
this.setState({ contextMenu: null }, () => {
|
|
||||||
this.focusContainer();
|
|
||||||
callback?.();
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<StaticCanvas
|
|
||||||
canvas={this.canvas}
|
|
||||||
rc={this.rc}
|
|
||||||
elementsMap={elementsMap}
|
|
||||||
allElementsMap={allElementsMap}
|
|
||||||
visibleElements={visibleElements}
|
|
||||||
sceneNonce={sceneNonce}
|
|
||||||
selectionNonce={
|
|
||||||
this.state.selectionElement?.versionNonce
|
|
||||||
}
|
|
||||||
scale={window.devicePixelRatio}
|
|
||||||
appState={this.state}
|
|
||||||
renderConfig={{
|
|
||||||
imageCache: this.imageCache,
|
|
||||||
isExporting: false,
|
|
||||||
renderGrid: isGridModeEnabled(this),
|
|
||||||
canvasBackgroundColor:
|
|
||||||
this.state.viewBackgroundColor,
|
|
||||||
embedsValidationStatus: this.embedsValidationStatus,
|
|
||||||
elementsPendingErasure: this.elementsPendingErasure,
|
|
||||||
pendingFlowchartNodes:
|
|
||||||
this.flowChartCreator.pendingNodes,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{this.state.newElement && (
|
|
||||||
<NewElementCanvas
|
|
||||||
appState={this.state}
|
|
||||||
scale={window.devicePixelRatio}
|
|
||||||
rc={this.rc}
|
rc={this.rc}
|
||||||
elementsMap={elementsMap}
|
elementsMap={elementsMap}
|
||||||
allElementsMap={allElementsMap}
|
allElementsMap={allElementsMap}
|
||||||
|
visibleElements={visibleElements}
|
||||||
|
sceneNonce={sceneNonce}
|
||||||
|
selectionNonce={
|
||||||
|
this.state.selectionElement?.versionNonce
|
||||||
|
}
|
||||||
|
scale={window.devicePixelRatio}
|
||||||
|
appState={this.state}
|
||||||
renderConfig={{
|
renderConfig={{
|
||||||
imageCache: this.imageCache,
|
imageCache: this.imageCache,
|
||||||
isExporting: false,
|
isExporting: false,
|
||||||
renderGrid: false,
|
renderGrid: isGridModeEnabled(this),
|
||||||
canvasBackgroundColor:
|
canvasBackgroundColor:
|
||||||
this.state.viewBackgroundColor,
|
this.state.viewBackgroundColor,
|
||||||
embedsValidationStatus:
|
embedsValidationStatus:
|
||||||
this.embedsValidationStatus,
|
this.embedsValidationStatus,
|
||||||
elementsPendingErasure:
|
elementsPendingErasure:
|
||||||
this.elementsPendingErasure,
|
this.elementsPendingErasure,
|
||||||
pendingFlowchartNodes: null,
|
pendingFlowchartNodes:
|
||||||
|
this.flowChartCreator.pendingNodes,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
{this.state.newElement && (
|
||||||
<InteractiveCanvas
|
<NewElementCanvas
|
||||||
containerRef={this.excalidrawContainerRef}
|
appState={this.state}
|
||||||
canvas={this.interactiveCanvas}
|
scale={window.devicePixelRatio}
|
||||||
elementsMap={elementsMap}
|
rc={this.rc}
|
||||||
visibleElements={visibleElements}
|
elementsMap={elementsMap}
|
||||||
allElementsMap={allElementsMap}
|
allElementsMap={allElementsMap}
|
||||||
selectedElements={selectedElements}
|
renderConfig={{
|
||||||
sceneNonce={sceneNonce}
|
imageCache: this.imageCache,
|
||||||
selectionNonce={
|
isExporting: false,
|
||||||
this.state.selectionElement?.versionNonce
|
renderGrid: false,
|
||||||
}
|
canvasBackgroundColor:
|
||||||
scale={window.devicePixelRatio}
|
this.state.viewBackgroundColor,
|
||||||
appState={this.state}
|
embedsValidationStatus:
|
||||||
device={this.device}
|
this.embedsValidationStatus,
|
||||||
renderInteractiveSceneCallback={
|
elementsPendingErasure:
|
||||||
this.renderInteractiveSceneCallback
|
this.elementsPendingErasure,
|
||||||
}
|
pendingFlowchartNodes: null,
|
||||||
handleCanvasRef={this.handleInteractiveCanvasRef}
|
}}
|
||||||
onContextMenu={this.handleCanvasContextMenu}
|
/>
|
||||||
onPointerMove={this.handleCanvasPointerMove}
|
)}
|
||||||
onPointerUp={this.handleCanvasPointerUp}
|
<InteractiveCanvas
|
||||||
onPointerCancel={this.removePointer}
|
containerRef={this.excalidrawContainerRef}
|
||||||
onTouchMove={this.handleTouchMove}
|
canvas={this.interactiveCanvas}
|
||||||
onPointerDown={this.handleCanvasPointerDown}
|
elementsMap={elementsMap}
|
||||||
onDoubleClick={this.handleCanvasDoubleClick}
|
visibleElements={visibleElements}
|
||||||
/>
|
allElementsMap={allElementsMap}
|
||||||
{this.state.userToFollow && (
|
selectedElements={selectedElements}
|
||||||
<FollowMode
|
sceneNonce={sceneNonce}
|
||||||
width={this.state.width}
|
selectionNonce={
|
||||||
height={this.state.height}
|
this.state.selectionElement?.versionNonce
|
||||||
userToFollow={this.state.userToFollow}
|
}
|
||||||
onDisconnect={this.maybeUnfollowRemoteUser}
|
scale={window.devicePixelRatio}
|
||||||
|
appState={this.state}
|
||||||
|
device={this.device}
|
||||||
|
renderInteractiveSceneCallback={
|
||||||
|
this.renderInteractiveSceneCallback
|
||||||
|
}
|
||||||
|
handleCanvasRef={this.handleInteractiveCanvasRef}
|
||||||
|
onContextMenu={this.handleCanvasContextMenu}
|
||||||
|
onPointerMove={this.handleCanvasPointerMove}
|
||||||
|
onPointerUp={this.handleCanvasPointerUp}
|
||||||
|
onPointerCancel={this.removePointer}
|
||||||
|
onTouchMove={this.handleTouchMove}
|
||||||
|
onPointerDown={this.handleCanvasPointerDown}
|
||||||
|
onDoubleClick={this.handleCanvasDoubleClick}
|
||||||
/>
|
/>
|
||||||
)}
|
{this.state.userToFollow && (
|
||||||
{this.renderFrameNames()}
|
<FollowMode
|
||||||
|
width={this.state.width}
|
||||||
|
height={this.state.height}
|
||||||
|
userToFollow={this.state.userToFollow}
|
||||||
|
onDisconnect={this.maybeUnfollowRemoteUser}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{this.renderFrameNames()}
|
||||||
|
</div>
|
||||||
</ExcalidrawActionManagerContext.Provider>
|
</ExcalidrawActionManagerContext.Provider>
|
||||||
{this.renderEmbeddables()}
|
{this.renderEmbeddables()}
|
||||||
</ExcalidrawElementsContext.Provider>
|
</ExcalidrawElementsContext.Provider>
|
||||||
|
@ -2242,7 +2272,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
this.setState((prevAppState) => {
|
this.setState((prevAppState) => {
|
||||||
const actionAppState = actionResult.appState || {};
|
const actionAppState = actionResult.appState || {};
|
||||||
|
|
||||||
return {
|
const res = {
|
||||||
...prevAppState,
|
...prevAppState,
|
||||||
...actionAppState,
|
...actionAppState,
|
||||||
// NOTE this will prevent opening context menu using an action
|
// NOTE this will prevent opening context menu using an action
|
||||||
|
@ -2256,6 +2286,21 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
name,
|
name,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Print differences between prevAppState and res
|
||||||
|
const differences = Object.keys(res).filter((key) => {
|
||||||
|
return (prevAppState as any)[key] !== (res as any)[key];
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
"State differences:",
|
||||||
|
differences.map((key) => ({
|
||||||
|
key,
|
||||||
|
prev: (prevAppState as any)[key],
|
||||||
|
new: (res as any)[key],
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
return res;
|
||||||
});
|
});
|
||||||
|
|
||||||
didUpdate = true;
|
didUpdate = true;
|
||||||
|
@ -2964,6 +3009,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
// Copy/paste
|
// Copy/paste
|
||||||
|
|
||||||
private onCut = withBatchedUpdates((event: ClipboardEvent) => {
|
private onCut = withBatchedUpdates((event: ClipboardEvent) => {
|
||||||
|
if (this.props.customOptions?.disableKeyEvents) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const isExcalidrawActive = this.excalidrawContainerRef.current?.contains(
|
const isExcalidrawActive = this.excalidrawContainerRef.current?.contains(
|
||||||
document.activeElement,
|
document.activeElement,
|
||||||
);
|
);
|
||||||
|
@ -4089,6 +4138,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
// Input handling
|
// Input handling
|
||||||
private onKeyDown = withBatchedUpdates(
|
private onKeyDown = withBatchedUpdates(
|
||||||
(event: React.KeyboardEvent | KeyboardEvent) => {
|
(event: React.KeyboardEvent | KeyboardEvent) => {
|
||||||
|
if (this.props.customOptions?.disableKeyEvents) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// normalize `event.key` when CapsLock is pressed #2372
|
// normalize `event.key` when CapsLock is pressed #2372
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -5958,7 +6011,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
let dxFromLastCommitted = gridX - rx - lastCommittedX;
|
let dxFromLastCommitted = gridX - rx - lastCommittedX;
|
||||||
let dyFromLastCommitted = gridY - ry - lastCommittedY;
|
let dyFromLastCommitted = gridY - ry - lastCommittedY;
|
||||||
|
|
||||||
if (shouldRotateWithDiscreteAngle(event)) {
|
if (
|
||||||
|
(
|
||||||
|
this.props.customOptions?.shouldRotateWithDiscreteAngle ??
|
||||||
|
shouldRotateWithDiscreteAngle
|
||||||
|
)(event)
|
||||||
|
) {
|
||||||
({ width: dxFromLastCommitted, height: dyFromLastCommitted } =
|
({ width: dxFromLastCommitted, height: dyFromLastCommitted } =
|
||||||
getLockedLinearCursorAlignSize(
|
getLockedLinearCursorAlignSize(
|
||||||
// actual coordinate of the last committed point
|
// actual coordinate of the last committed point
|
||||||
|
@ -8560,7 +8618,13 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
let dx = gridX - newElement.x;
|
let dx = gridX - newElement.x;
|
||||||
let dy = gridY - newElement.y;
|
let dy = gridY - newElement.y;
|
||||||
|
|
||||||
if (shouldRotateWithDiscreteAngle(event) && points.length === 2) {
|
if (
|
||||||
|
(
|
||||||
|
this.props.customOptions?.shouldRotateWithDiscreteAngle ??
|
||||||
|
shouldRotateWithDiscreteAngle
|
||||||
|
)(event) &&
|
||||||
|
points.length === 2
|
||||||
|
) {
|
||||||
({ width: dx, height: dy } = getLockedLinearCursorAlignSize(
|
({ width: dx, height: dy } = getLockedLinearCursorAlignSize(
|
||||||
newElement.x,
|
newElement.x,
|
||||||
newElement.y,
|
newElement.y,
|
||||||
|
@ -10451,7 +10515,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
width: distance(pointerDownState.origin.x, pointerCoords.x),
|
width: distance(pointerDownState.origin.x, pointerCoords.x),
|
||||||
height: distance(pointerDownState.origin.y, pointerCoords.y),
|
height: distance(pointerDownState.origin.y, pointerCoords.y),
|
||||||
shouldMaintainAspectRatio: shouldMaintainAspectRatio(event),
|
shouldMaintainAspectRatio: shouldMaintainAspectRatio(event),
|
||||||
shouldResizeFromCenter: false,
|
shouldResizeFromCenter: shouldResizeFromCenter(event),
|
||||||
zoom: this.state.zoom.value,
|
zoom: this.state.zoom.value,
|
||||||
informMutation,
|
informMutation,
|
||||||
});
|
});
|
||||||
|
@ -10744,11 +10808,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
selectedElements,
|
selectedElements,
|
||||||
this.scene.getElementsMapIncludingDeleted(),
|
this.scene.getElementsMapIncludingDeleted(),
|
||||||
this.scene,
|
this.scene,
|
||||||
shouldRotateWithDiscreteAngle(event),
|
(
|
||||||
|
this.props.customOptions?.shouldRotateWithDiscreteAngle ??
|
||||||
|
shouldRotateWithDiscreteAngle
|
||||||
|
)(event),
|
||||||
shouldResizeFromCenter(event),
|
shouldResizeFromCenter(event),
|
||||||
selectedElements.some((element) => isImageElement(element))
|
selectedElements.some((element) =>
|
||||||
? !shouldMaintainAspectRatio(event)
|
isImageElement(element)
|
||||||
: shouldMaintainAspectRatio(event),
|
? !shouldMaintainAspectRatio(event)
|
||||||
|
: shouldMaintainAspectRatio(event),
|
||||||
|
),
|
||||||
resizeX,
|
resizeX,
|
||||||
resizeY,
|
resizeY,
|
||||||
pointerDownState.resize.center.x,
|
pointerDownState.resize.center.x,
|
||||||
|
|
|
@ -1,37 +1,53 @@
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import { useContext, type JSX } from "react";
|
||||||
|
|
||||||
|
import { ExcalidrawPropsCustomOptionsContext } from "../types";
|
||||||
|
|
||||||
import { ButtonIcon } from "./ButtonIcon";
|
import { ButtonIcon } from "./ButtonIcon";
|
||||||
|
|
||||||
import type { JSX } from "react";
|
export type ButtonIconSelectProps<T> = {
|
||||||
|
options: {
|
||||||
|
value: T;
|
||||||
|
text: string;
|
||||||
|
icon: JSX.Element;
|
||||||
|
testId?: string;
|
||||||
|
/** if not supplied, defaults to value identity check */
|
||||||
|
active?: boolean;
|
||||||
|
}[];
|
||||||
|
value: T | null;
|
||||||
|
type?: "radio" | "button";
|
||||||
|
} & (
|
||||||
|
| { type?: "radio"; group: string; onChange: (value: T) => void }
|
||||||
|
| {
|
||||||
|
type: "button";
|
||||||
|
onClick: (
|
||||||
|
value: T,
|
||||||
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />
|
// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />
|
||||||
export const ButtonIconSelect = <T extends Object>(
|
export const ButtonIconSelect = <T extends Object>(
|
||||||
props: {
|
props: ButtonIconSelectProps<T>,
|
||||||
options: {
|
) => {
|
||||||
value: T;
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
text: string;
|
|
||||||
icon: JSX.Element;
|
if (customOptions?.pickerRenders?.buttonIconSelectRender) {
|
||||||
testId?: string;
|
return customOptions.pickerRenders.buttonIconSelectRender(props);
|
||||||
/** if not supplied, defaults to value identity check */
|
}
|
||||||
active?: boolean;
|
|
||||||
}[];
|
const renderButtonIcon = (
|
||||||
value: T | null;
|
option: ButtonIconSelectProps<T>["options"][number],
|
||||||
type?: "radio" | "button";
|
) => {
|
||||||
} & (
|
if (props.type !== "button") {
|
||||||
| { type?: "radio"; group: string; onChange: (value: T) => void }
|
return null;
|
||||||
| {
|
}
|
||||||
type: "button";
|
|
||||||
onClick: (
|
if (customOptions?.pickerRenders?.CustomButtonIcon) {
|
||||||
value: T,
|
return (
|
||||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
<customOptions.pickerRenders.CustomButtonIcon
|
||||||
) => void;
|
|
||||||
}
|
|
||||||
),
|
|
||||||
) => (
|
|
||||||
<div className="buttonList">
|
|
||||||
{props.options.map((option) =>
|
|
||||||
props.type === "button" ? (
|
|
||||||
<ButtonIcon
|
|
||||||
key={option.text}
|
key={option.text}
|
||||||
icon={option.icon}
|
icon={option.icon}
|
||||||
title={option.text}
|
title={option.text}
|
||||||
|
@ -39,22 +55,66 @@ export const ButtonIconSelect = <T extends Object>(
|
||||||
active={option.active ?? props.value === option.value}
|
active={option.active ?? props.value === option.value}
|
||||||
onClick={(event) => props.onClick(option.value, event)}
|
onClick={(event) => props.onClick(option.value, event)}
|
||||||
/>
|
/>
|
||||||
) : (
|
);
|
||||||
<label
|
}
|
||||||
key={option.text}
|
return (
|
||||||
className={clsx({ active: props.value === option.value })}
|
<ButtonIcon
|
||||||
title={option.text}
|
key={option.text}
|
||||||
>
|
icon={option.icon}
|
||||||
<input
|
title={option.text}
|
||||||
type="radio"
|
testId={option.testId}
|
||||||
name={props.group}
|
active={option.active ?? props.value === option.value}
|
||||||
onChange={() => props.onChange(option.value)}
|
onClick={(event) => props.onClick(option.value, event)}
|
||||||
checked={props.value === option.value}
|
/>
|
||||||
data-testid={option.testId}
|
);
|
||||||
/>
|
};
|
||||||
{option.icon}
|
|
||||||
</label>
|
const renderRadioButtonIcon = (
|
||||||
),
|
option: ButtonIconSelectProps<T>["options"][number],
|
||||||
)}
|
) => {
|
||||||
</div>
|
if (props.type === "button") {
|
||||||
);
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.buttonIconSelectRadioRender) {
|
||||||
|
return customOptions.pickerRenders.buttonIconSelectRadioRender({
|
||||||
|
key: option.text,
|
||||||
|
active: props.value === option.value,
|
||||||
|
title: option.text,
|
||||||
|
name: props.group,
|
||||||
|
onChange: () => props.onChange(option.value),
|
||||||
|
checked: props.value === option.value,
|
||||||
|
dataTestid: option.testId ?? "",
|
||||||
|
children: option.icon,
|
||||||
|
value: option.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={option.text}
|
||||||
|
className={clsx({ active: props.value === option.value })}
|
||||||
|
title={option.text}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name={props.group}
|
||||||
|
onChange={() => props.onChange(option.value)}
|
||||||
|
checked={props.value === option.value}
|
||||||
|
data-testid={option.testId}
|
||||||
|
/>
|
||||||
|
{option.icon}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="buttonList">
|
||||||
|
{props.options.map((option) =>
|
||||||
|
props.type === "button"
|
||||||
|
? renderButtonIcon(option)
|
||||||
|
: renderRadioButtonIcon(option),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useRef } from "react";
|
import { useContext, useRef } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
COLOR_OUTLINE_CONTRAST_THRESHOLD,
|
COLOR_OUTLINE_CONTRAST_THRESHOLD,
|
||||||
|
@ -19,6 +19,11 @@ import { ButtonSeparator } from "../ButtonSeparator";
|
||||||
import { activeEyeDropperAtom } from "../EyeDropper";
|
import { activeEyeDropperAtom } from "../EyeDropper";
|
||||||
import { PropertiesPopover } from "../PropertiesPopover";
|
import { PropertiesPopover } from "../PropertiesPopover";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ExcalidrawPropsCustomOptionsContext,
|
||||||
|
type AppState,
|
||||||
|
} from "../../types";
|
||||||
|
|
||||||
import { ColorInput } from "./ColorInput";
|
import { ColorInput } from "./ColorInput";
|
||||||
import { Picker } from "./Picker";
|
import { Picker } from "./Picker";
|
||||||
import PickerHeading from "./PickerHeading";
|
import PickerHeading from "./PickerHeading";
|
||||||
|
@ -29,8 +34,6 @@ import "./ColorPicker.scss";
|
||||||
|
|
||||||
import type { ColorPickerType } from "./colorPickerUtils";
|
import type { ColorPickerType } from "./colorPickerUtils";
|
||||||
|
|
||||||
import type { AppState } from "../../types";
|
|
||||||
|
|
||||||
const isValidColor = (color: string) => {
|
const isValidColor = (color: string) => {
|
||||||
const style = new Option().style;
|
const style = new Option().style;
|
||||||
style.color = color;
|
style.color = color;
|
||||||
|
@ -220,6 +223,46 @@ export const ColorPicker = ({
|
||||||
updateData,
|
updateData,
|
||||||
appState,
|
appState,
|
||||||
}: ColorPickerProps) => {
|
}: ColorPickerProps) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
const renderPopover = () => {
|
||||||
|
if (customOptions?.pickerRenders?.colorPickerPopoverRender) {
|
||||||
|
return customOptions.pickerRenders.colorPickerPopoverRender({
|
||||||
|
color,
|
||||||
|
label,
|
||||||
|
type,
|
||||||
|
onChange,
|
||||||
|
elements,
|
||||||
|
palette,
|
||||||
|
updateData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover.Root
|
||||||
|
open={appState.openPopup === type}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
updateData({ openPopup: open ? type : null });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* serves as an active color indicator as well */}
|
||||||
|
<ColorPickerTrigger color={color} label={label} type={type} />
|
||||||
|
{/* popup content */}
|
||||||
|
{appState.openPopup === type && (
|
||||||
|
<ColorPickerPopupContent
|
||||||
|
type={type}
|
||||||
|
color={color}
|
||||||
|
onChange={onChange}
|
||||||
|
label={label}
|
||||||
|
elements={elements}
|
||||||
|
palette={palette}
|
||||||
|
updateData={updateData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Popover.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div role="dialog" aria-modal="true" className="color-picker-container">
|
<div role="dialog" aria-modal="true" className="color-picker-container">
|
||||||
|
@ -230,27 +273,7 @@ export const ColorPicker = ({
|
||||||
topPicks={topPicks}
|
topPicks={topPicks}
|
||||||
/>
|
/>
|
||||||
<ButtonSeparator />
|
<ButtonSeparator />
|
||||||
<Popover.Root
|
{renderPopover()}
|
||||||
open={appState.openPopup === type}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
updateData({ openPopup: open ? type : null });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* serves as an active color indicator as well */}
|
|
||||||
<ColorPickerTrigger color={color} label={label} type={type} />
|
|
||||||
{/* popup content */}
|
|
||||||
{appState.openPopup === type && (
|
|
||||||
<ColorPickerPopupContent
|
|
||||||
type={type}
|
|
||||||
color={color}
|
|
||||||
onChange={onChange}
|
|
||||||
label={label}
|
|
||||||
elements={elements}
|
|
||||||
palette={palette}
|
|
||||||
updateData={updateData}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Popover.Root>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,10 +7,13 @@ import {
|
||||||
DEFAULT_ELEMENT_STROKE_PICKS,
|
DEFAULT_ELEMENT_STROKE_PICKS,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
import { ExcalidrawPropsCustomOptionsContext } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
import { isColorDark } from "./colorPickerUtils";
|
import { isColorDark } from "./colorPickerUtils";
|
||||||
|
|
||||||
import type { ColorPickerType } from "./colorPickerUtils";
|
import type { ColorPickerType } from "./colorPickerUtils";
|
||||||
|
|
||||||
interface TopPicksProps {
|
interface TopPicksProps {
|
||||||
onChange: (color: string) => void;
|
onChange: (color: string) => void;
|
||||||
type: ColorPickerType;
|
type: ColorPickerType;
|
||||||
|
@ -24,13 +27,19 @@ export const TopPicks = ({
|
||||||
activeColor,
|
activeColor,
|
||||||
topPicks,
|
topPicks,
|
||||||
}: TopPicksProps) => {
|
}: TopPicksProps) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
let colors;
|
let colors;
|
||||||
if (type === "elementStroke") {
|
if (type === "elementStroke") {
|
||||||
colors = DEFAULT_ELEMENT_STROKE_PICKS;
|
colors =
|
||||||
|
customOptions?.pickerRenders?.elementStrokeColors ??
|
||||||
|
DEFAULT_ELEMENT_STROKE_PICKS;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "elementBackground") {
|
if (type === "elementBackground") {
|
||||||
colors = DEFAULT_ELEMENT_BACKGROUND_PICKS;
|
colors =
|
||||||
|
customOptions?.pickerRenders?.elementBackgroundColors ??
|
||||||
|
DEFAULT_ELEMENT_BACKGROUND_PICKS;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === "canvasBackground") {
|
if (type === "canvasBackground") {
|
||||||
|
@ -49,26 +58,41 @@ export const TopPicks = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="color-picker__top-picks">
|
<div className="color-picker__top-picks">
|
||||||
{colors.map((color: string) => (
|
{colors.map((color: string) => {
|
||||||
<button
|
if (customOptions?.pickerRenders?.colorPickerTopPickesButtonRender) {
|
||||||
className={clsx("color-picker__button", {
|
return customOptions.pickerRenders.colorPickerTopPickesButtonRender({
|
||||||
active: color === activeColor,
|
active: color === activeColor,
|
||||||
"is-transparent": color === "transparent" || !color,
|
color,
|
||||||
"has-outline": !isColorDark(
|
isTransparent: color === "transparent" || !color,
|
||||||
color,
|
hasOutline: !isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD),
|
||||||
COLOR_OUTLINE_CONTRAST_THRESHOLD,
|
onClick: () => onChange(color),
|
||||||
),
|
dataTestid: `color-top-pick-${color}`,
|
||||||
})}
|
children: <div className="color-picker__button-outline" />,
|
||||||
style={{ "--swatch-color": color }}
|
key: color,
|
||||||
key={color}
|
});
|
||||||
type="button"
|
}
|
||||||
title={color}
|
|
||||||
onClick={() => onChange(color)}
|
return (
|
||||||
data-testid={`color-top-pick-${color}`}
|
<button
|
||||||
>
|
className={clsx("color-picker__button", {
|
||||||
<div className="color-picker__button-outline" />
|
active: color === activeColor,
|
||||||
</button>
|
"is-transparent": color === "transparent" || !color,
|
||||||
))}
|
"has-outline": !isColorDark(
|
||||||
|
color,
|
||||||
|
COLOR_OUTLINE_CONTRAST_THRESHOLD,
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
style={{ "--swatch-color": color }}
|
||||||
|
key={color}
|
||||||
|
type="button"
|
||||||
|
title={color}
|
||||||
|
onClick={() => onChange(color)}
|
||||||
|
data-testid={`color-top-pick-${color}`}
|
||||||
|
>
|
||||||
|
<div className="color-picker__button-outline" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -72,6 +72,7 @@ import type {
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
UIAppState,
|
UIAppState,
|
||||||
AppClassProperties,
|
AppClassProperties,
|
||||||
|
ExcalidrawPropsCustomOptions,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
|
@ -95,6 +96,7 @@ interface LayerUIProps {
|
||||||
app: AppClassProperties;
|
app: AppClassProperties;
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
generateLinkForSelection?: AppProps["generateLinkForSelection"];
|
generateLinkForSelection?: AppProps["generateLinkForSelection"];
|
||||||
|
customOptions?: ExcalidrawPropsCustomOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultMainMenu: React.FC<{
|
const DefaultMainMenu: React.FC<{
|
||||||
|
@ -153,6 +155,7 @@ const LayerUI = ({
|
||||||
app,
|
app,
|
||||||
isCollaborating,
|
isCollaborating,
|
||||||
generateLinkForSelection,
|
generateLinkForSelection,
|
||||||
|
customOptions,
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const tunnels = useInitializeTunnels();
|
const tunnels = useInitializeTunnels();
|
||||||
|
@ -209,13 +212,8 @@ const LayerUI = ({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderSelectedShapeActions = () => (
|
const renderSelectedShapeActions = () => {
|
||||||
<Section
|
const children = (
|
||||||
heading="selectedShapeActions"
|
|
||||||
className={clsx("selected-shape-actions zen-mode-transition", {
|
|
||||||
"transition-left": appState.zenModeEnabled,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Island
|
<Island
|
||||||
className={CLASSES.SHAPE_ACTIONS_MENU}
|
className={CLASSES.SHAPE_ACTIONS_MENU}
|
||||||
padding={2}
|
padding={2}
|
||||||
|
@ -232,8 +230,19 @@ const LayerUI = ({
|
||||||
app={app}
|
app={app}
|
||||||
/>
|
/>
|
||||||
</Island>
|
</Island>
|
||||||
</Section>
|
);
|
||||||
);
|
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
heading="selectedShapeActions"
|
||||||
|
className={clsx("selected-shape-actions zen-mode-transition", {
|
||||||
|
"transition-left": appState.zenModeEnabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{customOptions?.layoutRenders?.menuRender?.({ children }) ?? children}
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderFixedSideContainer = () => {
|
const renderFixedSideContainer = () => {
|
||||||
const shouldRenderSelectedShapeActions = showSelectedShapeActions(
|
const shouldRenderSelectedShapeActions = showSelectedShapeActions(
|
||||||
|
@ -249,7 +258,10 @@ const LayerUI = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FixedSideContainer side="top">
|
<FixedSideContainer side="top">
|
||||||
<div className="App-menu App-menu_top">
|
<div
|
||||||
|
className="App-menu App-menu_top"
|
||||||
|
style={{ display: customOptions?.hideMenu ? "none" : undefined }}
|
||||||
|
>
|
||||||
<Stack.Col gap={6} className={clsx("App-menu_top__left")}>
|
<Stack.Col gap={6} className={clsx("App-menu_top__left")}>
|
||||||
{renderCanvasActions()}
|
{renderCanvasActions()}
|
||||||
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
||||||
|
@ -274,6 +286,11 @@ const LayerUI = ({
|
||||||
className={clsx("App-toolbar", {
|
className={clsx("App-toolbar", {
|
||||||
"zen-mode": appState.zenModeEnabled,
|
"zen-mode": appState.zenModeEnabled,
|
||||||
})}
|
})}
|
||||||
|
style={{
|
||||||
|
display: customOptions?.hideMainToolbar
|
||||||
|
? "none"
|
||||||
|
: undefined,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<HintViewer
|
<HintViewer
|
||||||
appState={appState}
|
appState={appState}
|
||||||
|
@ -547,12 +564,14 @@ const LayerUI = ({
|
||||||
>
|
>
|
||||||
{renderWelcomeScreen && <tunnels.WelcomeScreenCenterTunnel.Out />}
|
{renderWelcomeScreen && <tunnels.WelcomeScreenCenterTunnel.Out />}
|
||||||
{renderFixedSideContainer()}
|
{renderFixedSideContainer()}
|
||||||
<Footer
|
{!customOptions?.hideFooter && (
|
||||||
appState={appState}
|
<Footer
|
||||||
actionManager={actionManager}
|
appState={appState}
|
||||||
showExitZenModeBtn={showExitZenModeBtn}
|
actionManager={actionManager}
|
||||||
renderWelcomeScreen={renderWelcomeScreen}
|
showExitZenModeBtn={showExitZenModeBtn}
|
||||||
/>
|
renderWelcomeScreen={renderWelcomeScreen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{appState.scrolledOutside && (
|
{appState.scrolledOutside && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import React, { useEffect } from "react";
|
import React, { useCallback, useContext, useEffect } from "react";
|
||||||
|
|
||||||
import { getFormValue } from "../actions/actionProperties";
|
import { getFormValue } from "../actions/actionProperties";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import { ExcalidrawPropsCustomOptionsContext } from "../types";
|
||||||
|
|
||||||
import "./Range.scss";
|
import "./Range.scss";
|
||||||
|
|
||||||
|
@ -40,28 +41,42 @@ export const Range = ({
|
||||||
}
|
}
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className="control-label">
|
<label className="control-label">
|
||||||
{t("labels.opacity")}
|
{t("labels.opacity")}
|
||||||
<div className="range-wrapper">
|
{customOptions?.pickerRenders?.rangeRender ? (
|
||||||
<input
|
customOptions?.pickerRenders?.rangeRender({
|
||||||
ref={rangeRef}
|
value,
|
||||||
type="range"
|
onChange: (value: number) => {
|
||||||
min="0"
|
updateData(value);
|
||||||
max="100"
|
},
|
||||||
step="10"
|
step: 10,
|
||||||
onChange={(event) => {
|
min: 0,
|
||||||
updateData(+event.target.value);
|
max: 100,
|
||||||
}}
|
})
|
||||||
value={value}
|
) : (
|
||||||
className="range-input"
|
<div className="range-wrapper">
|
||||||
data-testid={testId}
|
<input
|
||||||
/>
|
ref={rangeRef}
|
||||||
<div className="value-bubble" ref={valueRef}>
|
type="range"
|
||||||
{value !== 0 ? value : null}
|
min="0"
|
||||||
|
max="100"
|
||||||
|
step="10"
|
||||||
|
onChange={(event) => {
|
||||||
|
updateData(+event.target.value);
|
||||||
|
}}
|
||||||
|
value={value}
|
||||||
|
className="range-input"
|
||||||
|
data-testid={testId}
|
||||||
|
/>
|
||||||
|
<div className="value-bubble" ref={valueRef}>
|
||||||
|
{value !== 0 ? value : null}
|
||||||
|
</div>
|
||||||
|
<div className="zero-label">0</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="zero-label">0</div>
|
)}
|
||||||
</div>
|
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useImperativeHandle, useRef } from "react";
|
||||||
|
|
||||||
import { DEFAULT_UI_OPTIONS, isShallowEqual } from "@excalidraw/common";
|
import { DEFAULT_UI_OPTIONS, isShallowEqual } from "@excalidraw/common";
|
||||||
|
|
||||||
|
@ -16,7 +16,8 @@ import "./css/app.scss";
|
||||||
import "./css/styles.scss";
|
import "./css/styles.scss";
|
||||||
import "./fonts/fonts.css";
|
import "./fonts/fonts.css";
|
||||||
|
|
||||||
import type { AppProps, ExcalidrawProps } from "./types";
|
import { ExcalidrawPropsCustomOptionsContext, type AppProps, type ExcalidrawProps } from "./types";
|
||||||
|
import type { ActionResult } from "./actions/types";
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
|
@ -53,6 +54,8 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||||
renderEmbeddable,
|
renderEmbeddable,
|
||||||
aiEnabled,
|
aiEnabled,
|
||||||
showDeprecatedFonts,
|
showDeprecatedFonts,
|
||||||
|
customOptions,
|
||||||
|
actionRef,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const canvasActions = props.UIOptions?.canvasActions;
|
const canvasActions = props.UIOptions?.canvasActions;
|
||||||
|
@ -108,46 +111,61 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const appRef = useRef<App>(null);
|
||||||
|
useImperativeHandle(
|
||||||
|
actionRef,
|
||||||
|
() => ({
|
||||||
|
syncActionResult: (actionResult: ActionResult) => {
|
||||||
|
appRef.current?.syncActionResult(actionResult);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorJotaiProvider store={editorJotaiStore}>
|
<ExcalidrawPropsCustomOptionsContext.Provider value={customOptions}>
|
||||||
<InitializeApp langCode={langCode} theme={theme}>
|
<EditorJotaiProvider store={editorJotaiStore}>
|
||||||
<App
|
<InitializeApp langCode={langCode} theme={theme}>
|
||||||
onChange={onChange}
|
<App
|
||||||
initialData={initialData}
|
ref={appRef}
|
||||||
excalidrawAPI={excalidrawAPI}
|
onChange={onChange}
|
||||||
isCollaborating={isCollaborating}
|
initialData={initialData}
|
||||||
onPointerUpdate={onPointerUpdate}
|
excalidrawAPI={excalidrawAPI}
|
||||||
renderTopRightUI={renderTopRightUI}
|
isCollaborating={isCollaborating}
|
||||||
langCode={langCode}
|
onPointerUpdate={onPointerUpdate}
|
||||||
viewModeEnabled={viewModeEnabled}
|
renderTopRightUI={renderTopRightUI}
|
||||||
zenModeEnabled={zenModeEnabled}
|
langCode={langCode}
|
||||||
gridModeEnabled={gridModeEnabled}
|
viewModeEnabled={viewModeEnabled}
|
||||||
libraryReturnUrl={libraryReturnUrl}
|
zenModeEnabled={zenModeEnabled}
|
||||||
theme={theme}
|
gridModeEnabled={gridModeEnabled}
|
||||||
name={name}
|
libraryReturnUrl={libraryReturnUrl}
|
||||||
renderCustomStats={renderCustomStats}
|
theme={theme}
|
||||||
UIOptions={UIOptions}
|
name={name}
|
||||||
onPaste={onPaste}
|
renderCustomStats={renderCustomStats}
|
||||||
detectScroll={detectScroll}
|
UIOptions={UIOptions}
|
||||||
handleKeyboardGlobally={handleKeyboardGlobally}
|
onPaste={onPaste}
|
||||||
onLibraryChange={onLibraryChange}
|
detectScroll={detectScroll}
|
||||||
autoFocus={autoFocus}
|
handleKeyboardGlobally={handleKeyboardGlobally}
|
||||||
generateIdForFile={generateIdForFile}
|
onLibraryChange={onLibraryChange}
|
||||||
onLinkOpen={onLinkOpen}
|
autoFocus={autoFocus}
|
||||||
generateLinkForSelection={generateLinkForSelection}
|
generateIdForFile={generateIdForFile}
|
||||||
onPointerDown={onPointerDown}
|
onLinkOpen={onLinkOpen}
|
||||||
onPointerUp={onPointerUp}
|
generateLinkForSelection={generateLinkForSelection}
|
||||||
onScrollChange={onScrollChange}
|
onPointerDown={onPointerDown}
|
||||||
onDuplicate={onDuplicate}
|
onPointerUp={onPointerUp}
|
||||||
validateEmbeddable={validateEmbeddable}
|
onScrollChange={onScrollChange}
|
||||||
renderEmbeddable={renderEmbeddable}
|
onDuplicate={onDuplicate}
|
||||||
aiEnabled={aiEnabled !== false}
|
validateEmbeddable={validateEmbeddable}
|
||||||
showDeprecatedFonts={showDeprecatedFonts}
|
renderEmbeddable={renderEmbeddable}
|
||||||
>
|
aiEnabled={aiEnabled !== false}
|
||||||
{children}
|
showDeprecatedFonts={showDeprecatedFonts}
|
||||||
</App>
|
customOptions={customOptions}
|
||||||
</InitializeApp>
|
>
|
||||||
</EditorJotaiProvider>
|
{children}
|
||||||
|
</App>
|
||||||
|
</InitializeApp>
|
||||||
|
</EditorJotaiProvider>
|
||||||
|
</ExcalidrawPropsCustomOptionsContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -129,7 +129,7 @@
|
||||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||||
"homepage": "https://github.com/excalidraw/excalidraw/tree/master/packages/excalidraw",
|
"homepage": "https://github.com/excalidraw/excalidraw/tree/master/packages/excalidraw",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"gen:types": "rm -rf types && tsc",
|
"gen:types": "rimraf -rf types && tsc",
|
||||||
"build:esm": "rm -rf dist && node ../../scripts/buildPackage.js && yarn gen:types"
|
"build:esm": "rimraf -rf dist && node ../../scripts/buildPackage.js && yarn gen:types"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
type GlobalPoint,
|
type GlobalPoint,
|
||||||
} from "@excalidraw/math";
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
import { TOOL_TYPE, KEYS } from "@excalidraw/common";
|
import { TOOL_TYPE, KEYS, shouldSnapping } from "@excalidraw/common";
|
||||||
import {
|
import {
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
getDraggedElementsBounds,
|
getDraggedElementsBounds,
|
||||||
|
@ -173,9 +173,9 @@ export const isSnappingEnabled = ({
|
||||||
}) => {
|
}) => {
|
||||||
if (event) {
|
if (event) {
|
||||||
return (
|
return (
|
||||||
(app.state.objectsSnapModeEnabled && !event[KEYS.CTRL_OR_CMD]) ||
|
(app.state.objectsSnapModeEnabled && !shouldSnapping(event)) ||
|
||||||
(!app.state.objectsSnapModeEnabled &&
|
(!app.state.objectsSnapModeEnabled &&
|
||||||
event[KEYS.CTRL_OR_CMD] &&
|
shouldSnapping(event) &&
|
||||||
!isGridModeEnabled(app))
|
!isGridModeEnabled(app))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
|
import { createContext, type JSX } from "react";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
IMAGE_MIME_TYPES,
|
IMAGE_MIME_TYPES,
|
||||||
UserIdleState,
|
UserIdleState,
|
||||||
throttleRAF,
|
throttleRAF,
|
||||||
MIME_TYPES,
|
MIME_TYPES,
|
||||||
|
ColorPaletteCustom,
|
||||||
|
ColorTuple,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import type { SuggestedBinding } from "@excalidraw/element/binding";
|
import type { SuggestedBinding } from "@excalidraw/element/binding";
|
||||||
|
@ -43,7 +47,9 @@ import type {
|
||||||
MakeBrand,
|
MakeBrand,
|
||||||
} from "@excalidraw/common/utility-types";
|
} from "@excalidraw/common/utility-types";
|
||||||
|
|
||||||
import type { Action } from "./actions/types";
|
import type { ColorPickerType } from "./components/ColorPicker/colorPickerUtils";
|
||||||
|
|
||||||
|
import type { Action, ActionResult } from "./actions/types";
|
||||||
import type { Spreadsheet } from "./charts";
|
import type { Spreadsheet } from "./charts";
|
||||||
import type { ClipboardData } from "./clipboard";
|
import type { ClipboardData } from "./clipboard";
|
||||||
import type App from "./components/App";
|
import type App from "./components/App";
|
||||||
|
@ -57,7 +63,8 @@ import type { ImportedDataState } from "./data/types";
|
||||||
import type { Language } from "./i18n";
|
import type { Language } from "./i18n";
|
||||||
import type { isOverScrollBars } from "./scene/scrollbars";
|
import type { isOverScrollBars } from "./scene/scrollbars";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import type { JSX } from "react";
|
import type { ButtonIconSelectProps } from "./components/ButtonIconSelect";
|
||||||
|
import type { ButtonIcon } from "./components/ButtonIcon";
|
||||||
|
|
||||||
export type SocketId = string & { _brand: "SocketId" };
|
export type SocketId = string & { _brand: "SocketId" };
|
||||||
|
|
||||||
|
@ -512,6 +519,83 @@ export type OnUserFollowedPayload = {
|
||||||
action: "FOLLOW" | "UNFOLLOW";
|
action: "FOLLOW" | "UNFOLLOW";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ExcalidrawPropsCustomOptions {
|
||||||
|
disableKeyEvents?: boolean;
|
||||||
|
hideMainToolbar?: boolean;
|
||||||
|
hideMenu?: boolean;
|
||||||
|
hideFooter?: boolean;
|
||||||
|
hideContextMenu?: boolean;
|
||||||
|
shouldResizeFromCenter?: (event: MouseEvent | KeyboardEvent) => boolean;
|
||||||
|
shouldMaintainAspectRatio?: (event: MouseEvent | KeyboardEvent) => boolean;
|
||||||
|
shouldRotateWithDiscreteAngle?: (
|
||||||
|
event: MouseEvent | KeyboardEvent | React.PointerEvent<HTMLCanvasElement>,
|
||||||
|
) => boolean;
|
||||||
|
shouldSnapping?: (event: KeyboardModifiersObject) => boolean;
|
||||||
|
layoutRenders?: {
|
||||||
|
menuRender?: (props: { children: React.ReactNode }) => React.ReactNode;
|
||||||
|
};
|
||||||
|
pickerRenders?: {
|
||||||
|
ButtonList?: React.ComponentType<{ children: React.ReactNode }>;
|
||||||
|
elementStrokeColors?: ColorTuple;
|
||||||
|
elementBackgroundColors?: ColorTuple;
|
||||||
|
buttonIconSelectRender?: <T extends Object>(
|
||||||
|
props: ButtonIconSelectProps<T>,
|
||||||
|
) => JSX.Element;
|
||||||
|
buttonIconSelectRadioRender?: (props: {
|
||||||
|
key: string;
|
||||||
|
active: boolean;
|
||||||
|
title: string;
|
||||||
|
name: string;
|
||||||
|
onChange: () => void;
|
||||||
|
checked: boolean;
|
||||||
|
dataTestid?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
value: any;
|
||||||
|
}) => JSX.Element;
|
||||||
|
CustomButtonIcon?: typeof ButtonIcon;
|
||||||
|
layerButtonRender?: (props: {
|
||||||
|
onClick: () => void;
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
name: string;
|
||||||
|
}) => JSX.Element;
|
||||||
|
rangeRender?: (props: {
|
||||||
|
value: number;
|
||||||
|
onChange: (value: number) => void;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step: number;
|
||||||
|
}) => React.ReactNode;
|
||||||
|
colorPickerTopPickesButtonRender?: (props: {
|
||||||
|
active: boolean;
|
||||||
|
color: string;
|
||||||
|
isTransparent: boolean;
|
||||||
|
hasOutline: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
dataTestid: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
key: string;
|
||||||
|
}) => React.ReactNode;
|
||||||
|
colorPickerPopoverRender?: (props: {
|
||||||
|
color: string;
|
||||||
|
label: string;
|
||||||
|
type: ColorPickerType;
|
||||||
|
onChange: (color: string) => void;
|
||||||
|
elements: readonly ExcalidrawElement[];
|
||||||
|
palette: ColorPaletteCustom | null;
|
||||||
|
updateData: (formData?: any) => void;
|
||||||
|
}) => React.ReactNode;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExcalidrawPropsCustomOptionsContext = createContext<
|
||||||
|
ExcalidrawPropsCustomOptions | undefined
|
||||||
|
>({});
|
||||||
|
|
||||||
|
export interface ExcalidrawActionType {
|
||||||
|
syncActionResult: (actionResult: ActionResult) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExcalidrawProps {
|
export interface ExcalidrawProps {
|
||||||
onChange?: (
|
onChange?: (
|
||||||
elements: readonly OrderedExcalidrawElement[],
|
elements: readonly OrderedExcalidrawElement[],
|
||||||
|
@ -601,6 +685,8 @@ export interface ExcalidrawProps {
|
||||||
) => JSX.Element | null;
|
) => JSX.Element | null;
|
||||||
aiEnabled?: boolean;
|
aiEnabled?: boolean;
|
||||||
showDeprecatedFonts?: boolean;
|
showDeprecatedFonts?: boolean;
|
||||||
|
customOptions?: ExcalidrawPropsCustomOptions;
|
||||||
|
actionRef?: React.RefObject<ExcalidrawActionType | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SceneData = {
|
export type SceneData = {
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||||
"repository": "https://github.com/excalidraw/excalidraw",
|
"repository": "https://github.com/excalidraw/excalidraw",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"gen:types": "rm -rf types && tsc",
|
"gen:types": "rimraf -rf types && tsc",
|
||||||
"build:esm": "rm -rf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
"build:esm": "rimraf -rf dist && node ../../scripts/buildBase.js && yarn gen:types"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
"bugs": "https://github.com/excalidraw/excalidraw/issues",
|
||||||
"repository": "https://github.com/excalidraw/excalidraw",
|
"repository": "https://github.com/excalidraw/excalidraw",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"gen:types": "rm -rf types && tsc",
|
"gen:types": "rimraf -rf types && tsc",
|
||||||
"build:esm": "rm -rf dist && node ../../scripts/buildUtils.js && yarn gen:types"
|
"build:esm": "rimraf -rf dist && node ../../scripts/buildUtils.js && yarn gen:types"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue