diff --git a/excalidraw-app/App.tsx b/excalidraw-app/App.tsx index bb62a0e96..07e833c02 100644 --- a/excalidraw-app/App.tsx +++ b/excalidraw-app/App.tsx @@ -848,6 +848,13 @@ const ExcalidrawWrapper = () => { handleKeyboardGlobally={true} autoFocus={true} theme={editorTheme} + customOptions={{ + disableKeyEvents: true, + // hideMainToolbar: true, + // hideMenu: true, + // hideFooter: true, + hideContextMenu: true, + }} renderTopRightUI={(isMobile) => { if (isMobile || !collabAPI || isCollabDisabled) { return null; diff --git a/package.json b/package.json index 6f57f7e7c..8fa623852 100644 --- a/package.json +++ b/package.json @@ -78,8 +78,8 @@ "autorelease": "node scripts/autorelease.js", "prerelease:excalidraw": "node scripts/prerelease.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:node_modules": "rm -rf node_modules && rm -rf excalidraw-app/node_modules && rm -rf packages/*/node_modules", + "rm:build": "rimraf -rfexcalidraw-app/{build,dist,dev-dist} && rimraf -rfpackages/*/{dist,build} && rimraf -rfexamples/*/{build,dist}", + "rm:node_modules": "rimraf -rfnode_modules && rimraf -rfexcalidraw-app/node_modules && rimraf -rfpackages/*/node_modules", "clean-install": "yarn rm:node_modules && yarn install" }, "resolutions": { diff --git a/packages/common/package.json b/packages/common/package.json index 3e5f6413a..398212ff7 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -50,7 +50,7 @@ "bugs": "https://github.com/excalidraw/excalidraw/issues", "repository": "https://github.com/excalidraw/excalidraw", "scripts": { - "gen:types": "rm -rf types && tsc", - "build:esm": "rm -rf dist && node ../../scripts/buildBase.js && yarn gen:types" + "gen:types": "rimraf -rf types && tsc", + "build:esm": "rimraf -rf dist && node ../../scripts/buildBase.js && yarn gen:types" } } diff --git a/packages/common/src/keys.ts b/packages/common/src/keys.ts index 948e7f568..ba279a6d8 100644 --- a/packages/common/src/keys.ts +++ b/packages/common/src/keys.ts @@ -1,7 +1,10 @@ +import type { KeyboardModifiersObject } from "@excalidraw/excalidraw/types"; + import { isDarwin } from "./constants"; import type { ValueOf } from "./utility-types"; + export const CODES = { EQUAL: "Equal", MINUS: "Minus", @@ -140,12 +143,60 @@ export const isArrowKey = (key: string) => key === KEYS.ARROW_DOWN || key === KEYS.ARROW_UP; -export const shouldResizeFromCenter = (event: MouseEvent | KeyboardEvent) => +const shouldResizeFromCenterDefault = (event: MouseEvent | KeyboardEvent) => event.altKey; -export const shouldMaintainAspectRatio = (event: MouseEvent | KeyboardEvent) => +const shouldMaintainAspectRatioDefault = (event: MouseEvent | KeyboardEvent) => event.shiftKey; +const shouldRotateWithDiscreteAngleDefault = ( + event: MouseEvent | KeyboardEvent | React.PointerEvent, +) => 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, + ) => 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 = ( event: MouseEvent | KeyboardEvent | React.PointerEvent, -) => event.shiftKey; +) => shouldRotateWithDiscreteAngleFunction(event); + +export const shouldSnapping = (event: KeyboardModifiersObject) => + shouldSnappingFunction(event); diff --git a/packages/element/package.json b/packages/element/package.json index ae810e374..9de5dffa1 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -50,7 +50,7 @@ "bugs": "https://github.com/excalidraw/excalidraw/issues", "repository": "https://github.com/excalidraw/excalidraw", "scripts": { - "gen:types": "rm -rf types && tsc", - "build:esm": "rm -rf dist && node ../../scripts/buildBase.js && yarn gen:types" + "gen:types": "rimraf -rf types && tsc", + "build:esm": "rimraf -rf dist && node ../../scripts/buildBase.js && yarn gen:types" } } diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index 5a309b677..7248ecc19 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -1,5 +1,5 @@ import { pointFrom } from "@excalidraw/math"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE, @@ -137,7 +137,12 @@ import { CaptureUpdateAction } from "../store"; import { register } from "./register"; import type { CaptureUpdateActionType } from "../store"; -import type { AppClassProperties, AppState, Primitive } from "../types"; +import { + ExcalidrawPropsCustomOptionsContext, + type AppClassProperties, + type AppState, + type Primitive, +} from "../types"; const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1; @@ -322,28 +327,35 @@ export const actionChangeStrokeColor = register({ : CaptureUpdateAction.EVENTUALLY, }; }, - PanelComponent: ({ elements, appState, updateData, appProps }) => ( - <> - - element.strokeColor, - true, - appState.currentItemStrokeColor, - )} - onChange={(color) => updateData({ currentItemStrokeColor: color })} - elements={elements} - appState={appState} - updateData={updateData} - /> - - ), + PanelComponent: ({ elements, appState, updateData, appProps }) => { + const customOptions = useContext(ExcalidrawPropsCustomOptionsContext); + + return ( + <> + + element.strokeColor, + true, + appState.currentItemStrokeColor, + )} + onChange={(color) => updateData({ currentItemStrokeColor: color })} + elements={elements} + appState={appState} + updateData={updateData} + /> + + ); + }, }); export const actionChangeBackgroundColor = register({ @@ -368,28 +380,37 @@ export const actionChangeBackgroundColor = register({ : CaptureUpdateAction.EVENTUALLY, }; }, - PanelComponent: ({ elements, appState, updateData, appProps }) => ( - <> - - element.backgroundColor, - true, - appState.currentItemBackgroundColor, - )} - onChange={(color) => updateData({ currentItemBackgroundColor: color })} - elements={elements} - appState={appState} - updateData={updateData} - /> - - ), + PanelComponent: ({ elements, appState, updateData, appProps }) => { + const customOptions = useContext(ExcalidrawPropsCustomOptionsContext); + + return ( + <> + + element.backgroundColor, + true, + appState.currentItemBackgroundColor, + )} + onChange={(color) => + updateData({ currentItemBackgroundColor: color }) + } + elements={elements} + appState={appState} + updateData={updateData} + /> + + ); + }, }); export const actionChangeFillStyle = register({ diff --git a/packages/excalidraw/actions/actionZindex.tsx b/packages/excalidraw/actions/actionZindex.tsx index 8eb5a50f2..2bd2f9cfc 100644 --- a/packages/excalidraw/actions/actionZindex.tsx +++ b/packages/excalidraw/actions/actionZindex.tsx @@ -7,6 +7,8 @@ import { moveAllRight, } from "@excalidraw/element/zindex"; +import { useContext } from "react"; + import { BringForwardIcon, BringToFrontIcon, @@ -16,6 +18,8 @@ import { import { t } from "../i18n"; import { CaptureUpdateAction } from "../store"; +import { ExcalidrawPropsCustomOptionsContext } from "../types"; + import { register } from "./register"; export const actionSendBackward = register({ @@ -36,16 +40,29 @@ export const actionSendBackward = register({ event[KEYS.CTRL_OR_CMD] && !event.shiftKey && 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 ( + + ); + }, }); export const actionBringForward = register({ @@ -66,16 +83,29 @@ export const actionBringForward = register({ event[KEYS.CTRL_OR_CMD] && !event.shiftKey && 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 ( + + ); + }, }); export const actionSendToBack = register({ @@ -99,20 +129,33 @@ export const actionSendToBack = register({ : event[KEYS.CTRL_OR_CMD] && event.shiftKey && 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 ( + + ); + }, }); export const actionBringToFront = register({ @@ -137,18 +180,31 @@ export const actionBringToFront = register({ : event[KEYS.CTRL_OR_CMD] && event.shiftKey && 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 ( + + ); + }, }); diff --git a/packages/excalidraw/actions/manager.tsx b/packages/excalidraw/actions/manager.tsx index 171bb5df7..8ec2586b9 100644 --- a/packages/excalidraw/actions/manager.tsx +++ b/packages/excalidraw/actions/manager.tsx @@ -160,7 +160,6 @@ export class ActionManager { const appState = this.getAppState(); const updateData = (formState?: any) => { trackAction(action, "ui", appState, elements, this.app, formState); - this.updater( action.perform( this.getElementsIncludingDeleted(), diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx index 3a7df37a8..158e76af1 100644 --- a/packages/excalidraw/components/Actions.tsx +++ b/packages/excalidraw/components/Actions.tsx @@ -1,5 +1,5 @@ import clsx from "clsx"; -import { useState } from "react"; +import { useState, useContext } from "react"; import { CLASSES, @@ -46,6 +46,8 @@ import { hasStrokeWidth, } from "../scene"; +import { ExcalidrawPropsCustomOptionsContext } from "../types"; + import { SHAPES } from "./shapes"; import "./Actions.scss"; @@ -112,6 +114,8 @@ export const SelectedShapeActions = ({ renderAction: ActionManager["renderAction"]; app: AppClassProperties; }) => { + const customOptions = useContext(ExcalidrawPropsCustomOptionsContext); + const targetElements = getTargetElements(elementsMap, appState); let isSingleElementBoundContainer = false; @@ -212,12 +216,22 @@ export const SelectedShapeActions = ({
{t("labels.layers")} -
- {renderAction("sendToBack")} - {renderAction("sendBackward")} - {renderAction("bringForward")} - {renderAction("bringToFront")} -
+ {!customOptions?.pickerRenders?.ButtonList && ( +
+ {renderAction("sendToBack")} + {renderAction("sendBackward")} + {renderAction("bringForward")} + {renderAction("bringToFront")} +
+ )} + {customOptions?.pickerRenders?.ButtonList && ( + + {renderAction("sendToBack")} + {renderAction("sendBackward")} + {renderAction("bringForward")} + {renderAction("bringToFront")} + + )}
{showAlignActions && !isSingleElementBoundContainer && ( diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 242fd8e46..976bb7c66 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -100,6 +100,10 @@ import { arrayToMap, type EXPORT_IMAGE_TYPES, randomInteger, + setShouldResizeFromCenter, + setShouldMaintainAspectRatio, + setShouldRotateWithDiscreteAngle, + setShouldSnapping, } from "@excalidraw/common"; import { @@ -810,6 +814,26 @@ class App extends React.Component { this.actionManager.registerAction( 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) { @@ -1648,209 +1672,215 @@ class App extends React.Component { generateLinkForSelection={ this.props.generateLinkForSelection } + customOptions={this.props.customOptions} > {this.props.children} -
-
-
- - {selectedElements.length === 1 && - this.state.openDialog?.name !== - "elementLinkSelector" && - this.state.showHyperlinkPopup && ( - +
+
+
+ + {selectedElements.length === 1 && + this.state.openDialog?.name !== + "elementLinkSelector" && + this.state.showHyperlinkPopup && ( + + )} + {this.props.aiEnabled !== false && + selectedElements.length === 1 && + isMagicFrameElement(firstSelectedElement) && ( + + + this.onMagicFrameGenerate( + firstSelectedElement, + "button", + ) + } + /> + + )} + {selectedElements.length === 1 && + isIframeElement(firstSelectedElement) && + firstSelectedElement.customData?.generationData + ?.status === "done" && ( + + + this.onIframeSrcCopy(firstSelectedElement) + } + /> + { + const iframe = + this.getHTMLIFrameElement( + firstSelectedElement, + ); + if (iframe) { + try { + iframe.requestFullscreen(); + this.setState({ + activeEmbeddable: { + element: firstSelectedElement, + state: "active", + }, + selectedElementIds: { + [firstSelectedElement.id]: true, + }, + newElement: null, + selectionElement: null, + }); + } catch (err: any) { + console.warn(err); + this.setState({ + errorMessage: + "Couldn't enter fullscreen", + }); + } + } + }} + /> + + )} + {this.state.toast !== null && ( + this.setToast(null)} + duration={this.state.toast.duration} + closable={this.state.toast.closable} /> )} - {this.props.aiEnabled !== false && - selectedElements.length === 1 && - isMagicFrameElement(firstSelectedElement) && ( - - - this.onMagicFrameGenerate( - firstSelectedElement, - "button", - ) - } - /> - - )} - {selectedElements.length === 1 && - isIframeElement(firstSelectedElement) && - firstSelectedElement.customData?.generationData - ?.status === "done" && ( - - - this.onIframeSrcCopy(firstSelectedElement) - } - /> - { - const iframe = - this.getHTMLIFrameElement( - firstSelectedElement, - ); - if (iframe) { - try { - iframe.requestFullscreen(); - this.setState({ - activeEmbeddable: { - element: firstSelectedElement, - state: "active", - }, - selectedElementIds: { - [firstSelectedElement.id]: true, - }, - newElement: null, - selectionElement: null, - }); - } catch (err: any) { - console.warn(err); - this.setState({ - errorMessage: - "Couldn't enter fullscreen", - }); - } - } + {this.state.contextMenu && + !this.props.customOptions?.hideContextMenu && ( + { + this.setState({ contextMenu: null }, () => { + this.focusContainer(); + callback?.(); + }); }} /> - - )} - {this.state.toast !== null && ( - this.setToast(null)} - duration={this.state.toast.duration} - closable={this.state.toast.closable} - /> - )} - {this.state.contextMenu && ( - { - this.setState({ contextMenu: null }, () => { - this.focusContainer(); - callback?.(); - }); - }} - /> - )} - - {this.state.newElement && ( - - )} - - {this.state.userToFollow && ( - + )} + - )} - {this.renderFrameNames()} + {this.state.userToFollow && ( + + )} + {this.renderFrameNames()} +
{this.renderEmbeddables()} @@ -2242,7 +2272,7 @@ class App extends React.Component { this.setState((prevAppState) => { const actionAppState = actionResult.appState || {}; - return { + const res = { ...prevAppState, ...actionAppState, // NOTE this will prevent opening context menu using an action @@ -2256,6 +2286,21 @@ class App extends React.Component { name, errorMessage, }; + + // Print differences between prevAppState and res + const differences = Object.keys(res).filter((key) => { + return (prevAppState as any)[key] !== (res as any)[key]; + }); + console.log( + "State differences:", + differences.map((key) => ({ + key, + prev: (prevAppState as any)[key], + new: (res as any)[key], + })), + ); + + return res; }); didUpdate = true; @@ -2964,6 +3009,10 @@ class App extends React.Component { // Copy/paste private onCut = withBatchedUpdates((event: ClipboardEvent) => { + if (this.props.customOptions?.disableKeyEvents) { + return; + } + const isExcalidrawActive = this.excalidrawContainerRef.current?.contains( document.activeElement, ); @@ -4089,6 +4138,10 @@ class App extends React.Component { // Input handling private onKeyDown = withBatchedUpdates( (event: React.KeyboardEvent | KeyboardEvent) => { + if (this.props.customOptions?.disableKeyEvents) { + return; + } + // normalize `event.key` when CapsLock is pressed #2372 if ( @@ -5958,7 +6011,12 @@ class App extends React.Component { let dxFromLastCommitted = gridX - rx - lastCommittedX; let dyFromLastCommitted = gridY - ry - lastCommittedY; - if (shouldRotateWithDiscreteAngle(event)) { + if ( + ( + this.props.customOptions?.shouldRotateWithDiscreteAngle ?? + shouldRotateWithDiscreteAngle + )(event) + ) { ({ width: dxFromLastCommitted, height: dyFromLastCommitted } = getLockedLinearCursorAlignSize( // actual coordinate of the last committed point @@ -8560,7 +8618,13 @@ class App extends React.Component { let dx = gridX - newElement.x; 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( newElement.x, newElement.y, @@ -10451,7 +10515,7 @@ class App extends React.Component { width: distance(pointerDownState.origin.x, pointerCoords.x), height: distance(pointerDownState.origin.y, pointerCoords.y), shouldMaintainAspectRatio: shouldMaintainAspectRatio(event), - shouldResizeFromCenter: false, + shouldResizeFromCenter: shouldResizeFromCenter(event), zoom: this.state.zoom.value, informMutation, }); @@ -10744,11 +10808,16 @@ class App extends React.Component { selectedElements, this.scene.getElementsMapIncludingDeleted(), this.scene, - shouldRotateWithDiscreteAngle(event), + ( + this.props.customOptions?.shouldRotateWithDiscreteAngle ?? + shouldRotateWithDiscreteAngle + )(event), shouldResizeFromCenter(event), - selectedElements.some((element) => isImageElement(element)) - ? !shouldMaintainAspectRatio(event) - : shouldMaintainAspectRatio(event), + selectedElements.some((element) => + isImageElement(element) + ? !shouldMaintainAspectRatio(event) + : shouldMaintainAspectRatio(event), + ), resizeX, resizeY, pointerDownState.resize.center.x, diff --git a/packages/excalidraw/components/ButtonIconSelect.tsx b/packages/excalidraw/components/ButtonIconSelect.tsx index 45665e4ca..35391af07 100644 --- a/packages/excalidraw/components/ButtonIconSelect.tsx +++ b/packages/excalidraw/components/ButtonIconSelect.tsx @@ -1,37 +1,53 @@ import clsx from "clsx"; +import { useContext, type JSX } from "react"; + +import { ExcalidrawPropsCustomOptionsContext } from "../types"; + import { ButtonIcon } from "./ButtonIcon"; -import type { JSX } from "react"; +export type ButtonIconSelectProps = { + options: { + value: T; + text: string; + icon: JSX.Element; + testId?: string; + /** if not supplied, defaults to value identity check */ + active?: boolean; + }[]; + value: T | null; + type?: "radio" | "button"; +} & ( + | { type?: "radio"; group: string; onChange: (value: T) => void } + | { + type: "button"; + onClick: ( + value: T, + event: React.MouseEvent, + ) => void; + } +); // TODO: It might be "clever" to add option.icon to the existing component export const ButtonIconSelect = ( - props: { - options: { - value: T; - text: string; - icon: JSX.Element; - testId?: string; - /** if not supplied, defaults to value identity check */ - active?: boolean; - }[]; - value: T | null; - type?: "radio" | "button"; - } & ( - | { type?: "radio"; group: string; onChange: (value: T) => void } - | { - type: "button"; - onClick: ( - value: T, - event: React.MouseEvent, - ) => void; - } - ), -) => ( -
- {props.options.map((option) => - props.type === "button" ? ( - , +) => { + const customOptions = useContext(ExcalidrawPropsCustomOptionsContext); + + if (customOptions?.pickerRenders?.buttonIconSelectRender) { + return customOptions.pickerRenders.buttonIconSelectRender(props); + } + + const renderButtonIcon = ( + option: ButtonIconSelectProps["options"][number], + ) => { + if (props.type !== "button") { + return null; + } + + if (customOptions?.pickerRenders?.CustomButtonIcon) { + return ( + ( active={option.active ?? props.value === option.value} onClick={(event) => props.onClick(option.value, event)} /> - ) : ( - - ), - )} -
-); + ); + } + return ( + props.onClick(option.value, event)} + /> + ); + }; + + const renderRadioButtonIcon = ( + option: ButtonIconSelectProps["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 ( + + ); + }; + + return ( +
+ {props.options.map((option) => + props.type === "button" + ? renderButtonIcon(option) + : renderRadioButtonIcon(option), + )} +
+ ); +}; diff --git a/packages/excalidraw/components/ColorPicker/ColorPicker.tsx b/packages/excalidraw/components/ColorPicker/ColorPicker.tsx index eb6d82d9e..c10008f92 100644 --- a/packages/excalidraw/components/ColorPicker/ColorPicker.tsx +++ b/packages/excalidraw/components/ColorPicker/ColorPicker.tsx @@ -1,6 +1,6 @@ import * as Popover from "@radix-ui/react-popover"; import clsx from "clsx"; -import { useRef } from "react"; +import { useContext, useRef } from "react"; import { COLOR_OUTLINE_CONTRAST_THRESHOLD, @@ -19,6 +19,11 @@ import { ButtonSeparator } from "../ButtonSeparator"; import { activeEyeDropperAtom } from "../EyeDropper"; import { PropertiesPopover } from "../PropertiesPopover"; +import { + ExcalidrawPropsCustomOptionsContext, + type AppState, +} from "../../types"; + import { ColorInput } from "./ColorInput"; import { Picker } from "./Picker"; import PickerHeading from "./PickerHeading"; @@ -29,8 +34,6 @@ import "./ColorPicker.scss"; import type { ColorPickerType } from "./colorPickerUtils"; -import type { AppState } from "../../types"; - const isValidColor = (color: string) => { const style = new Option().style; style.color = color; @@ -220,6 +223,46 @@ export const ColorPicker = ({ updateData, appState, }: ColorPickerProps) => { + const customOptions = useContext(ExcalidrawPropsCustomOptionsContext); + + const renderPopover = () => { + if (customOptions?.pickerRenders?.colorPickerPopoverRender) { + return customOptions.pickerRenders.colorPickerPopoverRender({ + color, + label, + type, + onChange, + elements, + palette, + updateData, + }); + } + + return ( + { + updateData({ openPopup: open ? type : null }); + }} + > + {/* serves as an active color indicator as well */} + + {/* popup content */} + {appState.openPopup === type && ( + + )} + + ); + }; + return (
@@ -230,27 +273,7 @@ export const ColorPicker = ({ topPicks={topPicks} /> - { - updateData({ openPopup: open ? type : null }); - }} - > - {/* serves as an active color indicator as well */} - - {/* popup content */} - {appState.openPopup === type && ( - - )} - + {renderPopover()}
); diff --git a/packages/excalidraw/components/ColorPicker/TopPicks.tsx b/packages/excalidraw/components/ColorPicker/TopPicks.tsx index 8531172fb..c88bfe31e 100644 --- a/packages/excalidraw/components/ColorPicker/TopPicks.tsx +++ b/packages/excalidraw/components/ColorPicker/TopPicks.tsx @@ -7,10 +7,13 @@ import { DEFAULT_ELEMENT_STROKE_PICKS, } from "@excalidraw/common"; +import { useContext } from "react"; + +import { ExcalidrawPropsCustomOptionsContext } from "@excalidraw/excalidraw/types"; + import { isColorDark } from "./colorPickerUtils"; import type { ColorPickerType } from "./colorPickerUtils"; - interface TopPicksProps { onChange: (color: string) => void; type: ColorPickerType; @@ -24,13 +27,19 @@ export const TopPicks = ({ activeColor, topPicks, }: TopPicksProps) => { + const customOptions = useContext(ExcalidrawPropsCustomOptionsContext); + let colors; if (type === "elementStroke") { - colors = DEFAULT_ELEMENT_STROKE_PICKS; + colors = + customOptions?.pickerRenders?.elementStrokeColors ?? + DEFAULT_ELEMENT_STROKE_PICKS; } if (type === "elementBackground") { - colors = DEFAULT_ELEMENT_BACKGROUND_PICKS; + colors = + customOptions?.pickerRenders?.elementBackgroundColors ?? + DEFAULT_ELEMENT_BACKGROUND_PICKS; } if (type === "canvasBackground") { @@ -49,26 +58,41 @@ export const TopPicks = ({ return (
- {colors.map((color: string) => ( - - ))} + color, + isTransparent: color === "transparent" || !color, + hasOutline: !isColorDark(color, COLOR_OUTLINE_CONTRAST_THRESHOLD), + onClick: () => onChange(color), + dataTestid: `color-top-pick-${color}`, + children:
, + key: color, + }); + } + + return ( + + ); + })}
); }; diff --git a/packages/excalidraw/components/LayerUI.tsx b/packages/excalidraw/components/LayerUI.tsx index b5491dedd..64be1355b 100644 --- a/packages/excalidraw/components/LayerUI.tsx +++ b/packages/excalidraw/components/LayerUI.tsx @@ -72,6 +72,7 @@ import type { BinaryFiles, UIAppState, AppClassProperties, + ExcalidrawPropsCustomOptions, } from "../types"; interface LayerUIProps { @@ -95,6 +96,7 @@ interface LayerUIProps { app: AppClassProperties; isCollaborating: boolean; generateLinkForSelection?: AppProps["generateLinkForSelection"]; + customOptions?: ExcalidrawPropsCustomOptions; } const DefaultMainMenu: React.FC<{ @@ -153,6 +155,7 @@ const LayerUI = ({ app, isCollaborating, generateLinkForSelection, + customOptions, }: LayerUIProps) => { const device = useDevice(); const tunnels = useInitializeTunnels(); @@ -209,13 +212,8 @@ const LayerUI = ({
); - const renderSelectedShapeActions = () => ( -
+ const renderSelectedShapeActions = () => { + const children = ( -
- ); + ); + + return ( +
+ {customOptions?.layoutRenders?.menuRender?.({ children }) ?? children} +
+ ); + }; const renderFixedSideContainer = () => { const shouldRenderSelectedShapeActions = showSelectedShapeActions( @@ -249,7 +258,10 @@ const LayerUI = ({ return ( -
+
{renderCanvasActions()} {shouldRenderSelectedShapeActions && renderSelectedShapeActions()} @@ -274,6 +286,11 @@ const LayerUI = ({ className={clsx("App-toolbar", { "zen-mode": appState.zenModeEnabled, })} + style={{ + display: customOptions?.hideMainToolbar + ? "none" + : undefined, + }} > {renderWelcomeScreen && } {renderFixedSideContainer()} -