mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Merge pull request #1 from mg-chao/feats/20250416_custom
为 Snow Shot 提供自定义支持
This commit is contained in:
commit
4a6039b9a7
52 changed files with 1997 additions and 765 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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -438,6 +438,7 @@ export const TOOL_TYPE = {
|
||||||
magicframe: "magicframe",
|
magicframe: "magicframe",
|
||||||
embeddable: "embeddable",
|
embeddable: "embeddable",
|
||||||
laser: "laser",
|
laser: "laser",
|
||||||
|
blur: "blur",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const EDITOR_LS_KEYS = {
|
export const EDITOR_LS_KEYS = {
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,6 +98,7 @@ export const generateRoughOptions = (
|
||||||
case "iframe":
|
case "iframe":
|
||||||
case "embeddable":
|
case "embeddable":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
|
case "blur":
|
||||||
case "ellipse": {
|
case "ellipse": {
|
||||||
options.fillStyle = element.fillStyle;
|
options.fillStyle = element.fillStyle;
|
||||||
options.fill = isTransparent(element.backgroundColor)
|
options.fill = isTransparent(element.backgroundColor)
|
||||||
|
@ -326,6 +327,7 @@ export const _generateElementShape = (
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "iframe":
|
case "iframe":
|
||||||
|
case "blur":
|
||||||
case "embeddable": {
|
case "embeddable": {
|
||||||
let shape: ElementShapes[typeof element.type];
|
let shape: ElementShapes[typeof element.type];
|
||||||
// this is for rendering the stroke/bg of the embeddable, especially
|
// this is for rendering the stroke/bg of the embeddable, especially
|
||||||
|
|
|
@ -10,7 +10,10 @@ export const hasBackground = (type: ElementOrToolType) =>
|
||||||
type === "freedraw";
|
type === "freedraw";
|
||||||
|
|
||||||
export const hasStrokeColor = (type: ElementOrToolType) =>
|
export const hasStrokeColor = (type: ElementOrToolType) =>
|
||||||
type !== "image" && type !== "frame" && type !== "magicframe";
|
type !== "image" &&
|
||||||
|
type !== "frame" &&
|
||||||
|
type !== "magicframe" &&
|
||||||
|
type !== "blur";
|
||||||
|
|
||||||
export const hasStrokeWidth = (type: ElementOrToolType) =>
|
export const hasStrokeWidth = (type: ElementOrToolType) =>
|
||||||
type === "rectangle" ||
|
type === "rectangle" ||
|
||||||
|
@ -39,6 +42,10 @@ export const canChangeRoundness = (type: ElementOrToolType) =>
|
||||||
type === "diamond" ||
|
type === "diamond" ||
|
||||||
type === "image";
|
type === "image";
|
||||||
|
|
||||||
|
export const canChangeBlur = (type: ElementOrToolType) => type === "blur";
|
||||||
|
|
||||||
|
export const canChangeLayer = (type: ElementOrToolType) => type !== "blur";
|
||||||
|
|
||||||
export const toolIsArrow = (type: ElementOrToolType) => type === "arrow";
|
export const toolIsArrow = (type: ElementOrToolType) => type === "arrow";
|
||||||
|
|
||||||
export const canHaveArrowheads = (type: ElementOrToolType) => type === "arrow";
|
export const canHaveArrowheads = (type: ElementOrToolType) => type === "arrow";
|
||||||
|
|
|
@ -46,6 +46,7 @@ import type {
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
FixedSegment,
|
FixedSegment,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
|
ExcalidrawBlurElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export type ElementConstructorOpts = MarkOptional<
|
export type ElementConstructorOpts = MarkOptional<
|
||||||
|
@ -212,6 +213,25 @@ export const newMagicFrameElement = (
|
||||||
return frameElement;
|
return frameElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const newBlurElement = (
|
||||||
|
opts: {
|
||||||
|
blur: number;
|
||||||
|
} & ElementConstructorOpts,
|
||||||
|
): NonDeleted<ExcalidrawBlurElement> => {
|
||||||
|
const blurElement = newElementWith(
|
||||||
|
{
|
||||||
|
..._newElementBase<ExcalidrawBlurElement>("blur", opts),
|
||||||
|
type: "blur",
|
||||||
|
blur: opts.blur,
|
||||||
|
fillStyle: "solid",
|
||||||
|
backgroundColor: "#000000",
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
return blurElement;
|
||||||
|
};
|
||||||
|
|
||||||
/** computes element x/y offset based on textAlign/verticalAlign */
|
/** computes element x/y offset based on textAlign/verticalAlign */
|
||||||
const getTextElementPositionOffsets = (
|
const getTextElementPositionOffsets = (
|
||||||
opts: {
|
opts: {
|
||||||
|
|
|
@ -404,6 +404,8 @@ const drawElementOnCanvas = (
|
||||||
rc.draw(ShapeCache.get(element)!);
|
rc.draw(ShapeCache.get(element)!);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "blur":
|
||||||
|
break;
|
||||||
case "arrow":
|
case "arrow":
|
||||||
case "line": {
|
case "line": {
|
||||||
context.lineJoin = "round";
|
context.lineJoin = "round";
|
||||||
|
@ -814,6 +816,7 @@ export const renderElement = (
|
||||||
case "image":
|
case "image":
|
||||||
case "text":
|
case "text":
|
||||||
case "iframe":
|
case "iframe":
|
||||||
|
case "blur":
|
||||||
case "embeddable": {
|
case "embeddable": {
|
||||||
// TODO investigate if we can do this in situ. Right now we need to call
|
// TODO investigate if we can do this in situ. Right now we need to call
|
||||||
// beforehand because math helpers (such as getElementAbsoluteCoords)
|
// beforehand because math helpers (such as getElementAbsoluteCoords)
|
||||||
|
|
|
@ -58,6 +58,7 @@ export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
|
||||||
case "embeddable":
|
case "embeddable":
|
||||||
case "image":
|
case "image":
|
||||||
case "iframe":
|
case "iframe":
|
||||||
|
case "blur":
|
||||||
case "text":
|
case "text":
|
||||||
case "selection":
|
case "selection":
|
||||||
return getPolygonShape(element);
|
return getPolygonShape(element);
|
||||||
|
|
|
@ -28,6 +28,7 @@ import type {
|
||||||
PointBinding,
|
PointBinding,
|
||||||
FixedPointBinding,
|
FixedPointBinding,
|
||||||
ExcalidrawFlowchartNodeElement,
|
ExcalidrawFlowchartNodeElement,
|
||||||
|
ExcalidrawBlurElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const isInitializedImageElement = (
|
export const isInitializedImageElement = (
|
||||||
|
@ -107,6 +108,12 @@ export const isLinearElement = (
|
||||||
return element != null && isLinearElementType(element.type);
|
return element != null && isLinearElementType(element.type);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isBlurElement = (
|
||||||
|
element?: ExcalidrawElement | null,
|
||||||
|
): element is ExcalidrawBlurElement => {
|
||||||
|
return element != null && isBlurElementType(element.type);
|
||||||
|
};
|
||||||
|
|
||||||
export const isArrowElement = (
|
export const isArrowElement = (
|
||||||
element?: ExcalidrawElement | null,
|
element?: ExcalidrawElement | null,
|
||||||
): element is ExcalidrawArrowElement => {
|
): element is ExcalidrawArrowElement => {
|
||||||
|
@ -127,6 +134,10 @@ export const isLinearElementType = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isBlurElementType = (elementType: ElementOrToolType): boolean => {
|
||||||
|
return elementType === "blur";
|
||||||
|
};
|
||||||
|
|
||||||
export const isBindingElement = (
|
export const isBindingElement = (
|
||||||
element?: ExcalidrawElement | null,
|
element?: ExcalidrawElement | null,
|
||||||
includeLocked = true,
|
includeLocked = true,
|
||||||
|
@ -231,6 +242,7 @@ export const isExcalidrawElement = (
|
||||||
case "frame":
|
case "frame":
|
||||||
case "magicframe":
|
case "magicframe":
|
||||||
case "image":
|
case "image":
|
||||||
|
case "blur":
|
||||||
case "selection": {
|
case "selection": {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,11 @@ export type ExcalidrawRectangleElement = _ExcalidrawElementBase & {
|
||||||
type: "rectangle";
|
type: "rectangle";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ExcalidrawBlurElement = _ExcalidrawElementBase & {
|
||||||
|
type: "blur";
|
||||||
|
blur: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type ExcalidrawDiamondElement = _ExcalidrawElementBase & {
|
export type ExcalidrawDiamondElement = _ExcalidrawElementBase & {
|
||||||
type: "diamond";
|
type: "diamond";
|
||||||
};
|
};
|
||||||
|
@ -212,7 +217,8 @@ export type ExcalidrawElement =
|
||||||
| ExcalidrawFrameElement
|
| ExcalidrawFrameElement
|
||||||
| ExcalidrawMagicFrameElement
|
| ExcalidrawMagicFrameElement
|
||||||
| ExcalidrawIframeElement
|
| ExcalidrawIframeElement
|
||||||
| ExcalidrawEmbeddableElement;
|
| ExcalidrawEmbeddableElement
|
||||||
|
| ExcalidrawBlurElement;
|
||||||
|
|
||||||
export type ExcalidrawNonSelectionElement = Exclude<
|
export type ExcalidrawNonSelectionElement = Exclude<
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { KEYS, arrayToMap, getShortcutKey } from "@excalidraw/common";
|
||||||
|
|
||||||
import { alignElements } from "@excalidraw/element/align";
|
import { alignElements } from "@excalidraw/element/align";
|
||||||
|
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type { Alignment } from "@excalidraw/element/align";
|
import type { Alignment } from "@excalidraw/element/align";
|
||||||
|
@ -27,9 +29,14 @@ import { t } from "../i18n";
|
||||||
import { isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { CaptureUpdateAction } from "../store";
|
import { CaptureUpdateAction } from "../store";
|
||||||
|
|
||||||
import { register } from "./register";
|
import {
|
||||||
|
ExcalidrawPropsCustomOptionsContext,
|
||||||
|
type AppClassProperties,
|
||||||
|
type AppState,
|
||||||
|
type UIAppState,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
import type { AppClassProperties, AppState, UIAppState } from "../types";
|
import { register } from "./register";
|
||||||
|
|
||||||
export const alignActionsPredicate = (
|
export const alignActionsPredicate = (
|
||||||
appState: UIAppState,
|
appState: UIAppState,
|
||||||
|
@ -81,7 +88,24 @@ export const actionAlignTop = register({
|
||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
|
onClick: () => updateData(null),
|
||||||
|
title: `${t("labels.alignTop")}`,
|
||||||
|
children: AlignTopIcon,
|
||||||
|
name: "alignTop",
|
||||||
|
visible: isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
),
|
||||||
|
hidden: !alignActionsPredicate(appState, app),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(appState, app)}
|
hidden={!alignActionsPredicate(appState, app)}
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -91,9 +115,13 @@ export const actionAlignTop = register({
|
||||||
"CtrlOrCmd+Shift+Up",
|
"CtrlOrCmd+Shift+Up",
|
||||||
)}`}
|
)}`}
|
||||||
aria-label={t("labels.alignTop")}
|
aria-label={t("labels.alignTop")}
|
||||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
visible={isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionAlignBottom = register({
|
export const actionAlignBottom = register({
|
||||||
|
@ -115,7 +143,24 @@ export const actionAlignBottom = register({
|
||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
|
onClick: () => updateData(null),
|
||||||
|
title: `${t("labels.alignBottom")}`,
|
||||||
|
children: AlignBottomIcon,
|
||||||
|
name: "alignBottom",
|
||||||
|
visible: isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
),
|
||||||
|
hidden: !alignActionsPredicate(appState, app),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(appState, app)}
|
hidden={!alignActionsPredicate(appState, app)}
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -125,9 +170,13 @@ export const actionAlignBottom = register({
|
||||||
"CtrlOrCmd+Shift+Down",
|
"CtrlOrCmd+Shift+Down",
|
||||||
)}`}
|
)}`}
|
||||||
aria-label={t("labels.alignBottom")}
|
aria-label={t("labels.alignBottom")}
|
||||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
visible={isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionAlignLeft = register({
|
export const actionAlignLeft = register({
|
||||||
|
@ -149,7 +198,24 @@ export const actionAlignLeft = register({
|
||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
|
onClick: () => updateData(null),
|
||||||
|
title: `${t("labels.alignLeft")}`,
|
||||||
|
children: AlignLeftIcon,
|
||||||
|
name: "alignLeft",
|
||||||
|
visible: isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
),
|
||||||
|
hidden: !alignActionsPredicate(appState, app),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(appState, app)}
|
hidden={!alignActionsPredicate(appState, app)}
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -159,9 +225,13 @@ export const actionAlignLeft = register({
|
||||||
"CtrlOrCmd+Shift+Left",
|
"CtrlOrCmd+Shift+Left",
|
||||||
)}`}
|
)}`}
|
||||||
aria-label={t("labels.alignLeft")}
|
aria-label={t("labels.alignLeft")}
|
||||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
visible={isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionAlignRight = register({
|
export const actionAlignRight = register({
|
||||||
|
@ -183,7 +253,23 @@ export const actionAlignRight = register({
|
||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
|
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
|
onClick: () => updateData(null),
|
||||||
|
title: `${t("labels.alignRight")}`,
|
||||||
|
children: AlignRightIcon,
|
||||||
|
name: "alignRight",
|
||||||
|
visible: isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
),
|
||||||
|
hidden: !alignActionsPredicate(appState, app),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(appState, app)}
|
hidden={!alignActionsPredicate(appState, app)}
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -193,9 +279,13 @@ export const actionAlignRight = register({
|
||||||
"CtrlOrCmd+Shift+Right",
|
"CtrlOrCmd+Shift+Right",
|
||||||
)}`}
|
)}`}
|
||||||
aria-label={t("labels.alignRight")}
|
aria-label={t("labels.alignRight")}
|
||||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
visible={isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionAlignVerticallyCentered = register({
|
export const actionAlignVerticallyCentered = register({
|
||||||
|
@ -215,7 +305,24 @@ export const actionAlignVerticallyCentered = register({
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
|
onClick: () => updateData(null),
|
||||||
|
title: `${t("labels.centerVertically")}`,
|
||||||
|
children: CenterVerticallyIcon,
|
||||||
|
name: "alignVerticallyCentered",
|
||||||
|
visible: isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
),
|
||||||
|
hidden: !alignActionsPredicate(appState, app),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(appState, app)}
|
hidden={!alignActionsPredicate(appState, app)}
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -223,9 +330,13 @@ export const actionAlignVerticallyCentered = register({
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
title={t("labels.centerVertically")}
|
title={t("labels.centerVertically")}
|
||||||
aria-label={t("labels.centerVertically")}
|
aria-label={t("labels.centerVertically")}
|
||||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
visible={isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionAlignHorizontallyCentered = register({
|
export const actionAlignHorizontallyCentered = register({
|
||||||
|
@ -245,7 +356,24 @@ export const actionAlignHorizontallyCentered = register({
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
|
onClick: () => updateData(null),
|
||||||
|
title: `${t("labels.centerHorizontally")}`,
|
||||||
|
children: CenterHorizontallyIcon,
|
||||||
|
name: "alignHorizontallyCentered",
|
||||||
|
visible: isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
),
|
||||||
|
hidden: !alignActionsPredicate(appState, app),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!alignActionsPredicate(appState, app)}
|
hidden={!alignActionsPredicate(appState, app)}
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -253,7 +381,11 @@ export const actionAlignHorizontallyCentered = register({
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
title={t("labels.centerHorizontally")}
|
title={t("labels.centerHorizontally")}
|
||||||
aria-label={t("labels.centerHorizontally")}
|
aria-label={t("labels.centerHorizontally")}
|
||||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
visible={isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
48
packages/excalidraw/actions/actionBlur.tsx
Normal file
48
packages/excalidraw/actions/actionBlur.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { newElementWith } from "@excalidraw/element/mutateElement";
|
||||||
|
|
||||||
|
// TODO barnabasmolnar/editor-redesign
|
||||||
|
// TextAlignTopIcon, TextAlignBottomIcon,TextAlignMiddleIcon,
|
||||||
|
// ArrowHead icons
|
||||||
|
import { isBlurElement } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
|
import { BlurRange } from "../components/Range";
|
||||||
|
|
||||||
|
import { CaptureUpdateAction } from "../store";
|
||||||
|
|
||||||
|
import { register } from "./register";
|
||||||
|
|
||||||
|
import { changeProperty } from "./actionProperties";
|
||||||
|
|
||||||
|
export const actionChangeBlur = register({
|
||||||
|
name: "changeBlur",
|
||||||
|
label: "labels.blur",
|
||||||
|
trackEvent: false,
|
||||||
|
perform: (elements, appState, value) => {
|
||||||
|
return {
|
||||||
|
elements: changeProperty(
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
(el) => {
|
||||||
|
if (!isBlurElement(el)) {
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newElementWith(el, {
|
||||||
|
blur: value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
appState: { ...appState, currentItemBlur: value },
|
||||||
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
|
<BlurRange
|
||||||
|
updateData={updateData}
|
||||||
|
elements={elements}
|
||||||
|
appState={appState}
|
||||||
|
testId="blur"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
|
@ -17,6 +17,8 @@ import {
|
||||||
selectGroupsForSelectedElements,
|
selectGroupsForSelectedElements,
|
||||||
} from "@excalidraw/element/groups";
|
} from "@excalidraw/element/groups";
|
||||||
|
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
@ -25,6 +27,8 @@ import { CaptureUpdateAction } from "../store";
|
||||||
import { TrashIcon } from "../components/icons";
|
import { TrashIcon } from "../components/icons";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
|
|
||||||
|
import { ExcalidrawPropsCustomOptionsContext } from "../types";
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
import type { AppClassProperties, AppState } from "../types";
|
import type { AppClassProperties, AppState } from "../types";
|
||||||
|
@ -310,14 +314,34 @@ export const actionDeleteSelected = register({
|
||||||
keyTest: (event, appState, elements) =>
|
keyTest: (event, appState, elements) =>
|
||||||
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) &&
|
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) &&
|
||||||
!event[KEYS.CTRL_OR_CMD],
|
!event[KEYS.CTRL_OR_CMD],
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
|
onClick: () => updateData(null),
|
||||||
|
title: `${t("labels.delete")}`,
|
||||||
|
children: TrashIcon,
|
||||||
|
name: "deleteSelectedElements",
|
||||||
|
visible: isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={TrashIcon}
|
icon={TrashIcon}
|
||||||
title={t("labels.delete")}
|
title={t("labels.delete")}
|
||||||
aria-label={t("labels.delete")}
|
aria-label={t("labels.delete")}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
visible={isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element/fra
|
||||||
|
|
||||||
import { distributeElements } from "@excalidraw/element/distribute";
|
import { distributeElements } from "@excalidraw/element/distribute";
|
||||||
|
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type { Distribution } from "@excalidraw/element/distribute";
|
import type { Distribution } from "@excalidraw/element/distribute";
|
||||||
|
@ -23,9 +25,13 @@ import { t } from "../i18n";
|
||||||
import { isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { CaptureUpdateAction } from "../store";
|
import { CaptureUpdateAction } from "../store";
|
||||||
|
|
||||||
import { register } from "./register";
|
import {
|
||||||
|
ExcalidrawPropsCustomOptionsContext,
|
||||||
|
type AppClassProperties,
|
||||||
|
type AppState,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
import type { AppClassProperties, AppState } from "../types";
|
import { register } from "./register";
|
||||||
|
|
||||||
const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
|
const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
@ -75,7 +81,24 @@ export const distributeHorizontally = register({
|
||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H,
|
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H,
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
|
onClick: () => updateData(null),
|
||||||
|
title: `${t("labels.distributeHorizontally")}`,
|
||||||
|
children: DistributeHorizontallyIcon,
|
||||||
|
name: "distributeHorizontally",
|
||||||
|
visible: isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
),
|
||||||
|
hidden: !enableActionGroup(appState, app),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(appState, app)}
|
hidden={!enableActionGroup(appState, app)}
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -85,9 +108,13 @@ export const distributeHorizontally = register({
|
||||||
"Alt+H",
|
"Alt+H",
|
||||||
)}`}
|
)}`}
|
||||||
aria-label={t("labels.distributeHorizontally")}
|
aria-label={t("labels.distributeHorizontally")}
|
||||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
visible={isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const distributeVertically = register({
|
export const distributeVertically = register({
|
||||||
|
@ -106,15 +133,38 @@ export const distributeVertically = register({
|
||||||
},
|
},
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
|
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
|
onClick: () => updateData(null),
|
||||||
|
title: `${t("labels.distributeVertically")}`,
|
||||||
|
children: DistributeVerticallyIcon,
|
||||||
|
name: "distributeVertically",
|
||||||
|
visible: isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
),
|
||||||
|
hidden: !enableActionGroup(appState, app),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(appState, app)}
|
hidden={!enableActionGroup(appState, app)}
|
||||||
type="button"
|
type="button"
|
||||||
icon={DistributeVerticallyIcon}
|
icon={DistributeVerticallyIcon}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
title={`${t("labels.distributeVertically")} — ${getShortcutKey("Alt+V")}`}
|
title={`${t("labels.distributeVertically")} — ${getShortcutKey(
|
||||||
|
"Alt+V",
|
||||||
|
)}`}
|
||||||
aria-label={t("labels.distributeVertically")}
|
aria-label={t("labels.distributeVertically")}
|
||||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
visible={isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,6 +18,8 @@ import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
||||||
|
|
||||||
import { duplicateElements } from "@excalidraw/element/duplicate";
|
import { duplicateElements } from "@excalidraw/element/duplicate";
|
||||||
|
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { DuplicateIcon } from "../components/icons";
|
import { DuplicateIcon } from "../components/icons";
|
||||||
|
|
||||||
|
@ -25,6 +27,8 @@ import { t } from "../i18n";
|
||||||
import { isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { CaptureUpdateAction } from "../store";
|
import { CaptureUpdateAction } from "../store";
|
||||||
|
|
||||||
|
import { ExcalidrawPropsCustomOptionsContext } from "../types";
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
export const actionDuplicateSelection = register({
|
export const actionDuplicateSelection = register({
|
||||||
|
@ -105,7 +109,23 @@ export const actionDuplicateSelection = register({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.D,
|
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.D,
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
|
onClick: () => updateData(null),
|
||||||
|
title: `${t("labels.duplicateSelection")}`,
|
||||||
|
children: DuplicateIcon,
|
||||||
|
name: "duplicateSelection",
|
||||||
|
visible: isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={DuplicateIcon}
|
icon={DuplicateIcon}
|
||||||
|
@ -114,7 +134,11 @@ export const actionDuplicateSelection = register({
|
||||||
)}`}
|
)}`}
|
||||||
aria-label={t("labels.duplicateSelection")}
|
aria-label={t("labels.duplicateSelection")}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
visible={isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,6 +28,8 @@ import {
|
||||||
|
|
||||||
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
||||||
|
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
|
@ -42,9 +44,13 @@ import { t } from "../i18n";
|
||||||
import { isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { CaptureUpdateAction } from "../store";
|
import { CaptureUpdateAction } from "../store";
|
||||||
|
|
||||||
import { register } from "./register";
|
import {
|
||||||
|
ExcalidrawPropsCustomOptionsContext,
|
||||||
|
type AppClassProperties,
|
||||||
|
type AppState,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
import type { AppClassProperties, AppState } from "../types";
|
import { register } from "./register";
|
||||||
|
|
||||||
const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
|
const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
|
||||||
if (elements.length >= 2) {
|
if (elements.length >= 2) {
|
||||||
|
@ -195,7 +201,24 @@ export const actionGroup = register({
|
||||||
enableActionGroup(elements, appState, app),
|
enableActionGroup(elements, appState, app),
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
|
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
|
||||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
|
onClick: () => updateData(null),
|
||||||
|
title: `${t("labels.group")}`,
|
||||||
|
children: <GroupIcon theme={appState.theme} />,
|
||||||
|
name: "group",
|
||||||
|
visible: isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
),
|
||||||
|
hidden: !enableActionGroup(elements, appState, app),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
hidden={!enableActionGroup(elements, appState, app)}
|
hidden={!enableActionGroup(elements, appState, app)}
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -203,9 +226,13 @@ export const actionGroup = register({
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
title={`${t("labels.group")} — ${getShortcutKey("CtrlOrCmd+G")}`}
|
title={`${t("labels.group")} — ${getShortcutKey("CtrlOrCmd+G")}`}
|
||||||
aria-label={t("labels.group")}
|
aria-label={t("labels.group")}
|
||||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
visible={isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
)}
|
||||||
></ToolButton>
|
></ToolButton>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionUngroup = register({
|
export const actionUngroup = register({
|
||||||
|
@ -304,15 +331,38 @@ export const actionUngroup = register({
|
||||||
event.key === KEYS.G.toUpperCase(),
|
event.key === KEYS.G.toUpperCase(),
|
||||||
predicate: (elements, appState) => getSelectedGroupIds(appState).length > 0,
|
predicate: (elements, appState) => getSelectedGroupIds(appState).length > 0,
|
||||||
|
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
|
onClick: () => updateData(null),
|
||||||
|
title: `${t("labels.ungroup")}`,
|
||||||
|
children: <UngroupIcon theme={appState.theme} />,
|
||||||
|
name: "ungroup",
|
||||||
|
visible: isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
),
|
||||||
|
hidden: getSelectedGroupIds(appState).length === 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
hidden={getSelectedGroupIds(appState).length === 0}
|
hidden={getSelectedGroupIds(appState).length === 0}
|
||||||
icon={<UngroupIcon theme={appState.theme} />}
|
icon={<UngroupIcon theme={appState.theme} />}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
title={`${t("labels.ungroup")} — ${getShortcutKey("CtrlOrCmd+Shift+G")}`}
|
title={`${t("labels.ungroup")} — ${getShortcutKey(
|
||||||
|
"CtrlOrCmd+Shift+G",
|
||||||
|
)}`}
|
||||||
aria-label={t("labels.ungroup")}
|
aria-label={t("labels.ungroup")}
|
||||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
visible={isSomeElementSelected(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
)}
|
||||||
></ToolButton>
|
></ToolButton>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { isWindows, KEYS, matchKey, arrayToMap } from "@excalidraw/common";
|
import { isWindows, KEYS, matchKey, arrayToMap } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { useContext, useEffect } from "react";
|
||||||
|
|
||||||
import type { SceneElementsMap } from "@excalidraw/element/types";
|
import type { SceneElementsMap } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
|
@ -9,9 +11,14 @@ import { useEmitter } from "../hooks/useEmitter";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { CaptureUpdateAction } from "../store";
|
import { CaptureUpdateAction } from "../store";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ExcalidrawHistoryContext,
|
||||||
|
type AppClassProperties,
|
||||||
|
type AppState,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
import type { History } from "../history";
|
import type { History } from "../history";
|
||||||
import type { Store } from "../store";
|
import type { Store } from "../store";
|
||||||
import type { AppClassProperties, AppState } from "../types";
|
|
||||||
import type { Action, ActionResult } from "./types";
|
import type { Action, ActionResult } from "./types";
|
||||||
|
|
||||||
const executeHistoryAction = (
|
const executeHistoryAction = (
|
||||||
|
@ -74,6 +81,12 @@ export const createUndoAction: ActionCreator = (history, store) => ({
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const historyContext = useContext(ExcalidrawHistoryContext);
|
||||||
|
historyContext &&
|
||||||
|
(historyContext.undoRef.current = () => {
|
||||||
|
updateData();
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -114,6 +127,12 @@ export const createRedoAction: ActionCreator = (history, store) => ({
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const historyContext = useContext(ExcalidrawHistoryContext);
|
||||||
|
historyContext &&
|
||||||
|
(historyContext.redoRef.current = () => {
|
||||||
|
updateData();
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { isEmbeddableElement } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
import { KEYS, getShortcutKey } from "@excalidraw/common";
|
import { KEYS, getShortcutKey } from "@excalidraw/common";
|
||||||
|
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import { getContextMenuLabel } from "../components/hyperlink/Hyperlink";
|
import { getContextMenuLabel } from "../components/hyperlink/Hyperlink";
|
||||||
import { LinkIcon } from "../components/icons";
|
import { LinkIcon } from "../components/icons";
|
||||||
|
@ -10,6 +12,8 @@ import { t } from "../i18n";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import { CaptureUpdateAction } from "../store";
|
import { CaptureUpdateAction } from "../store";
|
||||||
|
|
||||||
|
import { ExcalidrawPropsCustomOptionsContext } from "../types";
|
||||||
|
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
|
|
||||||
export const actionLink = register({
|
export const actionLink = register({
|
||||||
|
@ -40,6 +44,20 @@ export const actionLink = register({
|
||||||
PanelComponent: ({ elements, appState, updateData }) => {
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
|
onClick: () => updateData(null),
|
||||||
|
title: isEmbeddableElement(elements[0])
|
||||||
|
? t("labels.link.labelEmbed")
|
||||||
|
: t("labels.link.label"),
|
||||||
|
children: LinkIcon,
|
||||||
|
name: "link",
|
||||||
|
visible: selectedElements.length === 1 && !!selectedElements[0].link,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -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,
|
||||||
|
@ -133,10 +133,16 @@ import {
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { CaptureUpdateAction } from "../store";
|
import { CaptureUpdateAction } from "../store";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ExcalidrawPropsCustomOptionsContext,
|
||||||
|
type AppClassProperties,
|
||||||
|
type AppState,
|
||||||
|
type Primitive,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
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";
|
|
||||||
|
|
||||||
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
||||||
|
|
||||||
|
@ -318,11 +324,17 @@ export const actionChangeStrokeColor = register({
|
||||||
: CaptureUpdateAction.EVENTUALLY,
|
: CaptureUpdateAction.EVENTUALLY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, appProps }) => (
|
PanelComponent: ({ elements, appState, updateData, appProps }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
<h3 aria-hidden="true">{t("labels.stroke")}</h3>
|
<h3 aria-hidden="true">{t("labels.stroke")}</h3>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
topPicks={DEFAULT_ELEMENT_STROKE_PICKS}
|
topPicks={
|
||||||
|
customOptions?.pickerRenders?.elementStrokeColors ??
|
||||||
|
DEFAULT_ELEMENT_STROKE_PICKS
|
||||||
|
}
|
||||||
palette={DEFAULT_ELEMENT_STROKE_COLOR_PALETTE}
|
palette={DEFAULT_ELEMENT_STROKE_COLOR_PALETTE}
|
||||||
type="elementStroke"
|
type="elementStroke"
|
||||||
label={t("labels.stroke")}
|
label={t("labels.stroke")}
|
||||||
|
@ -339,7 +351,8 @@ export const actionChangeStrokeColor = register({
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeBackgroundColor = register({
|
export const actionChangeBackgroundColor = register({
|
||||||
|
@ -364,11 +377,17 @@ export const actionChangeBackgroundColor = register({
|
||||||
: CaptureUpdateAction.EVENTUALLY,
|
: CaptureUpdateAction.EVENTUALLY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData, appProps }) => (
|
PanelComponent: ({ elements, appState, updateData, appProps }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
<h3 aria-hidden="true">{t("labels.background")}</h3>
|
<h3 aria-hidden="true">{t("labels.background")}</h3>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
topPicks={DEFAULT_ELEMENT_BACKGROUND_PICKS}
|
topPicks={
|
||||||
|
customOptions?.pickerRenders?.elementBackgroundColors ??
|
||||||
|
DEFAULT_ELEMENT_BACKGROUND_PICKS
|
||||||
|
}
|
||||||
palette={DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE}
|
palette={DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE}
|
||||||
type="elementBackground"
|
type="elementBackground"
|
||||||
label={t("labels.background")}
|
label={t("labels.background")}
|
||||||
|
@ -379,13 +398,16 @@ export const actionChangeBackgroundColor = register({
|
||||||
true,
|
true,
|
||||||
appState.currentItemBackgroundColor,
|
appState.currentItemBackgroundColor,
|
||||||
)}
|
)}
|
||||||
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
|
onChange={(color) =>
|
||||||
|
updateData({ currentItemBackgroundColor: color })
|
||||||
|
}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionChangeFillStyle = register({
|
export const actionChangeFillStyle = register({
|
||||||
|
@ -1520,11 +1542,54 @@ export const actionChangeArrowhead = register({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
PanelComponent: ({ elements, appState, updateData }) => {
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
const isRTL = getLanguage().rtl;
|
const isRTL = getLanguage().rtl;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.arrowheads")}</legend>
|
<legend>{t("labels.arrowheads")}</legend>
|
||||||
|
{customOptions?.pickerRenders?.ButtonList && (
|
||||||
|
<customOptions.pickerRenders.ButtonList className="iconPickerList">
|
||||||
|
<IconPicker
|
||||||
|
label="arrowhead_start"
|
||||||
|
options={getArrowheadOptions(!isRTL)}
|
||||||
|
value={getFormValue<Arrowhead | null>(
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
(element) =>
|
||||||
|
isLinearElement(element) && canHaveArrowheads(element.type)
|
||||||
|
? element.startArrowhead
|
||||||
|
: appState.currentItemStartArrowhead,
|
||||||
|
true,
|
||||||
|
appState.currentItemStartArrowhead,
|
||||||
|
)}
|
||||||
|
onChange={(value) =>
|
||||||
|
updateData({ position: "start", type: value })
|
||||||
|
}
|
||||||
|
numberOfOptionsToAlwaysShow={4}
|
||||||
|
/>
|
||||||
|
<IconPicker
|
||||||
|
label="arrowhead_end"
|
||||||
|
group="arrowheads"
|
||||||
|
options={getArrowheadOptions(!!isRTL)}
|
||||||
|
value={getFormValue<Arrowhead | null>(
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
(element) =>
|
||||||
|
isLinearElement(element) && canHaveArrowheads(element.type)
|
||||||
|
? element.endArrowhead
|
||||||
|
: appState.currentItemEndArrowhead,
|
||||||
|
true,
|
||||||
|
appState.currentItemEndArrowhead,
|
||||||
|
)}
|
||||||
|
onChange={(value) => updateData({ position: "end", type: value })}
|
||||||
|
numberOfOptionsToAlwaysShow={4}
|
||||||
|
/>
|
||||||
|
</customOptions.pickerRenders.ButtonList>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!customOptions?.pickerRenders?.ButtonList && (
|
||||||
<div className="iconSelectList buttonList">
|
<div className="iconSelectList buttonList">
|
||||||
<IconPicker
|
<IconPicker
|
||||||
label="arrowhead_start"
|
label="arrowhead_start"
|
||||||
|
@ -1539,7 +1604,9 @@ export const actionChangeArrowhead = register({
|
||||||
true,
|
true,
|
||||||
appState.currentItemStartArrowhead,
|
appState.currentItemStartArrowhead,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData({ position: "start", type: value })}
|
onChange={(value) =>
|
||||||
|
updateData({ position: "start", type: value })
|
||||||
|
}
|
||||||
numberOfOptionsToAlwaysShow={4}
|
numberOfOptionsToAlwaysShow={4}
|
||||||
/>
|
/>
|
||||||
<IconPicker
|
<IconPicker
|
||||||
|
@ -1560,6 +1627,7 @@ export const actionChangeArrowhead = register({
|
||||||
numberOfOptionsToAlwaysShow={4}
|
numberOfOptionsToAlwaysShow={4}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,7 +40,19 @@ 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 }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
|
onClick: () => updateData(null),
|
||||||
|
title: `${t("labels.sendBackward")}`,
|
||||||
|
children: SendBackwardIcon,
|
||||||
|
name: "sendBackward",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="zIndexButton"
|
className="zIndexButton"
|
||||||
|
@ -45,7 +61,8 @@ export const actionSendBackward = register({
|
||||||
>
|
>
|
||||||
{SendBackwardIcon}
|
{SendBackwardIcon}
|
||||||
</button>
|
</button>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionBringForward = register({
|
export const actionBringForward = register({
|
||||||
|
@ -66,7 +83,19 @@ 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 }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
|
onClick: () => updateData(null),
|
||||||
|
title: `${t("labels.bringForward")}`,
|
||||||
|
children: BringForwardIcon,
|
||||||
|
name: "bringForward",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="zIndexButton"
|
className="zIndexButton"
|
||||||
|
@ -75,7 +104,8 @@ export const actionBringForward = register({
|
||||||
>
|
>
|
||||||
{BringForwardIcon}
|
{BringForwardIcon}
|
||||||
</button>
|
</button>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionSendToBack = register({
|
export const actionSendToBack = register({
|
||||||
|
@ -99,7 +129,19 @@ 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 }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
|
onClick: () => updateData(null),
|
||||||
|
title: `${t("labels.sendToBack")}`,
|
||||||
|
children: SendToBackIcon,
|
||||||
|
name: "sendToBack",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="zIndexButton"
|
className="zIndexButton"
|
||||||
|
@ -112,7 +154,8 @@ export const actionSendToBack = register({
|
||||||
>
|
>
|
||||||
{SendToBackIcon}
|
{SendToBackIcon}
|
||||||
</button>
|
</button>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionBringToFront = register({
|
export const actionBringToFront = register({
|
||||||
|
@ -137,7 +180,19 @@ 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 }) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
|
onClick: () => updateData(null),
|
||||||
|
title: `${t("labels.bringToFront")}`,
|
||||||
|
children: BringToFrontIcon,
|
||||||
|
name: "bringToFront",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="zIndexButton"
|
className="zIndexButton"
|
||||||
|
@ -150,5 +205,6 @@ export const actionBringToFront = register({
|
||||||
>
|
>
|
||||||
{BringToFrontIcon}
|
{BringToFrontIcon}
|
||||||
</button>
|
</button>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,6 +20,8 @@ export {
|
||||||
actionChangeVerticalAlign,
|
actionChangeVerticalAlign,
|
||||||
} from "./actionProperties";
|
} from "./actionProperties";
|
||||||
|
|
||||||
|
export { actionChangeBlur } from "./actionBlur";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
actionChangeViewBackgroundColor,
|
actionChangeViewBackgroundColor,
|
||||||
actionClearCanvas,
|
actionClearCanvas,
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -140,7 +140,8 @@ export type ActionName =
|
||||||
| "linkToElement"
|
| "linkToElement"
|
||||||
| "cropEditor"
|
| "cropEditor"
|
||||||
| "wrapSelectionInFrame"
|
| "wrapSelectionInFrame"
|
||||||
| "toggleLassoTool";
|
| "toggleLassoTool"
|
||||||
|
| "changeBlur";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
|
|
|
@ -33,6 +33,7 @@ export const getDefaultAppState = (): Omit<
|
||||||
currentItemFontFamily: DEFAULT_FONT_FAMILY,
|
currentItemFontFamily: DEFAULT_FONT_FAMILY,
|
||||||
currentItemFontSize: DEFAULT_FONT_SIZE,
|
currentItemFontSize: DEFAULT_FONT_SIZE,
|
||||||
currentItemOpacity: DEFAULT_ELEMENT_PROPS.opacity,
|
currentItemOpacity: DEFAULT_ELEMENT_PROPS.opacity,
|
||||||
|
currentItemBlur: 50,
|
||||||
currentItemRoughness: DEFAULT_ELEMENT_PROPS.roughness,
|
currentItemRoughness: DEFAULT_ELEMENT_PROPS.roughness,
|
||||||
currentItemStartArrowhead: null,
|
currentItemStartArrowhead: null,
|
||||||
currentItemStrokeColor: DEFAULT_ELEMENT_PROPS.strokeColor,
|
currentItemStrokeColor: DEFAULT_ELEMENT_PROPS.strokeColor,
|
||||||
|
@ -161,6 +162,7 @@ const APP_STATE_STORAGE_CONF = (<
|
||||||
server: false,
|
server: false,
|
||||||
},
|
},
|
||||||
currentItemOpacity: { browser: true, export: false, server: false },
|
currentItemOpacity: { browser: true, export: false, server: false },
|
||||||
|
currentItemBlur: { browser: true, export: false, server: false },
|
||||||
currentItemRoughness: { browser: true, export: false, server: false },
|
currentItemRoughness: { browser: true, export: false, server: false },
|
||||||
currentItemStartArrowhead: { browser: true, export: false, server: false },
|
currentItemStartArrowhead: { browser: true, export: false, server: false },
|
||||||
currentItemStrokeColor: { browser: true, export: false, server: false },
|
currentItemStrokeColor: { browser: true, export: false, server: false },
|
||||||
|
|
|
@ -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,
|
||||||
|
@ -21,7 +21,12 @@ import {
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "@excalidraw/element/typeChecks";
|
} from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
import { hasStrokeColor, toolIsArrow } from "@excalidraw/element/comparisons";
|
import {
|
||||||
|
canChangeBlur,
|
||||||
|
canChangeLayer,
|
||||||
|
hasStrokeColor,
|
||||||
|
toolIsArrow,
|
||||||
|
} from "@excalidraw/element/comparisons";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
@ -46,6 +51,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";
|
||||||
|
@ -86,6 +93,7 @@ export const canChangeStrokeColor = (
|
||||||
(hasStrokeColor(appState.activeTool.type) &&
|
(hasStrokeColor(appState.activeTool.type) &&
|
||||||
commonSelectedType !== "image" &&
|
commonSelectedType !== "image" &&
|
||||||
commonSelectedType !== "frame" &&
|
commonSelectedType !== "frame" &&
|
||||||
|
commonSelectedType !== "blur" &&
|
||||||
commonSelectedType !== "magicframe") ||
|
commonSelectedType !== "magicframe") ||
|
||||||
targetElements.some((element) => hasStrokeColor(element.type))
|
targetElements.some((element) => hasStrokeColor(element.type))
|
||||||
);
|
);
|
||||||
|
@ -112,6 +120,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;
|
||||||
|
@ -153,6 +163,12 @@ export const SelectedShapeActions = ({
|
||||||
const showAlignActions =
|
const showAlignActions =
|
||||||
!isSingleElementBoundContainer && alignActionsPredicate(appState, app);
|
!isSingleElementBoundContainer && alignActionsPredicate(appState, app);
|
||||||
|
|
||||||
|
const showLayerActions =
|
||||||
|
appState.activeTool.type === "selection"
|
||||||
|
? !targetElements.some((element) => !canChangeLayer(element.type))
|
||||||
|
: canChangeLayer(appState.activeTool.type) ||
|
||||||
|
targetElements.some((element) => canChangeLayer(element.type));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panelColumn">
|
<div className="panelColumn">
|
||||||
<div>
|
<div>
|
||||||
|
@ -208,21 +224,38 @@ export const SelectedShapeActions = ({
|
||||||
<>{renderAction("changeArrowhead")}</>
|
<>{renderAction("changeArrowhead")}</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(canChangeBlur(appState.activeTool.type) ||
|
||||||
|
targetElements.some((element) => canChangeBlur(element.type))) &&
|
||||||
|
renderAction("changeBlur")}
|
||||||
|
|
||||||
{renderAction("changeOpacity")}
|
{renderAction("changeOpacity")}
|
||||||
|
|
||||||
|
{showLayerActions && (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.layers")}</legend>
|
<legend>{t("labels.layers")}</legend>
|
||||||
<div className="buttonList">
|
{!customOptions?.pickerRenders?.ButtonList && (
|
||||||
|
<div className={"buttonList"}>
|
||||||
{renderAction("sendToBack")}
|
{renderAction("sendToBack")}
|
||||||
{renderAction("sendBackward")}
|
{renderAction("sendBackward")}
|
||||||
{renderAction("bringForward")}
|
{renderAction("bringForward")}
|
||||||
{renderAction("bringToFront")}
|
{renderAction("bringToFront")}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{customOptions?.pickerRenders?.ButtonList && (
|
||||||
|
<customOptions.pickerRenders.ButtonList>
|
||||||
|
{renderAction("sendToBack")}
|
||||||
|
{renderAction("sendBackward")}
|
||||||
|
{renderAction("bringForward")}
|
||||||
|
{renderAction("bringToFront")}
|
||||||
|
</customOptions.pickerRenders.ButtonList>
|
||||||
|
)}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
)}
|
||||||
|
|
||||||
{showAlignActions && !isSingleElementBoundContainer && (
|
{showAlignActions && !isSingleElementBoundContainer && (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.align")}</legend>
|
<legend>{t("labels.align")}</legend>
|
||||||
|
{!customOptions?.pickerRenders?.ButtonList && (
|
||||||
<div className="buttonList">
|
<div className="buttonList">
|
||||||
{
|
{
|
||||||
// swap this order for RTL so the button positions always match their action
|
// swap this order for RTL so the button positions always match their action
|
||||||
|
@ -260,20 +293,69 @@ export const SelectedShapeActions = ({
|
||||||
renderAction("distributeVertically")}
|
renderAction("distributeVertically")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{customOptions?.pickerRenders?.ButtonList && (
|
||||||
|
<>
|
||||||
|
<customOptions.pickerRenders.ButtonList>
|
||||||
|
{
|
||||||
|
// swap this order for RTL so the button positions always match their action
|
||||||
|
// (i.e. the leftmost button aligns left)
|
||||||
|
}
|
||||||
|
{isRTL ? (
|
||||||
|
<>
|
||||||
|
{renderAction("alignRight")}
|
||||||
|
{renderAction("alignHorizontallyCentered")}
|
||||||
|
{renderAction("alignLeft")}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{renderAction("alignLeft")}
|
||||||
|
{renderAction("alignHorizontallyCentered")}
|
||||||
|
{renderAction("alignRight")}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{targetElements.length > 2 &&
|
||||||
|
renderAction("distributeHorizontally")}
|
||||||
|
</customOptions.pickerRenders.ButtonList>
|
||||||
|
<customOptions.pickerRenders.ButtonList>
|
||||||
|
{renderAction("alignTop")}
|
||||||
|
{renderAction("alignVerticallyCentered")}
|
||||||
|
{renderAction("alignBottom")}
|
||||||
|
|
||||||
|
{targetElements.length > 2 &&
|
||||||
|
renderAction("distributeVertically")}
|
||||||
|
</customOptions.pickerRenders.ButtonList>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
)}
|
)}
|
||||||
{!isEditingTextOrNewElement && targetElements.length > 0 && (
|
{!isEditingTextOrNewElement && targetElements.length > 0 && (
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.actions")}</legend>
|
<legend>{t("labels.actions")}</legend>
|
||||||
|
{!customOptions?.pickerRenders?.ButtonList && (
|
||||||
<div className="buttonList">
|
<div className="buttonList">
|
||||||
{!device.editor.isMobile && renderAction("duplicateSelection")}
|
{!device.editor.isMobile && renderAction("duplicateSelection")}
|
||||||
{!device.editor.isMobile && renderAction("deleteSelectedElements")}
|
{!device.editor.isMobile &&
|
||||||
|
renderAction("deleteSelectedElements")}
|
||||||
{renderAction("group")}
|
{renderAction("group")}
|
||||||
{renderAction("ungroup")}
|
{renderAction("ungroup")}
|
||||||
{showLinkIcon && renderAction("hyperlink")}
|
{showLinkIcon && renderAction("hyperlink")}
|
||||||
{showCropEditorAction && renderAction("cropEditor")}
|
{showCropEditorAction && renderAction("cropEditor")}
|
||||||
{showLineEditorAction && renderAction("toggleLinearEditor")}
|
{showLineEditorAction && renderAction("toggleLinearEditor")}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{customOptions?.pickerRenders?.ButtonList && (
|
||||||
|
<customOptions.pickerRenders.ButtonList>
|
||||||
|
{!device.editor.isMobile && renderAction("duplicateSelection")}
|
||||||
|
{!device.editor.isMobile &&
|
||||||
|
renderAction("deleteSelectedElements")}
|
||||||
|
{renderAction("group")}
|
||||||
|
{renderAction("ungroup")}
|
||||||
|
{showLinkIcon && renderAction("hyperlink")}
|
||||||
|
{showCropEditorAction && renderAction("cropEditor")}
|
||||||
|
{showLineEditorAction && renderAction("toggleLinearEditor")}
|
||||||
|
</customOptions.pickerRenders.ButtonList>
|
||||||
|
)}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -136,6 +140,7 @@ import {
|
||||||
newLinearElement,
|
newLinearElement,
|
||||||
newTextElement,
|
newTextElement,
|
||||||
refreshTextDimensions,
|
refreshTextDimensions,
|
||||||
|
newBlurElement,
|
||||||
} from "@excalidraw/element/newElement";
|
} from "@excalidraw/element/newElement";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -643,6 +648,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
public id: string;
|
public id: string;
|
||||||
private store: Store;
|
private store: Store;
|
||||||
private history: History;
|
private history: History;
|
||||||
|
|
||||||
|
public getHistory = () => this.history;
|
||||||
|
|
||||||
public excalidrawContainerValue: {
|
public excalidrawContainerValue: {
|
||||||
container: HTMLDivElement | null;
|
container: HTMLDivElement | null;
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -804,7 +812,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.fonts = new Fonts(this.scene);
|
this.fonts = new Fonts(this.scene);
|
||||||
this.history = new History();
|
this.history = new History(this.props.customOptions?.onHistoryChange);
|
||||||
|
|
||||||
this.actionManager.registerAll(actions);
|
this.actionManager.registerAll(actions);
|
||||||
this.actionManager.registerAction(
|
this.actionManager.registerAction(
|
||||||
|
@ -813,6 +821,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) {
|
||||||
|
@ -1651,10 +1679,12 @@ 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-container-inner">
|
||||||
<div className="excalidraw-textEditorContainer" />
|
<div className="excalidraw-textEditorContainer" />
|
||||||
<div className="excalidraw-contextMenuContainer" />
|
<div className="excalidraw-contextMenuContainer" />
|
||||||
<div className="excalidraw-eye-dropper-container" />
|
<div className="excalidraw-eye-dropper-container" />
|
||||||
|
@ -1760,7 +1790,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
closable={this.state.toast.closable}
|
closable={this.state.toast.closable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{this.state.contextMenu && (
|
{this.state.contextMenu &&
|
||||||
|
!this.props.customOptions?.hideContextMenu && (
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
items={this.state.contextMenu.items}
|
items={this.state.contextMenu.items}
|
||||||
top={this.state.contextMenu.top}
|
top={this.state.contextMenu.top}
|
||||||
|
@ -1792,8 +1823,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
renderGrid: isGridModeEnabled(this),
|
renderGrid: isGridModeEnabled(this),
|
||||||
canvasBackgroundColor:
|
canvasBackgroundColor:
|
||||||
this.state.viewBackgroundColor,
|
this.state.viewBackgroundColor,
|
||||||
embedsValidationStatus: this.embedsValidationStatus,
|
embedsValidationStatus:
|
||||||
elementsPendingErasure: this.elementsPendingErasure,
|
this.embedsValidationStatus,
|
||||||
|
elementsPendingErasure:
|
||||||
|
this.elementsPendingErasure,
|
||||||
pendingFlowchartNodes:
|
pendingFlowchartNodes:
|
||||||
this.flowChartCreator.pendingNodes,
|
this.flowChartCreator.pendingNodes,
|
||||||
}}
|
}}
|
||||||
|
@ -1857,6 +1890,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{this.renderFrameNames()}
|
{this.renderFrameNames()}
|
||||||
|
</div>
|
||||||
</ExcalidrawActionManagerContext.Provider>
|
</ExcalidrawActionManagerContext.Provider>
|
||||||
{this.renderEmbeddables()}
|
{this.renderEmbeddables()}
|
||||||
</ExcalidrawElementsContext.Provider>
|
</ExcalidrawElementsContext.Provider>
|
||||||
|
@ -2252,7 +2286,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
|
||||||
|
@ -2266,6 +2300,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
name,
|
name,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return res;
|
||||||
});
|
});
|
||||||
|
|
||||||
didUpdate = true;
|
didUpdate = true;
|
||||||
|
@ -2973,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,
|
||||||
);
|
);
|
||||||
|
@ -4109,6 +4149,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 (
|
||||||
|
@ -5986,7 +6030,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
|
||||||
|
@ -6240,6 +6289,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
|
|
||||||
this.elementsPendingErasure = new Set(elementsToErase);
|
this.elementsPendingErasure = new Set(elementsToErase);
|
||||||
this.triggerRender();
|
this.triggerRender();
|
||||||
|
this.props.customOptions?.onHandleEraser?.(this.elementsPendingErasure);
|
||||||
};
|
};
|
||||||
|
|
||||||
// set touch moving for mobile context menu
|
// set touch moving for mobile context menu
|
||||||
|
@ -7820,7 +7870,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
| "diamond"
|
| "diamond"
|
||||||
| "ellipse"
|
| "ellipse"
|
||||||
| "iframe"
|
| "iframe"
|
||||||
| "embeddable",
|
| "embeddable"
|
||||||
|
| "blur",
|
||||||
) {
|
) {
|
||||||
return this.state.currentItemRoundness === "round"
|
return this.state.currentItemRoundness === "round"
|
||||||
? {
|
? {
|
||||||
|
@ -7832,7 +7883,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private createGenericElementOnPointerDown = (
|
private createGenericElementOnPointerDown = (
|
||||||
elementType: ExcalidrawGenericElement["type"] | "embeddable",
|
elementType: ExcalidrawGenericElement["type"] | "embeddable" | "blur",
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
): void => {
|
): void => {
|
||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
|
@ -7869,6 +7920,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
type: "embeddable",
|
type: "embeddable",
|
||||||
...baseElementAttributes,
|
...baseElementAttributes,
|
||||||
});
|
});
|
||||||
|
} else if (elementType === "blur") {
|
||||||
|
element = newBlurElement({
|
||||||
|
...baseElementAttributes,
|
||||||
|
blur: this.state.currentItemBlur,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
element = newElement({
|
element = newElement({
|
||||||
type: elementType,
|
type: elementType,
|
||||||
|
@ -8653,7 +8709,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,
|
||||||
|
@ -10578,7 +10640,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),
|
||||||
scene: this.scene,
|
scene: this.scene,
|
||||||
zoom: this.state.zoom.value,
|
zoom: this.state.zoom.value,
|
||||||
informMutation: false,
|
informMutation: false,
|
||||||
|
@ -10868,11 +10930,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
this.scene,
|
this.scene,
|
||||||
shouldRotateWithDiscreteAngle(event),
|
(
|
||||||
|
this.props.customOptions?.shouldRotateWithDiscreteAngle ??
|
||||||
|
shouldRotateWithDiscreteAngle
|
||||||
|
)(event),
|
||||||
shouldResizeFromCenter(event),
|
shouldResizeFromCenter(event),
|
||||||
selectedElements.some((element) => isImageElement(element))
|
selectedElements.some((element) =>
|
||||||
|
isImageElement(element)
|
||||||
? !shouldMaintainAspectRatio(event)
|
? !shouldMaintainAspectRatio(event)
|
||||||
: shouldMaintainAspectRatio(event),
|
: shouldMaintainAspectRatio(event),
|
||||||
|
),
|
||||||
resizeX,
|
resizeX,
|
||||||
resizeY,
|
resizeY,
|
||||||
pointerDownState.resize.center.x,
|
pointerDownState.resize.center.x,
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
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> = {
|
||||||
|
|
||||||
// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />
|
|
||||||
export const ButtonIconSelect = <T extends Object>(
|
|
||||||
props: {
|
|
||||||
options: {
|
options: {
|
||||||
value: T;
|
value: T;
|
||||||
text: string;
|
text: string;
|
||||||
|
@ -17,7 +17,7 @@ export const ButtonIconSelect = <T extends Object>(
|
||||||
}[];
|
}[];
|
||||||
value: T | null;
|
value: T | null;
|
||||||
type?: "radio" | "button";
|
type?: "radio" | "button";
|
||||||
} & (
|
} & (
|
||||||
| { type?: "radio"; group: string; onChange: (value: T) => void }
|
| { type?: "radio"; group: string; onChange: (value: T) => void }
|
||||||
| {
|
| {
|
||||||
type: "button";
|
type: "button";
|
||||||
|
@ -26,11 +26,38 @@ export const ButtonIconSelect = <T extends Object>(
|
||||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
),
|
);
|
||||||
) => (
|
|
||||||
<div className="buttonList">
|
// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />
|
||||||
{props.options.map((option) =>
|
export const ButtonIconSelect = <T extends Object>(
|
||||||
props.type === "button" ? (
|
props: ButtonIconSelectProps<T>,
|
||||||
|
) => {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.buttonIconSelectRender) {
|
||||||
|
return customOptions.pickerRenders.buttonIconSelectRender(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderButtonIcon = (
|
||||||
|
option: ButtonIconSelectProps<T>["options"][number],
|
||||||
|
) => {
|
||||||
|
if (props.type !== "button") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customOptions?.pickerRenders?.CustomButtonIcon) {
|
||||||
|
return (
|
||||||
|
<customOptions.pickerRenders.CustomButtonIcon
|
||||||
|
key={option.text}
|
||||||
|
icon={option.icon}
|
||||||
|
title={option.text}
|
||||||
|
testId={option.testId}
|
||||||
|
active={option.active ?? props.value === option.value}
|
||||||
|
onClick={(event) => props.onClick(option.value, event)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
<ButtonIcon
|
<ButtonIcon
|
||||||
key={option.text}
|
key={option.text}
|
||||||
icon={option.icon}
|
icon={option.icon}
|
||||||
|
@ -39,7 +66,31 @@ 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)}
|
||||||
/>
|
/>
|
||||||
) : (
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderRadioButtonIcon = (
|
||||||
|
option: ButtonIconSelectProps<T>["options"][number],
|
||||||
|
) => {
|
||||||
|
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
|
<label
|
||||||
key={option.text}
|
key={option.text}
|
||||||
className={clsx({ active: props.value === option.value })}
|
className={clsx({ active: props.value === option.value })}
|
||||||
|
@ -54,7 +105,16 @@ export const ButtonIconSelect = <T extends Object>(
|
||||||
/>
|
/>
|
||||||
{option.icon}
|
{option.icon}
|
||||||
</label>
|
</label>
|
||||||
),
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="buttonList">
|
||||||
|
{props.options.map((option) =>
|
||||||
|
props.type === "button"
|
||||||
|
? renderButtonIcon(option)
|
||||||
|
: renderRadioButtonIcon(option),
|
||||||
)}
|
)}
|
||||||
</div>
|
</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,16 +223,22 @@ 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 (
|
return (
|
||||||
<div>
|
|
||||||
<div role="dialog" aria-modal="true" className="color-picker-container">
|
|
||||||
<TopPicks
|
|
||||||
activeColor={color}
|
|
||||||
onChange={onChange}
|
|
||||||
type={type}
|
|
||||||
topPicks={topPicks}
|
|
||||||
/>
|
|
||||||
<ButtonSeparator />
|
|
||||||
<Popover.Root
|
<Popover.Root
|
||||||
open={appState.openPopup === type}
|
open={appState.openPopup === type}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
|
@ -251,6 +260,20 @@ export const ColorPicker = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div role="dialog" aria-modal="true" className="color-picker-container">
|
||||||
|
<TopPicks
|
||||||
|
activeColor={color}
|
||||||
|
onChange={onChange}
|
||||||
|
type={type}
|
||||||
|
topPicks={topPicks}
|
||||||
|
/>
|
||||||
|
<ButtonSeparator />
|
||||||
|
{renderPopover()}
|
||||||
</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,7 +58,21 @@ 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) => {
|
||||||
|
if (customOptions?.pickerRenders?.colorPickerTopPickesButtonRender) {
|
||||||
|
return customOptions.pickerRenders.colorPickerTopPickesButtonRender({
|
||||||
|
active: color === activeColor,
|
||||||
|
color,
|
||||||
|
isTransparent: color === "transparent" || !color,
|
||||||
|
hasOutline: !isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD),
|
||||||
|
onClick: () => onChange(color),
|
||||||
|
dataTestid: `color-top-pick-${color}`,
|
||||||
|
children: <div className="color-picker__button-outline" />,
|
||||||
|
key: color,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
className={clsx("color-picker__button", {
|
className={clsx("color-picker__button", {
|
||||||
active: color === activeColor,
|
active: color === activeColor,
|
||||||
|
@ -68,7 +91,8 @@ export const TopPicks = ({
|
||||||
>
|
>
|
||||||
<div className="color-picker__button-outline" />
|
<div className="color-picker__button-outline" />
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
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 React, { useEffect } from "react";
|
import React, { useContext, useEffect } from "react";
|
||||||
|
|
||||||
import { isArrowKey, KEYS } from "@excalidraw/common";
|
import { isArrowKey, KEYS } from "@excalidraw/common";
|
||||||
|
|
||||||
import { atom, useAtom } from "../editor-jotai";
|
import { atom, useAtom } from "../editor-jotai";
|
||||||
import { getLanguage, t } from "../i18n";
|
import { getLanguage, t } from "../i18n";
|
||||||
|
|
||||||
|
import { ExcalidrawPropsCustomOptionsContext } from "../types";
|
||||||
|
|
||||||
import Collapsible from "./Stats/Collapsible";
|
import Collapsible from "./Stats/Collapsible";
|
||||||
import { useDevice } from "./App";
|
import { useDevice } from "./App";
|
||||||
|
|
||||||
|
@ -115,10 +117,33 @@ function Picker<T>({
|
||||||
}
|
}
|
||||||
}, [value, alwaysVisibleOptions, setShowMoreOptions]);
|
}, [value, alwaysVisibleOptions, setShowMoreOptions]);
|
||||||
|
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
const renderOptions = (options: Option<T>[]) => {
|
const renderOptions = (options: Option<T>[]) => {
|
||||||
return (
|
return (
|
||||||
<div className="picker-content">
|
<div className="picker-content">
|
||||||
{options.map((option, i) => (
|
{options.map((option, i) => {
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
|
return customOptions.pickerRenders.layerButtonRender({
|
||||||
|
active: value === option.value,
|
||||||
|
title: option.text,
|
||||||
|
children: (
|
||||||
|
<>
|
||||||
|
{option.icon}
|
||||||
|
{/* {option.keyBinding && (
|
||||||
|
<span className="picker-keybinding">
|
||||||
|
{option.keyBinding}
|
||||||
|
</span>
|
||||||
|
)} */}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
key: option.text,
|
||||||
|
onClick: () => onChange(option.value),
|
||||||
|
name: option.text,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={clsx("picker-option", {
|
className={clsx("picker-option", {
|
||||||
|
@ -147,7 +172,8 @@ function Picker<T>({
|
||||||
<span className="picker-keybinding">{option.keyBinding}</span>
|
<span className="picker-keybinding">{option.keyBinding}</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -162,7 +188,7 @@ function Picker<T>({
|
||||||
align="start"
|
align="start"
|
||||||
sideOffset={12}
|
sideOffset={12}
|
||||||
style={{ zIndex: "var(--zIndex-popup)" }}
|
style={{ zIndex: "var(--zIndex-popup)" }}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={customOptions?.disableKeyEvents ? undefined : handleKeyDown}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`picker`}
|
className={`picker`}
|
||||||
|
@ -209,12 +235,15 @@ export function IconPicker<T>({
|
||||||
numberOfOptionsToAlwaysShow?: number;
|
numberOfOptionsToAlwaysShow?: number;
|
||||||
group?: string;
|
group?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
const [isActive, setActive] = React.useState(false);
|
const [isActive, setActive] = React.useState(false);
|
||||||
const rPickerButton = React.useRef<any>(null);
|
const rPickerButton = React.useRef<any>(null);
|
||||||
|
|
||||||
|
const renderTrigger = () => {
|
||||||
|
if (customOptions?.pickerRenders?.layerButtonRender) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<Popover.Root open={isActive} onOpenChange={(open) => setActive(open)}>
|
|
||||||
<Popover.Trigger
|
<Popover.Trigger
|
||||||
name={group}
|
name={group}
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -222,9 +251,30 @@ export function IconPicker<T>({
|
||||||
onClick={() => setActive(!isActive)}
|
onClick={() => setActive(!isActive)}
|
||||||
ref={rPickerButton}
|
ref={rPickerButton}
|
||||||
className={isActive ? "active" : ""}
|
className={isActive ? "active" : ""}
|
||||||
|
style={{
|
||||||
|
padding: 0,
|
||||||
|
border: "unset",
|
||||||
|
width: 0,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{options.find((option) => option.value === value)?.icon}
|
{options.find((option) => option.value === value)?.icon}
|
||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
|
{customOptions.pickerRenders.layerButtonRender({
|
||||||
|
name: group,
|
||||||
|
title: "",
|
||||||
|
onClick: () => setActive(!isActive),
|
||||||
|
children: options.find((option) => option.value === value)?.icon,
|
||||||
|
active: isActive,
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Popover.Root open={isActive} onOpenChange={(open) => setActive(open)}>
|
||||||
|
{renderTrigger()}
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<Picker
|
<Picker
|
||||||
options={options}
|
options={options}
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
heading="selectedShapeActions"
|
||||||
|
className={clsx("selected-shape-actions zen-mode-transition", {
|
||||||
|
"transition-left": appState.zenModeEnabled,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{customOptions?.layoutRenders?.menuRender?.({ children }) ?? children}
|
||||||
</Section>
|
</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}
|
||||||
|
@ -543,12 +560,14 @@ const LayerUI = ({
|
||||||
>
|
>
|
||||||
{renderWelcomeScreen && <tunnels.WelcomeScreenCenterTunnel.Out />}
|
{renderWelcomeScreen && <tunnels.WelcomeScreenCenterTunnel.Out />}
|
||||||
{renderFixedSideContainer()}
|
{renderFixedSideContainer()}
|
||||||
|
{!customOptions?.hideFooter && (
|
||||||
<Footer
|
<Footer
|
||||||
appState={appState}
|
appState={appState}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
showExitZenModeBtn={showExitZenModeBtn}
|
showExitZenModeBtn={showExitZenModeBtn}
|
||||||
renderWelcomeScreen={renderWelcomeScreen}
|
renderWelcomeScreen={renderWelcomeScreen}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{appState.scrolledOutside && (
|
{appState.scrolledOutside && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import React, { useEffect } from "react";
|
import React, { useContext, useEffect } from "react";
|
||||||
|
|
||||||
|
import { isBlurElement } from "@excalidraw/element/typeChecks";
|
||||||
|
|
||||||
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,9 +43,22 @@ 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")}
|
||||||
|
{customOptions?.pickerRenders?.rangeRender ? (
|
||||||
|
customOptions?.pickerRenders?.rangeRender({
|
||||||
|
value,
|
||||||
|
onChange: (value: number) => {
|
||||||
|
updateData(value);
|
||||||
|
},
|
||||||
|
step: 10,
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
})
|
||||||
|
) : (
|
||||||
<div className="range-wrapper">
|
<div className="range-wrapper">
|
||||||
<input
|
<input
|
||||||
ref={rangeRef}
|
ref={rangeRef}
|
||||||
|
@ -62,6 +78,80 @@ export const Range = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="zero-label">0</div>
|
<div className="zero-label">0</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BlurRange = ({
|
||||||
|
updateData,
|
||||||
|
appState,
|
||||||
|
elements,
|
||||||
|
testId,
|
||||||
|
}: RangeProps) => {
|
||||||
|
const rangeRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const valueRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const value = getFormValue(
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
(element) => {
|
||||||
|
if (isBlurElement(element)) {
|
||||||
|
return element.blur;
|
||||||
|
}
|
||||||
|
return appState.currentItemBlur;
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
appState.currentItemBlur,
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (rangeRef.current && valueRef.current) {
|
||||||
|
const rangeElement = rangeRef.current;
|
||||||
|
const valueElement = valueRef.current;
|
||||||
|
const inputWidth = rangeElement.offsetWidth;
|
||||||
|
const thumbWidth = 15; // 15 is the width of the thumb
|
||||||
|
const position =
|
||||||
|
(value / 100) * (inputWidth - thumbWidth) + thumbWidth / 2;
|
||||||
|
valueElement.style.left = `${position}px`;
|
||||||
|
rangeElement.style.background = `linear-gradient(to right, var(--color-slider-track) 0%, var(--color-slider-track) ${value}%, var(--button-bg) ${value}%, var(--button-bg) 100%)`;
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const customOptions = useContext(ExcalidrawPropsCustomOptionsContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label className="control-label">
|
||||||
|
{t("labels.blur")}
|
||||||
|
{customOptions?.pickerRenders?.rangeRender ? (
|
||||||
|
customOptions?.pickerRenders?.rangeRender({
|
||||||
|
value,
|
||||||
|
onChange: (value: number) => {
|
||||||
|
updateData(value);
|
||||||
|
},
|
||||||
|
step: 10,
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="range-wrapper">
|
||||||
|
<input
|
||||||
|
ref={rangeRef}
|
||||||
|
type="range"
|
||||||
|
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>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -47,6 +47,7 @@ const StaticCanvas = (props: StaticCanvasProps) => {
|
||||||
|
|
||||||
wrapper.replaceChildren(canvas);
|
wrapper.replaceChildren(canvas);
|
||||||
canvas.classList.add("excalidraw__canvas", "static");
|
canvas.classList.add("excalidraw__canvas", "static");
|
||||||
|
canvas.id = "excalidraw__content-canvas";
|
||||||
}
|
}
|
||||||
|
|
||||||
const widthString = `${props.appState.width}px`;
|
const widthString = `${props.appState.width}px`;
|
||||||
|
|
|
@ -9,8 +9,8 @@ import {
|
||||||
LineIcon,
|
LineIcon,
|
||||||
FreedrawIcon,
|
FreedrawIcon,
|
||||||
TextIcon,
|
TextIcon,
|
||||||
ImageIcon,
|
|
||||||
EraserIcon,
|
EraserIcon,
|
||||||
|
ImageIcon,
|
||||||
} from "./icons";
|
} from "./icons";
|
||||||
|
|
||||||
export const SHAPES = [
|
export const SHAPES = [
|
||||||
|
@ -77,6 +77,13 @@ export const SHAPES = [
|
||||||
numericKey: KEYS["9"],
|
numericKey: KEYS["9"],
|
||||||
fillable: false,
|
fillable: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: RectangleIcon,
|
||||||
|
key: null,
|
||||||
|
value: "blur",
|
||||||
|
numericKey: KEYS["9"],
|
||||||
|
fillable: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: EraserIcon,
|
icon: EraserIcon,
|
||||||
value: "eraser",
|
value: "eraser",
|
||||||
|
|
|
@ -102,6 +102,7 @@ export const AllowedExcalidrawActiveTools: Record<
|
||||||
hand: true,
|
hand: true,
|
||||||
laser: false,
|
laser: false,
|
||||||
magicframe: false,
|
magicframe: false,
|
||||||
|
blur: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RestoredDataState = {
|
export type RestoredDataState = {
|
||||||
|
@ -383,6 +384,7 @@ const restoreElement = (
|
||||||
|
|
||||||
// generic elements
|
// generic elements
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
|
case "blur":
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
case "iframe":
|
case "iframe":
|
||||||
|
|
|
@ -20,6 +20,13 @@ export class History {
|
||||||
[HistoryChangedEvent]
|
[HistoryChangedEvent]
|
||||||
>();
|
>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly onChange: (
|
||||||
|
history: History,
|
||||||
|
type: "undo" | "redo" | "record" | "clear",
|
||||||
|
) => void = () => {},
|
||||||
|
) {}
|
||||||
|
|
||||||
private readonly undoStack: HistoryStack = [];
|
private readonly undoStack: HistoryStack = [];
|
||||||
private readonly redoStack: HistoryStack = [];
|
private readonly redoStack: HistoryStack = [];
|
||||||
|
|
||||||
|
@ -34,6 +41,8 @@ export class History {
|
||||||
public clear() {
|
public clear() {
|
||||||
this.undoStack.length = 0;
|
this.undoStack.length = 0;
|
||||||
this.redoStack.length = 0;
|
this.redoStack.length = 0;
|
||||||
|
|
||||||
|
this.onChange(this, "clear");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,6 +52,8 @@ export class History {
|
||||||
elementsChange: ElementsChange,
|
elementsChange: ElementsChange,
|
||||||
appStateChange: AppStateChange,
|
appStateChange: AppStateChange,
|
||||||
) {
|
) {
|
||||||
|
this.onChange(this, "record");
|
||||||
|
|
||||||
const entry = HistoryEntry.create(appStateChange, elementsChange);
|
const entry = HistoryEntry.create(appStateChange, elementsChange);
|
||||||
|
|
||||||
if (!entry.isEmpty()) {
|
if (!entry.isEmpty()) {
|
||||||
|
@ -67,6 +78,8 @@ export class History {
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
snapshot: Readonly<Snapshot>,
|
snapshot: Readonly<Snapshot>,
|
||||||
) {
|
) {
|
||||||
|
this.onChange(this, "undo");
|
||||||
|
|
||||||
return this.perform(
|
return this.perform(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
|
@ -81,6 +94,8 @@ export class History {
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
snapshot: Readonly<Snapshot>,
|
snapshot: Readonly<Snapshot>,
|
||||||
) {
|
) {
|
||||||
|
this.onChange(this, "redo");
|
||||||
|
|
||||||
return this.perform(
|
return this.perform(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useImperativeHandle, useMemo, useRef } from "react";
|
||||||
|
|
||||||
import { DEFAULT_UI_OPTIONS, isShallowEqual } from "@excalidraw/common";
|
import { DEFAULT_UI_OPTIONS, isShallowEqual } from "@excalidraw/common";
|
||||||
|
|
||||||
|
@ -16,7 +16,16 @@ 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 {
|
||||||
|
ExcalidrawHistoryContext,
|
||||||
|
ExcalidrawPropsCustomOptionsContext,
|
||||||
|
type AppProps,
|
||||||
|
type ExcalidrawProps,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
import type { ExcalidrawHistoryContextType } from "./types";
|
||||||
|
|
||||||
|
import type { ActionResult } from "./actions/types";
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
|
@ -53,6 +62,8 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||||
renderEmbeddable,
|
renderEmbeddable,
|
||||||
aiEnabled,
|
aiEnabled,
|
||||||
showDeprecatedFonts,
|
showDeprecatedFonts,
|
||||||
|
customOptions,
|
||||||
|
actionRef,
|
||||||
renderScrollbars,
|
renderScrollbars,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
@ -109,10 +120,43 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const appRef = useRef<App>(null);
|
||||||
|
|
||||||
|
const historyRedoRef = useRef<() => void>(undefined);
|
||||||
|
const historyUndoRef = useRef<() => void>(undefined);
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
actionRef,
|
||||||
|
() => ({
|
||||||
|
syncActionResult: (actionResult: ActionResult) => {
|
||||||
|
appRef.current?.syncActionResult(actionResult);
|
||||||
|
},
|
||||||
|
getHistory: () => appRef.current?.getHistory(),
|
||||||
|
historyRedo: () => {
|
||||||
|
historyRedoRef.current?.();
|
||||||
|
},
|
||||||
|
historyUndo: () => {
|
||||||
|
historyUndoRef.current?.();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const historyContextValue = useMemo<ExcalidrawHistoryContextType>(
|
||||||
|
() => ({
|
||||||
|
undoRef: historyUndoRef,
|
||||||
|
redoRef: historyRedoRef,
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ExcalidrawHistoryContext.Provider value={historyContextValue}>
|
||||||
|
<ExcalidrawPropsCustomOptionsContext.Provider value={customOptions}>
|
||||||
<EditorJotaiProvider store={editorJotaiStore}>
|
<EditorJotaiProvider store={editorJotaiStore}>
|
||||||
<InitializeApp langCode={langCode} theme={theme}>
|
<InitializeApp langCode={langCode} theme={theme}>
|
||||||
<App
|
<App
|
||||||
|
ref={appRef}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
initialData={initialData}
|
initialData={initialData}
|
||||||
excalidrawAPI={excalidrawAPI}
|
excalidrawAPI={excalidrawAPI}
|
||||||
|
@ -144,12 +188,15 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
||||||
renderEmbeddable={renderEmbeddable}
|
renderEmbeddable={renderEmbeddable}
|
||||||
aiEnabled={aiEnabled !== false}
|
aiEnabled={aiEnabled !== false}
|
||||||
showDeprecatedFonts={showDeprecatedFonts}
|
showDeprecatedFonts={showDeprecatedFonts}
|
||||||
|
customOptions={customOptions}
|
||||||
renderScrollbars={renderScrollbars}
|
renderScrollbars={renderScrollbars}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</App>
|
</App>
|
||||||
</InitializeApp>
|
</InitializeApp>
|
||||||
</EditorJotaiProvider>
|
</EditorJotaiProvider>
|
||||||
|
</ExcalidrawPropsCustomOptionsContext.Provider>
|
||||||
|
</ExcalidrawHistoryContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -127,6 +127,7 @@
|
||||||
"unbindText": "Unbind text",
|
"unbindText": "Unbind text",
|
||||||
"bindText": "Bind text to the container",
|
"bindText": "Bind text to the container",
|
||||||
"createContainerFromText": "Wrap text in a container",
|
"createContainerFromText": "Wrap text in a container",
|
||||||
|
"blur": "Strength",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "Edit link",
|
"edit": "Edit link",
|
||||||
"editEmbed": "Edit embeddable link",
|
"editEmbed": "Edit embeddable link",
|
||||||
|
@ -279,6 +280,7 @@
|
||||||
"lasso": "Lasso selection",
|
"lasso": "Lasso selection",
|
||||||
"image": "Insert image",
|
"image": "Insert image",
|
||||||
"rectangle": "Rectangle",
|
"rectangle": "Rectangle",
|
||||||
|
"blur": "Blur",
|
||||||
"diamond": "Diamond",
|
"diamond": "Diamond",
|
||||||
"ellipse": "Ellipse",
|
"ellipse": "Ellipse",
|
||||||
"arrow": "Arrow",
|
"arrow": "Arrow",
|
||||||
|
@ -312,6 +314,7 @@
|
||||||
"magicframe": "Wireframe to code",
|
"magicframe": "Wireframe to code",
|
||||||
"embeddable": "Web Embed",
|
"embeddable": "Web Embed",
|
||||||
"selection": "Selection",
|
"selection": "Selection",
|
||||||
|
"blur": "Blur",
|
||||||
"iframe": "IFrame"
|
"iframe": "IFrame"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
|
|
|
@ -44,11 +44,19 @@
|
||||||
"arrowhead_triangle_outline": "三角箭头(空心)",
|
"arrowhead_triangle_outline": "三角箭头(空心)",
|
||||||
"arrowhead_diamond": "菱形",
|
"arrowhead_diamond": "菱形",
|
||||||
"arrowhead_diamond_outline": "菱形(空心)",
|
"arrowhead_diamond_outline": "菱形(空心)",
|
||||||
|
"arrowhead_crowfoot_many": "交叉箭头(多个)",
|
||||||
|
"arrowhead_crowfoot_one": "交叉箭头(一个)",
|
||||||
|
"arrowhead_crowfoot_one_or_many": "交叉箭头(一个或多个)",
|
||||||
|
"arrowtypes": "箭头类型",
|
||||||
|
"arrowtype_sharp": "尖锐箭头",
|
||||||
|
"arrowtype_round": "圆润箭头",
|
||||||
|
"arrowtype_elbowed": "弯曲箭头",
|
||||||
"fontSize": "字体大小",
|
"fontSize": "字体大小",
|
||||||
"fontFamily": "字体",
|
"fontFamily": "字体",
|
||||||
"addWatermark": "添加 “使用 Excalidraw 创建” 水印",
|
"addWatermark": "添加 “使用 Excalidraw 创建” 水印",
|
||||||
"handDrawn": "手写",
|
"handDrawn": "手写",
|
||||||
"normal": "普通",
|
"normal": "普通",
|
||||||
|
"more_options": "更多选项",
|
||||||
"code": "代码",
|
"code": "代码",
|
||||||
"small": "小",
|
"small": "小",
|
||||||
"medium": "中",
|
"medium": "中",
|
||||||
|
@ -113,6 +121,7 @@
|
||||||
"unbindText": "取消文本绑定",
|
"unbindText": "取消文本绑定",
|
||||||
"bindText": "将文本绑定到容器",
|
"bindText": "将文本绑定到容器",
|
||||||
"createContainerFromText": "将文本包围在容器中",
|
"createContainerFromText": "将文本包围在容器中",
|
||||||
|
"blur": "强度",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "编辑链接",
|
"edit": "编辑链接",
|
||||||
"editEmbed": "编辑链接与嵌入",
|
"editEmbed": "编辑链接与嵌入",
|
||||||
|
@ -237,6 +246,7 @@
|
||||||
"selection": "选择",
|
"selection": "选择",
|
||||||
"image": "插入图像",
|
"image": "插入图像",
|
||||||
"rectangle": "矩形",
|
"rectangle": "矩形",
|
||||||
|
"blur": "模糊",
|
||||||
"diamond": "菱形",
|
"diamond": "菱形",
|
||||||
"ellipse": "椭圆",
|
"ellipse": "椭圆",
|
||||||
"arrow": "箭头",
|
"arrow": "箭头",
|
||||||
|
|
|
@ -44,11 +44,19 @@
|
||||||
"arrowhead_triangle_outline": "三角形(外框)",
|
"arrowhead_triangle_outline": "三角形(外框)",
|
||||||
"arrowhead_diamond": "菱形",
|
"arrowhead_diamond": "菱形",
|
||||||
"arrowhead_diamond_outline": "菱形(外框)",
|
"arrowhead_diamond_outline": "菱形(外框)",
|
||||||
|
"arrowhead_crowfoot_many": "交叉箭頭(多個)",
|
||||||
|
"arrowhead_crowfoot_one": "交叉箭頭(一個)",
|
||||||
|
"arrowhead_crowfoot_one_or_many": "交叉箭頭(一個或多個)",
|
||||||
|
"arrowtypes": "箭頭類型",
|
||||||
|
"arrowtype_sharp": "尖銳箭頭",
|
||||||
|
"arrowtype_round": "圓潤箭頭",
|
||||||
|
"arrowtype_elbowed": "彎曲箭頭",
|
||||||
"fontSize": "字型大小",
|
"fontSize": "字型大小",
|
||||||
"fontFamily": "字體集",
|
"fontFamily": "字體集",
|
||||||
"addWatermark": "加上 \"Made with Excalidraw\" 浮水印",
|
"addWatermark": "加上 \"Made with Excalidraw\" 浮水印",
|
||||||
"handDrawn": "手寫",
|
"handDrawn": "手寫",
|
||||||
"normal": "一般",
|
"normal": "一般",
|
||||||
|
"more_options": "更多選項",
|
||||||
"code": "代碼",
|
"code": "代碼",
|
||||||
"small": "小",
|
"small": "小",
|
||||||
"medium": "中",
|
"medium": "中",
|
||||||
|
@ -113,6 +121,7 @@
|
||||||
"unbindText": "取消綁定文字",
|
"unbindText": "取消綁定文字",
|
||||||
"bindText": "結合文字至容器",
|
"bindText": "結合文字至容器",
|
||||||
"createContainerFromText": "將文字包於容器中",
|
"createContainerFromText": "將文字包於容器中",
|
||||||
|
"blur": "強度",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "編輯連結",
|
"edit": "編輯連結",
|
||||||
"editEmbed": "編輯連結&嵌入",
|
"editEmbed": "編輯連結&嵌入",
|
||||||
|
@ -237,6 +246,7 @@
|
||||||
"selection": "選取",
|
"selection": "選取",
|
||||||
"image": "插入圖片",
|
"image": "插入圖片",
|
||||||
"rectangle": "長方形",
|
"rectangle": "長方形",
|
||||||
|
"blur": "模糊",
|
||||||
"diamond": "菱形",
|
"diamond": "菱形",
|
||||||
"ellipse": "橢圓",
|
"ellipse": "橢圓",
|
||||||
"arrow": "箭頭",
|
"arrow": "箭頭",
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,6 +152,7 @@ const renderElementToSvg = (
|
||||||
}
|
}
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "diamond":
|
case "diamond":
|
||||||
|
case "blur":
|
||||||
case "ellipse": {
|
case "ellipse": {
|
||||||
const shape = ShapeCache.generateElementShape(element, null);
|
const shape = ShapeCache.generateElementShape(element, null);
|
||||||
const node = roughSVGDrawWithPrecision(
|
const node = roughSVGDrawWithPrecision(
|
||||||
|
|
|
@ -145,6 +145,7 @@ export type ElementShape = Drawable | Drawable[] | null;
|
||||||
|
|
||||||
export type ElementShapes = {
|
export type ElementShapes = {
|
||||||
rectangle: Drawable;
|
rectangle: Drawable;
|
||||||
|
blur: Drawable;
|
||||||
ellipse: Drawable;
|
ellipse: Drawable;
|
||||||
diamond: Drawable;
|
diamond: Drawable;
|
||||||
iframe: Drawable;
|
iframe: Drawable;
|
||||||
|
|
|
@ -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))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS, assertNever } from "@excalidraw/comm
|
||||||
|
|
||||||
import {
|
import {
|
||||||
newArrowElement,
|
newArrowElement,
|
||||||
|
newBlurElement,
|
||||||
newElement,
|
newElement,
|
||||||
newEmbeddableElement,
|
newEmbeddableElement,
|
||||||
newFrameElement,
|
newFrameElement,
|
||||||
|
@ -361,6 +362,9 @@ export class API {
|
||||||
case "magicframe":
|
case "magicframe":
|
||||||
element = newMagicFrameElement({ ...base, width, height });
|
element = newMagicFrameElement({ ...base, width, height });
|
||||||
break;
|
break;
|
||||||
|
case "blur":
|
||||||
|
element = newBlurElement({ ...base, width, height, blur: appState.currentItemBlur});
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
assertNever(
|
assertNever(
|
||||||
type,
|
type,
|
||||||
|
|
|
@ -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,10 @@ 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";
|
||||||
|
|
||||||
|
import type { History } from "./history";
|
||||||
|
|
||||||
export type SocketId = string & { _brand: "SocketId" };
|
export type SocketId = string & { _brand: "SocketId" };
|
||||||
|
|
||||||
|
@ -150,7 +159,8 @@ export type ToolType =
|
||||||
| "frame"
|
| "frame"
|
||||||
| "magicframe"
|
| "magicframe"
|
||||||
| "embeddable"
|
| "embeddable"
|
||||||
| "laser";
|
| "laser"
|
||||||
|
| "blur";
|
||||||
|
|
||||||
export type ElementOrToolType = ExcalidrawElementType | ToolType | "custom";
|
export type ElementOrToolType = ExcalidrawElementType | ToolType | "custom";
|
||||||
|
|
||||||
|
@ -325,6 +335,7 @@ export interface AppState {
|
||||||
currentItemStrokeStyle: ExcalidrawElement["strokeStyle"];
|
currentItemStrokeStyle: ExcalidrawElement["strokeStyle"];
|
||||||
currentItemRoughness: number;
|
currentItemRoughness: number;
|
||||||
currentItemOpacity: number;
|
currentItemOpacity: number;
|
||||||
|
currentItemBlur: number;
|
||||||
currentItemFontFamily: FontFamilyValues;
|
currentItemFontFamily: FontFamilyValues;
|
||||||
currentItemFontSize: number;
|
currentItemFontSize: number;
|
||||||
currentItemTextAlign: TextAlign;
|
currentItemTextAlign: TextAlign;
|
||||||
|
@ -512,6 +523,104 @@ export type OnUserFollowedPayload = {
|
||||||
action: "FOLLOW" | "UNFOLLOW";
|
action: "FOLLOW" | "UNFOLLOW";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ExcalidrawPropsCustomOptions {
|
||||||
|
disableKeyEvents?: boolean;
|
||||||
|
hideMainToolbar?: boolean;
|
||||||
|
hideMenu?: boolean;
|
||||||
|
hideFooter?: boolean;
|
||||||
|
hideContextMenu?: boolean;
|
||||||
|
onHistoryChange?: (
|
||||||
|
history: History,
|
||||||
|
type: "undo" | "redo" | "record" | "clear",
|
||||||
|
) => void;
|
||||||
|
shouldResizeFromCenter?: (event: MouseEvent | KeyboardEvent) => boolean;
|
||||||
|
shouldMaintainAspectRatio?: (event: MouseEvent | KeyboardEvent) => boolean;
|
||||||
|
shouldRotateWithDiscreteAngle?: (
|
||||||
|
event: MouseEvent | KeyboardEvent | React.PointerEvent<HTMLCanvasElement>,
|
||||||
|
) => boolean;
|
||||||
|
shouldSnapping?: (event: KeyboardModifiersObject) => boolean;
|
||||||
|
onHandleEraser?: (elements: Set<ExcalidrawElement["id"]>) => void;
|
||||||
|
layoutRenders?: {
|
||||||
|
menuRender?: (props: { children: React.ReactNode }) => React.ReactNode;
|
||||||
|
};
|
||||||
|
pickerRenders?: {
|
||||||
|
ButtonList?: React.ComponentType<{ children: React.ReactNode; className?: string }>;
|
||||||
|
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;
|
||||||
|
visible?: boolean;
|
||||||
|
hidden?: boolean;
|
||||||
|
key?: string;
|
||||||
|
active?: boolean;
|
||||||
|
}) => 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;
|
||||||
|
getHistory: () => History | undefined;
|
||||||
|
historyRedo: () => void;
|
||||||
|
historyUndo: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExcalidrawHistoryContextType {
|
||||||
|
undoRef: React.RefObject<(() => void) | undefined>;
|
||||||
|
redoRef: React.RefObject<(() => void) | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ExcalidrawHistoryContext = createContext<
|
||||||
|
ExcalidrawHistoryContextType | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
export interface ExcalidrawProps {
|
export interface ExcalidrawProps {
|
||||||
onChange?: (
|
onChange?: (
|
||||||
elements: readonly OrderedExcalidrawElement[],
|
elements: readonly OrderedExcalidrawElement[],
|
||||||
|
@ -601,6 +710,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>;
|
||||||
renderScrollbars?: boolean;
|
renderScrollbars?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ import { getElementAbsoluteCoords } from "@excalidraw/element/bounds";
|
||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
|
ExcalidrawBlurElement,
|
||||||
ExcalidrawDiamondElement,
|
ExcalidrawDiamondElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawEllipseElement,
|
ExcalidrawEllipseElement,
|
||||||
|
@ -107,6 +108,7 @@ type RectangularElement =
|
||||||
| ExcalidrawDiamondElement
|
| ExcalidrawDiamondElement
|
||||||
| ExcalidrawFrameLikeElement
|
| ExcalidrawFrameLikeElement
|
||||||
| ExcalidrawEmbeddableElement
|
| ExcalidrawEmbeddableElement
|
||||||
|
| ExcalidrawBlurElement
|
||||||
| ExcalidrawImageElement
|
| ExcalidrawImageElement
|
||||||
| ExcalidrawIframeElement
|
| ExcalidrawIframeElement
|
||||||
| ExcalidrawTextElement
|
| ExcalidrawTextElement
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue