mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feats: 为 snow-shot 适配功能
This commit is contained in:
parent
82525f6c13
commit
5652b8e01b
35 changed files with 942 additions and 286 deletions
|
@ -438,6 +438,7 @@ export const TOOL_TYPE = {
|
|||
magicframe: "magicframe",
|
||||
embeddable: "embeddable",
|
||||
laser: "laser",
|
||||
blur: "blur",
|
||||
} as const;
|
||||
|
||||
export const EDITOR_LS_KEYS = {
|
||||
|
|
|
@ -98,6 +98,7 @@ export const generateRoughOptions = (
|
|||
case "iframe":
|
||||
case "embeddable":
|
||||
case "diamond":
|
||||
case "blur":
|
||||
case "ellipse": {
|
||||
options.fillStyle = element.fillStyle;
|
||||
options.fill = isTransparent(element.backgroundColor)
|
||||
|
@ -326,6 +327,7 @@ export const _generateElementShape = (
|
|||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "iframe":
|
||||
case "blur":
|
||||
case "embeddable": {
|
||||
let shape: ElementShapes[typeof element.type];
|
||||
// this is for rendering the stroke/bg of the embeddable, especially
|
||||
|
|
|
@ -10,7 +10,10 @@ export const hasBackground = (type: ElementOrToolType) =>
|
|||
type === "freedraw";
|
||||
|
||||
export const hasStrokeColor = (type: ElementOrToolType) =>
|
||||
type !== "image" && type !== "frame" && type !== "magicframe";
|
||||
type !== "image" &&
|
||||
type !== "frame" &&
|
||||
type !== "magicframe" &&
|
||||
type !== "blur";
|
||||
|
||||
export const hasStrokeWidth = (type: ElementOrToolType) =>
|
||||
type === "rectangle" ||
|
||||
|
@ -39,6 +42,10 @@ export const canChangeRoundness = (type: ElementOrToolType) =>
|
|||
type === "diamond" ||
|
||||
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 canHaveArrowheads = (type: ElementOrToolType) => type === "arrow";
|
||||
|
|
|
@ -46,6 +46,7 @@ import type {
|
|||
ExcalidrawArrowElement,
|
||||
FixedSegment,
|
||||
ExcalidrawElbowArrowElement,
|
||||
ExcalidrawBlurElement,
|
||||
} from "./types";
|
||||
|
||||
export type ElementConstructorOpts = MarkOptional<
|
||||
|
@ -212,6 +213,25 @@ export const newMagicFrameElement = (
|
|||
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 */
|
||||
const getTextElementPositionOffsets = (
|
||||
opts: {
|
||||
|
|
|
@ -404,6 +404,8 @@ const drawElementOnCanvas = (
|
|||
rc.draw(ShapeCache.get(element)!);
|
||||
break;
|
||||
}
|
||||
case "blur":
|
||||
break;
|
||||
case "arrow":
|
||||
case "line": {
|
||||
context.lineJoin = "round";
|
||||
|
@ -814,6 +816,7 @@ export const renderElement = (
|
|||
case "image":
|
||||
case "text":
|
||||
case "iframe":
|
||||
case "blur":
|
||||
case "embeddable": {
|
||||
// TODO investigate if we can do this in situ. Right now we need to call
|
||||
// beforehand because math helpers (such as getElementAbsoluteCoords)
|
||||
|
|
|
@ -58,6 +58,7 @@ export const getElementShape = <Point extends GlobalPoint | LocalPoint>(
|
|||
case "embeddable":
|
||||
case "image":
|
||||
case "iframe":
|
||||
case "blur":
|
||||
case "text":
|
||||
case "selection":
|
||||
return getPolygonShape(element);
|
||||
|
|
|
@ -28,6 +28,7 @@ import type {
|
|||
PointBinding,
|
||||
FixedPointBinding,
|
||||
ExcalidrawFlowchartNodeElement,
|
||||
ExcalidrawBlurElement,
|
||||
} from "./types";
|
||||
|
||||
export const isInitializedImageElement = (
|
||||
|
@ -107,6 +108,12 @@ export const isLinearElement = (
|
|||
return element != null && isLinearElementType(element.type);
|
||||
};
|
||||
|
||||
export const isBlurElement = (
|
||||
element?: ExcalidrawElement | null,
|
||||
): element is ExcalidrawBlurElement => {
|
||||
return element != null && isBlurElementType(element.type);
|
||||
};
|
||||
|
||||
export const isArrowElement = (
|
||||
element?: ExcalidrawElement | null,
|
||||
): element is ExcalidrawArrowElement => {
|
||||
|
@ -127,6 +134,10 @@ export const isLinearElementType = (
|
|||
);
|
||||
};
|
||||
|
||||
export const isBlurElementType = (elementType: ElementOrToolType): boolean => {
|
||||
return elementType === "blur";
|
||||
};
|
||||
|
||||
export const isBindingElement = (
|
||||
element?: ExcalidrawElement | null,
|
||||
includeLocked = true,
|
||||
|
@ -231,6 +242,7 @@ export const isExcalidrawElement = (
|
|||
case "frame":
|
||||
case "magicframe":
|
||||
case "image":
|
||||
case "blur":
|
||||
case "selection": {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -89,6 +89,11 @@ export type ExcalidrawRectangleElement = _ExcalidrawElementBase & {
|
|||
type: "rectangle";
|
||||
};
|
||||
|
||||
export type ExcalidrawBlurElement = _ExcalidrawElementBase & {
|
||||
type: "blur";
|
||||
blur: number;
|
||||
};
|
||||
|
||||
export type ExcalidrawDiamondElement = _ExcalidrawElementBase & {
|
||||
type: "diamond";
|
||||
};
|
||||
|
@ -212,7 +217,8 @@ export type ExcalidrawElement =
|
|||
| ExcalidrawFrameElement
|
||||
| ExcalidrawMagicFrameElement
|
||||
| ExcalidrawIframeElement
|
||||
| ExcalidrawEmbeddableElement;
|
||||
| ExcalidrawEmbeddableElement
|
||||
| ExcalidrawBlurElement;
|
||||
|
||||
export type ExcalidrawNonSelectionElement = Exclude<
|
||||
ExcalidrawElement,
|
||||
|
|
|
@ -8,6 +8,8 @@ import { KEYS, arrayToMap, getShortcutKey } from "@excalidraw/common";
|
|||
|
||||
import { alignElements } from "@excalidraw/element/align";
|
||||
|
||||
import { useContext } from "react";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type { Alignment } from "@excalidraw/element/align";
|
||||
|
@ -27,9 +29,14 @@ import { t } from "../i18n";
|
|||
import { isSomeElementSelected } from "../scene";
|
||||
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 = (
|
||||
appState: UIAppState,
|
||||
|
@ -87,19 +94,40 @@ export const actionAlignTop = register({
|
|||
},
|
||||
keyTest: (event) =>
|
||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
<ToolButton
|
||||
hidden={!alignActionsPredicate(appState, app)}
|
||||
type="button"
|
||||
icon={AlignTopIcon}
|
||||
onClick={() => updateData(null)}
|
||||
title={`${t("labels.alignTop")} — ${getShortcutKey(
|
||||
"CtrlOrCmd+Shift+Up",
|
||||
)}`}
|
||||
aria-label={t("labels.alignTop")}
|
||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||
/>
|
||||
),
|
||||
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
|
||||
hidden={!alignActionsPredicate(appState, app)}
|
||||
type="button"
|
||||
icon={AlignTopIcon}
|
||||
onClick={() => updateData(null)}
|
||||
title={`${t("labels.alignTop")} — ${getShortcutKey(
|
||||
"CtrlOrCmd+Shift+Up",
|
||||
)}`}
|
||||
aria-label={t("labels.alignTop")}
|
||||
visible={isSomeElementSelected(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const actionAlignBottom = register({
|
||||
|
@ -121,19 +149,40 @@ export const actionAlignBottom = register({
|
|||
},
|
||||
keyTest: (event) =>
|
||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
<ToolButton
|
||||
hidden={!alignActionsPredicate(appState, app)}
|
||||
type="button"
|
||||
icon={AlignBottomIcon}
|
||||
onClick={() => updateData(null)}
|
||||
title={`${t("labels.alignBottom")} — ${getShortcutKey(
|
||||
"CtrlOrCmd+Shift+Down",
|
||||
)}`}
|
||||
aria-label={t("labels.alignBottom")}
|
||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||
/>
|
||||
),
|
||||
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
|
||||
hidden={!alignActionsPredicate(appState, app)}
|
||||
type="button"
|
||||
icon={AlignBottomIcon}
|
||||
onClick={() => updateData(null)}
|
||||
title={`${t("labels.alignBottom")} — ${getShortcutKey(
|
||||
"CtrlOrCmd+Shift+Down",
|
||||
)}`}
|
||||
aria-label={t("labels.alignBottom")}
|
||||
visible={isSomeElementSelected(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const actionAlignLeft = register({
|
||||
|
@ -155,19 +204,40 @@ export const actionAlignLeft = register({
|
|||
},
|
||||
keyTest: (event) =>
|
||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
<ToolButton
|
||||
hidden={!alignActionsPredicate(appState, app)}
|
||||
type="button"
|
||||
icon={AlignLeftIcon}
|
||||
onClick={() => updateData(null)}
|
||||
title={`${t("labels.alignLeft")} — ${getShortcutKey(
|
||||
"CtrlOrCmd+Shift+Left",
|
||||
)}`}
|
||||
aria-label={t("labels.alignLeft")}
|
||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||
/>
|
||||
),
|
||||
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
|
||||
hidden={!alignActionsPredicate(appState, app)}
|
||||
type="button"
|
||||
icon={AlignLeftIcon}
|
||||
onClick={() => updateData(null)}
|
||||
title={`${t("labels.alignLeft")} — ${getShortcutKey(
|
||||
"CtrlOrCmd+Shift+Left",
|
||||
)}`}
|
||||
aria-label={t("labels.alignLeft")}
|
||||
visible={isSomeElementSelected(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const actionAlignRight = register({
|
||||
|
@ -189,19 +259,39 @@ export const actionAlignRight = register({
|
|||
},
|
||||
keyTest: (event) =>
|
||||
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
<ToolButton
|
||||
hidden={!alignActionsPredicate(appState, app)}
|
||||
type="button"
|
||||
icon={AlignRightIcon}
|
||||
onClick={() => updateData(null)}
|
||||
title={`${t("labels.alignRight")} — ${getShortcutKey(
|
||||
"CtrlOrCmd+Shift+Right",
|
||||
)}`}
|
||||
aria-label={t("labels.alignRight")}
|
||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||
/>
|
||||
),
|
||||
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
|
||||
hidden={!alignActionsPredicate(appState, app)}
|
||||
type="button"
|
||||
icon={AlignRightIcon}
|
||||
onClick={() => updateData(null)}
|
||||
title={`${t("labels.alignRight")} — ${getShortcutKey(
|
||||
"CtrlOrCmd+Shift+Right",
|
||||
)}`}
|
||||
aria-label={t("labels.alignRight")}
|
||||
visible={isSomeElementSelected(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const actionAlignVerticallyCentered = register({
|
||||
|
@ -221,17 +311,38 @@ export const actionAlignVerticallyCentered = register({
|
|||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
<ToolButton
|
||||
hidden={!alignActionsPredicate(appState, app)}
|
||||
type="button"
|
||||
icon={CenterVerticallyIcon}
|
||||
onClick={() => updateData(null)}
|
||||
title={t("labels.centerVertically")}
|
||||
aria-label={t("labels.centerVertically")}
|
||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||
/>
|
||||
),
|
||||
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
|
||||
hidden={!alignActionsPredicate(appState, app)}
|
||||
type="button"
|
||||
icon={CenterVerticallyIcon}
|
||||
onClick={() => updateData(null)}
|
||||
title={t("labels.centerVertically")}
|
||||
aria-label={t("labels.centerVertically")}
|
||||
visible={isSomeElementSelected(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const actionAlignHorizontallyCentered = register({
|
||||
|
@ -251,15 +362,36 @@ export const actionAlignHorizontallyCentered = register({
|
|||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
<ToolButton
|
||||
hidden={!alignActionsPredicate(appState, app)}
|
||||
type="button"
|
||||
icon={CenterHorizontallyIcon}
|
||||
onClick={() => updateData(null)}
|
||||
title={t("labels.centerHorizontally")}
|
||||
aria-label={t("labels.centerHorizontally")}
|
||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||
/>
|
||||
),
|
||||
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
|
||||
hidden={!alignActionsPredicate(appState, app)}
|
||||
type="button"
|
||||
icon={CenterHorizontallyIcon}
|
||||
onClick={() => updateData(null)}
|
||||
title={t("labels.centerHorizontally")}
|
||||
aria-label={t("labels.centerHorizontally")}
|
||||
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"
|
||||
/>
|
||||
),
|
||||
});
|
|
@ -20,6 +20,8 @@ import {
|
|||
selectGroupsForSelectedElements,
|
||||
} from "@excalidraw/element/groups";
|
||||
|
||||
import { useContext } from "react";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import { t } from "../i18n";
|
||||
|
@ -28,6 +30,8 @@ import { CaptureUpdateAction } from "../store";
|
|||
import { TrashIcon } from "../components/icons";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
|
||||
import { ExcalidrawPropsCustomOptionsContext } from "../types";
|
||||
|
||||
import { register } from "./register";
|
||||
|
||||
import type { AppClassProperties, AppState } from "../types";
|
||||
|
@ -310,14 +314,34 @@ export const actionDeleteSelected = register({
|
|||
keyTest: (event, appState, elements) =>
|
||||
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) &&
|
||||
!event[KEYS.CTRL_OR_CMD],
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<ToolButton
|
||||
type="button"
|
||||
icon={TrashIcon}
|
||||
title={t("labels.delete")}
|
||||
aria-label={t("labels.delete")}
|
||||
onClick={() => updateData(null)}
|
||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||
/>
|
||||
),
|
||||
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
|
||||
type="button"
|
||||
icon={TrashIcon}
|
||||
title={t("labels.delete")}
|
||||
aria-label={t("labels.delete")}
|
||||
onClick={() => updateData(null)}
|
||||
visible={isSomeElementSelected(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -8,6 +8,8 @@ import { updateFrameMembershipOfSelectedElements } from "@excalidraw/element/fra
|
|||
|
||||
import { distributeElements } from "@excalidraw/element/distribute";
|
||||
|
||||
import { useContext } from "react";
|
||||
|
||||
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||
|
||||
import type { Distribution } from "@excalidraw/element/distribute";
|
||||
|
@ -23,9 +25,13 @@ import { t } from "../i18n";
|
|||
import { isSomeElementSelected } from "../scene";
|
||||
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 selectedElements = app.scene.getSelectedElements(appState);
|
||||
|
@ -75,19 +81,40 @@ export const distributeHorizontally = register({
|
|||
},
|
||||
keyTest: (event) =>
|
||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H,
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
<ToolButton
|
||||
hidden={!enableActionGroup(appState, app)}
|
||||
type="button"
|
||||
icon={DistributeHorizontallyIcon}
|
||||
onClick={() => updateData(null)}
|
||||
title={`${t("labels.distributeHorizontally")} — ${getShortcutKey(
|
||||
"Alt+H",
|
||||
)}`}
|
||||
aria-label={t("labels.distributeHorizontally")}
|
||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||
/>
|
||||
),
|
||||
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
|
||||
hidden={!enableActionGroup(appState, app)}
|
||||
type="button"
|
||||
icon={DistributeHorizontallyIcon}
|
||||
onClick={() => updateData(null)}
|
||||
title={`${t("labels.distributeHorizontally")} — ${getShortcutKey(
|
||||
"Alt+H",
|
||||
)}`}
|
||||
aria-label={t("labels.distributeHorizontally")}
|
||||
visible={isSomeElementSelected(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const distributeVertically = register({
|
||||
|
@ -106,15 +133,38 @@ export const distributeVertically = register({
|
|||
},
|
||||
keyTest: (event) =>
|
||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
<ToolButton
|
||||
hidden={!enableActionGroup(appState, app)}
|
||||
type="button"
|
||||
icon={DistributeVerticallyIcon}
|
||||
onClick={() => updateData(null)}
|
||||
title={`${t("labels.distributeVertically")} — ${getShortcutKey("Alt+V")}`}
|
||||
aria-label={t("labels.distributeVertically")}
|
||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||
/>
|
||||
),
|
||||
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
|
||||
hidden={!enableActionGroup(appState, app)}
|
||||
type="button"
|
||||
icon={DistributeVerticallyIcon}
|
||||
onClick={() => updateData(null)}
|
||||
title={`${t("labels.distributeVertically")} — ${getShortcutKey(
|
||||
"Alt+V",
|
||||
)}`}
|
||||
aria-label={t("labels.distributeVertically")}
|
||||
visible={isSomeElementSelected(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -18,6 +18,8 @@ import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
|||
|
||||
import { duplicateElements } from "@excalidraw/element/duplicate";
|
||||
|
||||
import { useContext } from "react";
|
||||
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { DuplicateIcon } from "../components/icons";
|
||||
|
||||
|
@ -25,6 +27,8 @@ import { t } from "../i18n";
|
|||
import { isSomeElementSelected } from "../scene";
|
||||
import { CaptureUpdateAction } from "../store";
|
||||
|
||||
import { ExcalidrawPropsCustomOptionsContext } from "../types";
|
||||
|
||||
import { register } from "./register";
|
||||
|
||||
export const actionDuplicateSelection = register({
|
||||
|
@ -105,16 +109,36 @@ export const actionDuplicateSelection = register({
|
|||
};
|
||||
},
|
||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.D,
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<ToolButton
|
||||
type="button"
|
||||
icon={DuplicateIcon}
|
||||
title={`${t("labels.duplicateSelection")} — ${getShortcutKey(
|
||||
"CtrlOrCmd+D",
|
||||
)}`}
|
||||
aria-label={t("labels.duplicateSelection")}
|
||||
onClick={() => updateData(null)}
|
||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||
/>
|
||||
),
|
||||
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
|
||||
type="button"
|
||||
icon={DuplicateIcon}
|
||||
title={`${t("labels.duplicateSelection")} — ${getShortcutKey(
|
||||
"CtrlOrCmd+D",
|
||||
)}`}
|
||||
aria-label={t("labels.duplicateSelection")}
|
||||
onClick={() => updateData(null)}
|
||||
visible={isSomeElementSelected(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -28,6 +28,8 @@ import {
|
|||
|
||||
import { syncMovedIndices } from "@excalidraw/element/fractionalIndex";
|
||||
|
||||
import { useContext } from "react";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawTextElement,
|
||||
|
@ -42,9 +44,13 @@ import { t } from "../i18n";
|
|||
import { isSomeElementSelected } from "../scene";
|
||||
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[]) => {
|
||||
if (elements.length >= 2) {
|
||||
|
@ -195,17 +201,38 @@ export const actionGroup = register({
|
|||
enableActionGroup(elements, appState, app),
|
||||
keyTest: (event) =>
|
||||
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
<ToolButton
|
||||
hidden={!enableActionGroup(elements, appState, app)}
|
||||
type="button"
|
||||
icon={<GroupIcon theme={appState.theme} />}
|
||||
onClick={() => updateData(null)}
|
||||
title={`${t("labels.group")} — ${getShortcutKey("CtrlOrCmd+G")}`}
|
||||
aria-label={t("labels.group")}
|
||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||
></ToolButton>
|
||||
),
|
||||
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
|
||||
hidden={!enableActionGroup(elements, appState, app)}
|
||||
type="button"
|
||||
icon={<GroupIcon theme={appState.theme} />}
|
||||
onClick={() => updateData(null)}
|
||||
title={`${t("labels.group")} — ${getShortcutKey("CtrlOrCmd+G")}`}
|
||||
aria-label={t("labels.group")}
|
||||
visible={isSomeElementSelected(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
)}
|
||||
></ToolButton>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const actionUngroup = register({
|
||||
|
@ -304,15 +331,38 @@ export const actionUngroup = register({
|
|||
event.key === KEYS.G.toUpperCase(),
|
||||
predicate: (elements, appState) => getSelectedGroupIds(appState).length > 0,
|
||||
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<ToolButton
|
||||
type="button"
|
||||
hidden={getSelectedGroupIds(appState).length === 0}
|
||||
icon={<UngroupIcon theme={appState.theme} />}
|
||||
onClick={() => updateData(null)}
|
||||
title={`${t("labels.ungroup")} — ${getShortcutKey("CtrlOrCmd+Shift+G")}`}
|
||||
aria-label={t("labels.ungroup")}
|
||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||
></ToolButton>
|
||||
),
|
||||
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
|
||||
type="button"
|
||||
hidden={getSelectedGroupIds(appState).length === 0}
|
||||
icon={<UngroupIcon theme={appState.theme} />}
|
||||
onClick={() => updateData(null)}
|
||||
title={`${t("labels.ungroup")} — ${getShortcutKey(
|
||||
"CtrlOrCmd+Shift+G",
|
||||
)}`}
|
||||
aria-label={t("labels.ungroup")}
|
||||
visible={isSomeElementSelected(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
)}
|
||||
></ToolButton>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { isWindows, KEYS, matchKey, arrayToMap } from "@excalidraw/common";
|
||||
|
||||
import { useContext, useEffect } from "react";
|
||||
|
||||
import type { SceneElementsMap } from "@excalidraw/element/types";
|
||||
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
|
@ -9,9 +11,14 @@ import { useEmitter } from "../hooks/useEmitter";
|
|||
import { t } from "../i18n";
|
||||
import { CaptureUpdateAction } from "../store";
|
||||
|
||||
import {
|
||||
ExcalidrawHistoryContext,
|
||||
type AppClassProperties,
|
||||
type AppState,
|
||||
} from "../types";
|
||||
|
||||
import type { History } from "../history";
|
||||
import type { Store } from "../store";
|
||||
import type { AppClassProperties, AppState } from "../types";
|
||||
import type { Action, ActionResult } from "./types";
|
||||
|
||||
const executeHistoryAction = (
|
||||
|
@ -74,6 +81,12 @@ export const createUndoAction: ActionCreator = (history, store) => ({
|
|||
),
|
||||
);
|
||||
|
||||
const historyContext = useContext(ExcalidrawHistoryContext);
|
||||
historyContext &&
|
||||
(historyContext.undoRef.current = () => {
|
||||
updateData();
|
||||
});
|
||||
|
||||
return (
|
||||
<ToolButton
|
||||
type="button"
|
||||
|
@ -114,6 +127,12 @@ export const createRedoAction: ActionCreator = (history, store) => ({
|
|||
),
|
||||
);
|
||||
|
||||
const historyContext = useContext(ExcalidrawHistoryContext);
|
||||
historyContext &&
|
||||
(historyContext.redoRef.current = () => {
|
||||
updateData();
|
||||
});
|
||||
|
||||
return (
|
||||
<ToolButton
|
||||
type="button"
|
||||
|
|
|
@ -2,6 +2,8 @@ import { isEmbeddableElement } from "@excalidraw/element/typeChecks";
|
|||
|
||||
import { KEYS, getShortcutKey } from "@excalidraw/common";
|
||||
|
||||
import { useContext } from "react";
|
||||
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { getContextMenuLabel } from "../components/hyperlink/Hyperlink";
|
||||
import { LinkIcon } from "../components/icons";
|
||||
|
@ -10,6 +12,8 @@ import { t } from "../i18n";
|
|||
import { getSelectedElements } from "../scene";
|
||||
import { CaptureUpdateAction } from "../store";
|
||||
|
||||
import { ExcalidrawPropsCustomOptionsContext } from "../types";
|
||||
|
||||
import { register } from "./register";
|
||||
|
||||
export const actionLink = register({
|
||||
|
@ -40,6 +44,20 @@ export const actionLink = register({
|
|||
PanelComponent: ({ elements, appState, updateData }) => {
|
||||
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 (
|
||||
<ToolButton
|
||||
type="button"
|
||||
|
|
|
@ -134,9 +134,6 @@ import {
|
|||
} from "../scene";
|
||||
import { CaptureUpdateAction } from "../store";
|
||||
|
||||
import { register } from "./register";
|
||||
|
||||
import type { CaptureUpdateActionType } from "../store";
|
||||
import {
|
||||
ExcalidrawPropsCustomOptionsContext,
|
||||
type AppClassProperties,
|
||||
|
@ -144,6 +141,10 @@ import {
|
|||
type Primitive,
|
||||
} from "../types";
|
||||
|
||||
import { register } from "./register";
|
||||
|
||||
import type { CaptureUpdateActionType } from "../store";
|
||||
|
||||
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
||||
|
||||
export const changeProperty = (
|
||||
|
|
|
@ -20,6 +20,8 @@ export {
|
|||
actionChangeVerticalAlign,
|
||||
} from "./actionProperties";
|
||||
|
||||
export { actionChangeBlur } from "./actionBlur";
|
||||
|
||||
export {
|
||||
actionChangeViewBackgroundColor,
|
||||
actionClearCanvas,
|
||||
|
|
|
@ -140,7 +140,8 @@ export type ActionName =
|
|||
| "linkToElement"
|
||||
| "cropEditor"
|
||||
| "wrapSelectionInFrame"
|
||||
| "toggleLassoTool";
|
||||
| "toggleLassoTool"
|
||||
| "changeBlur";
|
||||
|
||||
export type PanelComponentProps = {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
|
|
|
@ -33,6 +33,7 @@ export const getDefaultAppState = (): Omit<
|
|||
currentItemFontFamily: DEFAULT_FONT_FAMILY,
|
||||
currentItemFontSize: DEFAULT_FONT_SIZE,
|
||||
currentItemOpacity: DEFAULT_ELEMENT_PROPS.opacity,
|
||||
currentItemBlur: 50,
|
||||
currentItemRoughness: DEFAULT_ELEMENT_PROPS.roughness,
|
||||
currentItemStartArrowhead: null,
|
||||
currentItemStrokeColor: DEFAULT_ELEMENT_PROPS.strokeColor,
|
||||
|
@ -161,6 +162,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||
server: false,
|
||||
},
|
||||
currentItemOpacity: { browser: true, export: false, server: false },
|
||||
currentItemBlur: { browser: true, export: false, server: false },
|
||||
currentItemRoughness: { browser: true, export: false, server: false },
|
||||
currentItemStartArrowhead: { browser: true, export: false, server: false },
|
||||
currentItemStrokeColor: { browser: true, export: false, server: false },
|
||||
|
|
|
@ -21,7 +21,12 @@ import {
|
|||
isTextElement,
|
||||
} from "@excalidraw/element/typeChecks";
|
||||
|
||||
import { hasStrokeColor, toolIsArrow } from "@excalidraw/element/comparisons";
|
||||
import {
|
||||
canChangeBlur,
|
||||
canChangeLayer,
|
||||
hasStrokeColor,
|
||||
toolIsArrow,
|
||||
} from "@excalidraw/element/comparisons";
|
||||
|
||||
import type {
|
||||
ExcalidrawElement,
|
||||
|
@ -88,6 +93,7 @@ export const canChangeStrokeColor = (
|
|||
(hasStrokeColor(appState.activeTool.type) &&
|
||||
commonSelectedType !== "image" &&
|
||||
commonSelectedType !== "frame" &&
|
||||
commonSelectedType !== "blur" &&
|
||||
commonSelectedType !== "magicframe") ||
|
||||
targetElements.some((element) => hasStrokeColor(element.type))
|
||||
);
|
||||
|
@ -157,6 +163,12 @@ export const SelectedShapeActions = ({
|
|||
const showAlignActions =
|
||||
!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 (
|
||||
<div className="panelColumn">
|
||||
<div>
|
||||
|
@ -212,82 +224,138 @@ export const SelectedShapeActions = ({
|
|||
<>{renderAction("changeArrowhead")}</>
|
||||
)}
|
||||
|
||||
{(canChangeBlur(appState.activeTool.type) ||
|
||||
targetElements.some((element) => canChangeBlur(element.type))) &&
|
||||
renderAction("changeBlur")}
|
||||
|
||||
{renderAction("changeOpacity")}
|
||||
|
||||
<fieldset>
|
||||
<legend>{t("labels.layers")}</legend>
|
||||
{!customOptions?.pickerRenders?.ButtonList && (
|
||||
<div className={"buttonList"}>
|
||||
{renderAction("sendToBack")}
|
||||
{renderAction("sendBackward")}
|
||||
{renderAction("bringForward")}
|
||||
{renderAction("bringToFront")}
|
||||
</div>
|
||||
)}
|
||||
{customOptions?.pickerRenders?.ButtonList && (
|
||||
<customOptions.pickerRenders.ButtonList>
|
||||
{renderAction("sendToBack")}
|
||||
{renderAction("sendBackward")}
|
||||
{renderAction("bringForward")}
|
||||
{renderAction("bringToFront")}
|
||||
</customOptions.pickerRenders.ButtonList>
|
||||
)}
|
||||
</fieldset>
|
||||
{showLayerActions && (
|
||||
<fieldset>
|
||||
<legend>{t("labels.layers")}</legend>
|
||||
{!customOptions?.pickerRenders?.ButtonList && (
|
||||
<div className={"buttonList"}>
|
||||
{renderAction("sendToBack")}
|
||||
{renderAction("sendBackward")}
|
||||
{renderAction("bringForward")}
|
||||
{renderAction("bringToFront")}
|
||||
</div>
|
||||
)}
|
||||
{customOptions?.pickerRenders?.ButtonList && (
|
||||
<customOptions.pickerRenders.ButtonList>
|
||||
{renderAction("sendToBack")}
|
||||
{renderAction("sendBackward")}
|
||||
{renderAction("bringForward")}
|
||||
{renderAction("bringToFront")}
|
||||
</customOptions.pickerRenders.ButtonList>
|
||||
)}
|
||||
</fieldset>
|
||||
)}
|
||||
|
||||
{showAlignActions && !isSingleElementBoundContainer && (
|
||||
<fieldset>
|
||||
<legend>{t("labels.align")}</legend>
|
||||
<div className="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")}
|
||||
{/* breaks the row ˇˇ */}
|
||||
<div style={{ flexBasis: "100%", height: 0 }} />
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: ".5rem",
|
||||
marginTop: "-0.5rem",
|
||||
}}
|
||||
>
|
||||
{renderAction("alignTop")}
|
||||
{renderAction("alignVerticallyCentered")}
|
||||
{renderAction("alignBottom")}
|
||||
{!customOptions?.pickerRenders?.ButtonList && (
|
||||
<div className="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("distributeVertically")}
|
||||
renderAction("distributeHorizontally")}
|
||||
{/* breaks the row ˇˇ */}
|
||||
<div style={{ flexBasis: "100%", height: 0 }} />
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
gap: ".5rem",
|
||||
marginTop: "-0.5rem",
|
||||
}}
|
||||
>
|
||||
{renderAction("alignTop")}
|
||||
{renderAction("alignVerticallyCentered")}
|
||||
{renderAction("alignBottom")}
|
||||
{targetElements.length > 2 &&
|
||||
renderAction("distributeVertically")}
|
||||
</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>
|
||||
)}
|
||||
{!isEditingTextOrNewElement && targetElements.length > 0 && (
|
||||
<fieldset>
|
||||
<legend>{t("labels.actions")}</legend>
|
||||
<div className="buttonList">
|
||||
{!device.editor.isMobile && renderAction("duplicateSelection")}
|
||||
{!device.editor.isMobile && renderAction("deleteSelectedElements")}
|
||||
{renderAction("group")}
|
||||
{renderAction("ungroup")}
|
||||
{showLinkIcon && renderAction("hyperlink")}
|
||||
{showCropEditorAction && renderAction("cropEditor")}
|
||||
{showLineEditorAction && renderAction("toggleLinearEditor")}
|
||||
</div>
|
||||
{!customOptions?.pickerRenders?.ButtonList && (
|
||||
<div className="buttonList">
|
||||
{!device.editor.isMobile && renderAction("duplicateSelection")}
|
||||
{!device.editor.isMobile &&
|
||||
renderAction("deleteSelectedElements")}
|
||||
{renderAction("group")}
|
||||
{renderAction("ungroup")}
|
||||
{showLinkIcon && renderAction("hyperlink")}
|
||||
{showCropEditorAction && renderAction("cropEditor")}
|
||||
{showLineEditorAction && renderAction("toggleLinearEditor")}
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -143,6 +143,7 @@ import {
|
|||
newLinearElement,
|
||||
newTextElement,
|
||||
refreshTextDimensions,
|
||||
newBlurElement,
|
||||
} from "@excalidraw/element/newElement";
|
||||
|
||||
import {
|
||||
|
@ -646,6 +647,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||
public id: string;
|
||||
private store: Store;
|
||||
private history: History;
|
||||
|
||||
public getHistory = () => this.history;
|
||||
|
||||
public excalidrawContainerValue: {
|
||||
container: HTMLDivElement | null;
|
||||
id: string;
|
||||
|
@ -806,7 +810,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
};
|
||||
|
||||
this.fonts = new Fonts(this.scene);
|
||||
this.history = new History();
|
||||
this.history = new History(this.props.customOptions?.onHistoryChange);
|
||||
|
||||
this.actionManager.registerAll(actions);
|
||||
this.actionManager.registerAction(
|
||||
|
@ -2288,19 +2292,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||
errorMessage,
|
||||
};
|
||||
|
||||
// Print differences between prevAppState and res
|
||||
const differences = Object.keys(res).filter((key) => {
|
||||
return (prevAppState as any)[key] !== (res as any)[key];
|
||||
});
|
||||
console.log(
|
||||
"State differences:",
|
||||
differences.map((key) => ({
|
||||
key,
|
||||
prev: (prevAppState as any)[key],
|
||||
new: (res as any)[key],
|
||||
})),
|
||||
);
|
||||
|
||||
return res;
|
||||
});
|
||||
|
||||
|
@ -6270,6 +6261,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
this.elementsPendingErasure = new Set(elementsToErase);
|
||||
this.triggerRender();
|
||||
this.props.customOptions?.onHandleEraser?.(this.elementsPendingErasure);
|
||||
};
|
||||
|
||||
// set touch moving for mobile context menu
|
||||
|
@ -7850,7 +7842,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
| "diamond"
|
||||
| "ellipse"
|
||||
| "iframe"
|
||||
| "embeddable",
|
||||
| "embeddable"
|
||||
| "blur",
|
||||
) {
|
||||
return this.state.currentItemRoundness === "round"
|
||||
? {
|
||||
|
@ -7862,7 +7855,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
|
||||
private createGenericElementOnPointerDown = (
|
||||
elementType: ExcalidrawGenericElement["type"] | "embeddable",
|
||||
elementType: ExcalidrawGenericElement["type"] | "embeddable" | "blur",
|
||||
pointerDownState: PointerDownState,
|
||||
): void => {
|
||||
const [gridX, gridY] = getGridPoint(
|
||||
|
@ -7899,6 +7892,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||
type: "embeddable",
|
||||
...baseElementAttributes,
|
||||
});
|
||||
} else if (elementType === "blur") {
|
||||
element = newBlurElement({
|
||||
...baseElementAttributes,
|
||||
blur: this.state.currentItemBlur,
|
||||
});
|
||||
} else {
|
||||
element = newElement({
|
||||
type: elementType,
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import React, { useCallback, useContext, useEffect } from "react";
|
||||
import React, { useContext, useEffect } from "react";
|
||||
|
||||
import { isBlurElement } from "@excalidraw/element/typeChecks";
|
||||
|
||||
import { getFormValue } from "../actions/actionProperties";
|
||||
import { t } from "../i18n";
|
||||
|
@ -80,3 +82,76 @@ export const Range = ({
|
|||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,8 +9,8 @@ import {
|
|||
LineIcon,
|
||||
FreedrawIcon,
|
||||
TextIcon,
|
||||
ImageIcon,
|
||||
EraserIcon,
|
||||
ImageIcon,
|
||||
} from "./icons";
|
||||
|
||||
export const SHAPES = [
|
||||
|
@ -77,6 +77,13 @@ export const SHAPES = [
|
|||
numericKey: KEYS["9"],
|
||||
fillable: false,
|
||||
},
|
||||
{
|
||||
icon: RectangleIcon,
|
||||
key: null,
|
||||
value: "blur",
|
||||
numericKey: KEYS["9"],
|
||||
fillable: false,
|
||||
},
|
||||
{
|
||||
icon: EraserIcon,
|
||||
value: "eraser",
|
||||
|
|
|
@ -102,6 +102,7 @@ export const AllowedExcalidrawActiveTools: Record<
|
|||
hand: true,
|
||||
laser: false,
|
||||
magicframe: false,
|
||||
blur: true,
|
||||
};
|
||||
|
||||
export type RestoredDataState = {
|
||||
|
@ -383,6 +384,7 @@ const restoreElement = (
|
|||
|
||||
// generic elements
|
||||
case "ellipse":
|
||||
case "blur":
|
||||
case "rectangle":
|
||||
case "diamond":
|
||||
case "iframe":
|
||||
|
|
|
@ -20,6 +20,13 @@ export class History {
|
|||
[HistoryChangedEvent]
|
||||
>();
|
||||
|
||||
constructor(
|
||||
private readonly onChange: (
|
||||
history: History,
|
||||
type: "undo" | "redo" | "record" | "clear",
|
||||
) => void = () => {},
|
||||
) {}
|
||||
|
||||
private readonly undoStack: HistoryStack = [];
|
||||
private readonly redoStack: HistoryStack = [];
|
||||
|
||||
|
@ -34,6 +41,8 @@ export class History {
|
|||
public clear() {
|
||||
this.undoStack.length = 0;
|
||||
this.redoStack.length = 0;
|
||||
|
||||
this.onChange(this, "clear");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -43,6 +52,8 @@ export class History {
|
|||
elementsChange: ElementsChange,
|
||||
appStateChange: AppStateChange,
|
||||
) {
|
||||
this.onChange(this, "record");
|
||||
|
||||
const entry = HistoryEntry.create(appStateChange, elementsChange);
|
||||
|
||||
if (!entry.isEmpty()) {
|
||||
|
@ -67,6 +78,8 @@ export class History {
|
|||
appState: AppState,
|
||||
snapshot: Readonly<Snapshot>,
|
||||
) {
|
||||
this.onChange(this, "undo");
|
||||
|
||||
return this.perform(
|
||||
elements,
|
||||
appState,
|
||||
|
@ -81,6 +94,8 @@ export class History {
|
|||
appState: AppState,
|
||||
snapshot: Readonly<Snapshot>,
|
||||
) {
|
||||
this.onChange(this, "redo");
|
||||
|
||||
return this.perform(
|
||||
elements,
|
||||
appState,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useImperativeHandle, useRef } from "react";
|
||||
import React, { useEffect, useImperativeHandle, useMemo, useRef } from "react";
|
||||
|
||||
import { DEFAULT_UI_OPTIONS, isShallowEqual } from "@excalidraw/common";
|
||||
|
||||
|
@ -16,7 +16,15 @@ import "./css/app.scss";
|
|||
import "./css/styles.scss";
|
||||
import "./fonts/fonts.css";
|
||||
|
||||
import { ExcalidrawPropsCustomOptionsContext, type AppProps, type ExcalidrawProps } from "./types";
|
||||
import {
|
||||
ExcalidrawHistoryContext,
|
||||
ExcalidrawPropsCustomOptionsContext,
|
||||
type AppProps,
|
||||
type ExcalidrawProps,
|
||||
} from "./types";
|
||||
|
||||
import type { ExcalidrawHistoryContextType } from "./types";
|
||||
|
||||
import type { ActionResult } from "./actions/types";
|
||||
|
||||
polyfill();
|
||||
|
@ -112,60 +120,81 @@ 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 (
|
||||
<ExcalidrawPropsCustomOptionsContext.Provider value={customOptions}>
|
||||
<EditorJotaiProvider store={editorJotaiStore}>
|
||||
<InitializeApp langCode={langCode} theme={theme}>
|
||||
<App
|
||||
ref={appRef}
|
||||
onChange={onChange}
|
||||
initialData={initialData}
|
||||
excalidrawAPI={excalidrawAPI}
|
||||
isCollaborating={isCollaborating}
|
||||
onPointerUpdate={onPointerUpdate}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
langCode={langCode}
|
||||
viewModeEnabled={viewModeEnabled}
|
||||
zenModeEnabled={zenModeEnabled}
|
||||
gridModeEnabled={gridModeEnabled}
|
||||
libraryReturnUrl={libraryReturnUrl}
|
||||
theme={theme}
|
||||
name={name}
|
||||
renderCustomStats={renderCustomStats}
|
||||
UIOptions={UIOptions}
|
||||
onPaste={onPaste}
|
||||
detectScroll={detectScroll}
|
||||
handleKeyboardGlobally={handleKeyboardGlobally}
|
||||
onLibraryChange={onLibraryChange}
|
||||
autoFocus={autoFocus}
|
||||
generateIdForFile={generateIdForFile}
|
||||
onLinkOpen={onLinkOpen}
|
||||
generateLinkForSelection={generateLinkForSelection}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerUp={onPointerUp}
|
||||
onScrollChange={onScrollChange}
|
||||
onDuplicate={onDuplicate}
|
||||
validateEmbeddable={validateEmbeddable}
|
||||
renderEmbeddable={renderEmbeddable}
|
||||
aiEnabled={aiEnabled !== false}
|
||||
showDeprecatedFonts={showDeprecatedFonts}
|
||||
customOptions={customOptions}
|
||||
>
|
||||
{children}
|
||||
</App>
|
||||
</InitializeApp>
|
||||
</EditorJotaiProvider>
|
||||
</ExcalidrawPropsCustomOptionsContext.Provider>
|
||||
<ExcalidrawHistoryContext.Provider value={historyContextValue}>
|
||||
<ExcalidrawPropsCustomOptionsContext.Provider value={customOptions}>
|
||||
<EditorJotaiProvider store={editorJotaiStore}>
|
||||
<InitializeApp langCode={langCode} theme={theme}>
|
||||
<App
|
||||
ref={appRef}
|
||||
onChange={onChange}
|
||||
initialData={initialData}
|
||||
excalidrawAPI={excalidrawAPI}
|
||||
isCollaborating={isCollaborating}
|
||||
onPointerUpdate={onPointerUpdate}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
langCode={langCode}
|
||||
viewModeEnabled={viewModeEnabled}
|
||||
zenModeEnabled={zenModeEnabled}
|
||||
gridModeEnabled={gridModeEnabled}
|
||||
libraryReturnUrl={libraryReturnUrl}
|
||||
theme={theme}
|
||||
name={name}
|
||||
renderCustomStats={renderCustomStats}
|
||||
UIOptions={UIOptions}
|
||||
onPaste={onPaste}
|
||||
detectScroll={detectScroll}
|
||||
handleKeyboardGlobally={handleKeyboardGlobally}
|
||||
onLibraryChange={onLibraryChange}
|
||||
autoFocus={autoFocus}
|
||||
generateIdForFile={generateIdForFile}
|
||||
onLinkOpen={onLinkOpen}
|
||||
generateLinkForSelection={generateLinkForSelection}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerUp={onPointerUp}
|
||||
onScrollChange={onScrollChange}
|
||||
onDuplicate={onDuplicate}
|
||||
validateEmbeddable={validateEmbeddable}
|
||||
renderEmbeddable={renderEmbeddable}
|
||||
aiEnabled={aiEnabled !== false}
|
||||
showDeprecatedFonts={showDeprecatedFonts}
|
||||
customOptions={customOptions}
|
||||
>
|
||||
{children}
|
||||
</App>
|
||||
</InitializeApp>
|
||||
</EditorJotaiProvider>
|
||||
</ExcalidrawPropsCustomOptionsContext.Provider>
|
||||
</ExcalidrawHistoryContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -127,6 +127,7 @@
|
|||
"unbindText": "Unbind text",
|
||||
"bindText": "Bind text to the container",
|
||||
"createContainerFromText": "Wrap text in a container",
|
||||
"blur": "Strength",
|
||||
"link": {
|
||||
"edit": "Edit link",
|
||||
"editEmbed": "Edit embeddable link",
|
||||
|
@ -279,6 +280,7 @@
|
|||
"lasso": "Lasso selection",
|
||||
"image": "Insert image",
|
||||
"rectangle": "Rectangle",
|
||||
"blur": "Blur",
|
||||
"diamond": "Diamond",
|
||||
"ellipse": "Ellipse",
|
||||
"arrow": "Arrow",
|
||||
|
@ -312,6 +314,7 @@
|
|||
"magicframe": "Wireframe to code",
|
||||
"embeddable": "Web Embed",
|
||||
"selection": "Selection",
|
||||
"blur": "Blur",
|
||||
"iframe": "IFrame"
|
||||
},
|
||||
"headings": {
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
"arrowhead_triangle_outline": "三角箭头(空心)",
|
||||
"arrowhead_diamond": "菱形",
|
||||
"arrowhead_diamond_outline": "菱形(空心)",
|
||||
"arrowtypes": "箭头类型",
|
||||
"fontSize": "字体大小",
|
||||
"fontFamily": "字体",
|
||||
"addWatermark": "添加 “使用 Excalidraw 创建” 水印",
|
||||
|
@ -113,6 +114,7 @@
|
|||
"unbindText": "取消文本绑定",
|
||||
"bindText": "将文本绑定到容器",
|
||||
"createContainerFromText": "将文本包围在容器中",
|
||||
"blur": "强度",
|
||||
"link": {
|
||||
"edit": "编辑链接",
|
||||
"editEmbed": "编辑链接与嵌入",
|
||||
|
@ -237,6 +239,7 @@
|
|||
"selection": "选择",
|
||||
"image": "插入图像",
|
||||
"rectangle": "矩形",
|
||||
"blur": "模糊",
|
||||
"diamond": "菱形",
|
||||
"ellipse": "椭圆",
|
||||
"arrow": "箭头",
|
||||
|
|
|
@ -113,6 +113,7 @@
|
|||
"unbindText": "取消綁定文字",
|
||||
"bindText": "結合文字至容器",
|
||||
"createContainerFromText": "將文字包於容器中",
|
||||
"blur": "強度",
|
||||
"link": {
|
||||
"edit": "編輯連結",
|
||||
"editEmbed": "編輯連結&嵌入",
|
||||
|
@ -237,6 +238,7 @@
|
|||
"selection": "選取",
|
||||
"image": "插入圖片",
|
||||
"rectangle": "長方形",
|
||||
"blur": "模糊",
|
||||
"diamond": "菱形",
|
||||
"ellipse": "橢圓",
|
||||
"arrow": "箭頭",
|
||||
|
|
|
@ -152,6 +152,7 @@ const renderElementToSvg = (
|
|||
}
|
||||
case "rectangle":
|
||||
case "diamond":
|
||||
case "blur":
|
||||
case "ellipse": {
|
||||
const shape = ShapeCache.generateElementShape(element, null);
|
||||
const node = roughSVGDrawWithPrecision(
|
||||
|
|
|
@ -143,6 +143,7 @@ export type ElementShape = Drawable | Drawable[] | null;
|
|||
|
||||
export type ElementShapes = {
|
||||
rectangle: Drawable;
|
||||
blur: Drawable;
|
||||
ellipse: Drawable;
|
||||
diamond: Drawable;
|
||||
iframe: Drawable;
|
||||
|
|
|
@ -9,6 +9,7 @@ import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS, assertNever } from "@excalidraw/comm
|
|||
import { mutateElement } from "@excalidraw/element/mutateElement";
|
||||
import {
|
||||
newArrowElement,
|
||||
newBlurElement,
|
||||
newElement,
|
||||
newEmbeddableElement,
|
||||
newFrameElement,
|
||||
|
@ -356,12 +357,15 @@ export class API {
|
|||
scale: rest.scale || [1, 1],
|
||||
});
|
||||
break;
|
||||
case "frame":
|
||||
element = newFrameElement({ ...base, width, height });
|
||||
break;
|
||||
case "frame":
|
||||
element = newFrameElement({ ...base, width, height });
|
||||
break;
|
||||
case "magicframe":
|
||||
element = newMagicFrameElement({ ...base, width, height });
|
||||
break;
|
||||
case "blur":
|
||||
element = newBlurElement({ ...base, width, height, blur: appState.currentItemBlur});
|
||||
break;
|
||||
default:
|
||||
assertNever(
|
||||
type,
|
||||
|
|
|
@ -66,6 +66,8 @@ import type React 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 Collaborator = Readonly<{
|
||||
|
@ -157,7 +159,8 @@ export type ToolType =
|
|||
| "frame"
|
||||
| "magicframe"
|
||||
| "embeddable"
|
||||
| "laser";
|
||||
| "laser"
|
||||
| "blur";
|
||||
|
||||
export type ElementOrToolType = ExcalidrawElementType | ToolType | "custom";
|
||||
|
||||
|
@ -332,6 +335,7 @@ export interface AppState {
|
|||
currentItemStrokeStyle: ExcalidrawElement["strokeStyle"];
|
||||
currentItemRoughness: number;
|
||||
currentItemOpacity: number;
|
||||
currentItemBlur: number;
|
||||
currentItemFontFamily: FontFamilyValues;
|
||||
currentItemFontSize: number;
|
||||
currentItemTextAlign: TextAlign;
|
||||
|
@ -525,12 +529,17 @@ export interface ExcalidrawPropsCustomOptions {
|
|||
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;
|
||||
};
|
||||
|
@ -558,6 +567,8 @@ export interface ExcalidrawPropsCustomOptions {
|
|||
title: string;
|
||||
children: React.ReactNode;
|
||||
name: string;
|
||||
visible?: boolean;
|
||||
hidden?: boolean;
|
||||
}) => JSX.Element;
|
||||
rangeRender?: (props: {
|
||||
value: number;
|
||||
|
@ -594,8 +605,20 @@ export const ExcalidrawPropsCustomOptionsContext = createContext<
|
|||
|
||||
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 {
|
||||
onChange?: (
|
||||
elements: readonly OrderedExcalidrawElement[],
|
||||
|
|
|
@ -39,6 +39,7 @@ import { getElementAbsoluteCoords } from "@excalidraw/element/bounds";
|
|||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawBlurElement,
|
||||
ExcalidrawDiamondElement,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawEllipseElement,
|
||||
|
@ -107,6 +108,7 @@ type RectangularElement =
|
|||
| ExcalidrawDiamondElement
|
||||
| ExcalidrawFrameLikeElement
|
||||
| ExcalidrawEmbeddableElement
|
||||
| ExcalidrawBlurElement
|
||||
| ExcalidrawImageElement
|
||||
| ExcalidrawIframeElement
|
||||
| ExcalidrawTextElement
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue