mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-10-31 10:54:33 +01:00 
			
		
		
		
	Compare commits
	
		
			8 Commits
		
	
	
		
			zsviczian-
			...
			zsviczian-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 09ae07ed7f | ||
|   | 6d45430344 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 3aa0c5ebc0 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e940993e0e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 8f90aeb8d5 | ||
|   | e92d133973 | ||
|   | b682d88167 | ||
|   | 7daf1a7944 | 
| @@ -1,5 +1,5 @@ | |||||||
| import { ColorPicker } from "../components/ColorPicker"; | import { ColorPicker } from "../components/ColorPicker"; | ||||||
| import { zoomIn, zoomOut } from "../components/icons"; | import { eraser, zoomIn, zoomOut } from "../components/icons"; | ||||||
| import { ToolButton } from "../components/ToolButton"; | import { ToolButton } from "../components/ToolButton"; | ||||||
| import { DarkModeToggle } from "../components/DarkModeToggle"; | import { DarkModeToggle } from "../components/DarkModeToggle"; | ||||||
| import { THEME, ZOOM_STEP } from "../constants"; | import { THEME, ZOOM_STEP } from "../constants"; | ||||||
| @@ -15,8 +15,9 @@ import { getShortcutKey } from "../utils"; | |||||||
| import { register } from "./register"; | import { register } from "./register"; | ||||||
| import { Tooltip } from "../components/Tooltip"; | import { Tooltip } from "../components/Tooltip"; | ||||||
| import { newElementWith } from "../element/mutateElement"; | import { newElementWith } from "../element/mutateElement"; | ||||||
| import { getDefaultAppState } from "../appState"; | import { getDefaultAppState, isEraserActive } from "../appState"; | ||||||
| import ClearCanvas from "../components/ClearCanvas"; | import ClearCanvas from "../components/ClearCanvas"; | ||||||
|  | import clsx from "clsx"; | ||||||
|  |  | ||||||
| export const actionChangeViewBackgroundColor = register({ | export const actionChangeViewBackgroundColor = register({ | ||||||
|   name: "changeViewBackgroundColor", |   name: "changeViewBackgroundColor", | ||||||
| @@ -289,3 +290,31 @@ export const actionToggleTheme = register({ | |||||||
|   ), |   ), | ||||||
|   keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D, |   keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D, | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | export const actionErase = register({ | ||||||
|  |   name: "eraser", | ||||||
|  |   perform: (elements, appState) => { | ||||||
|  |     return { | ||||||
|  |       appState: { | ||||||
|  |         ...appState, | ||||||
|  |         selectedElementIds: {}, | ||||||
|  |         selectedGroupIds: {}, | ||||||
|  |         elementType: isEraserActive(appState) ? "selection" : "eraser", | ||||||
|  |       }, | ||||||
|  |       commitToHistory: true, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   PanelComponent: ({ elements, appState, updateData, data }) => ( | ||||||
|  |     <ToolButton | ||||||
|  |       type="button" | ||||||
|  |       icon={eraser} | ||||||
|  |       className={clsx("eraser", { active: isEraserActive(appState) })} | ||||||
|  |       title={t("toolBar.eraser")} | ||||||
|  |       aria-label={t("toolBar.eraser")} | ||||||
|  |       onClick={() => { | ||||||
|  |         updateData(null); | ||||||
|  |       }} | ||||||
|  |       size={data?.size || "medium"} | ||||||
|  |     ></ToolButton> | ||||||
|  |   ), | ||||||
|  | }); | ||||||
|   | |||||||
| @@ -51,6 +51,7 @@ import { | |||||||
|   getContainerElement, |   getContainerElement, | ||||||
| } from "../element/textElement"; | } from "../element/textElement"; | ||||||
| import { | import { | ||||||
|  |   hasBoundTextElement, | ||||||
|   isBoundToContainer, |   isBoundToContainer, | ||||||
|   isLinearElement, |   isLinearElement, | ||||||
|   isLinearElementType, |   isLinearElementType, | ||||||
| @@ -106,6 +107,7 @@ const getFormValue = function <T>( | |||||||
|   appState: AppState, |   appState: AppState, | ||||||
|   getAttribute: (element: ExcalidrawElement) => T, |   getAttribute: (element: ExcalidrawElement) => T, | ||||||
|   defaultValue?: T, |   defaultValue?: T, | ||||||
|  |   onlyBoundTextElements: boolean = false, | ||||||
| ): T | null { | ): T | null { | ||||||
|   const editingElement = appState.editingElement; |   const editingElement = appState.editingElement; | ||||||
|   const nonDeletedElements = getNonDeletedElements(elements); |   const nonDeletedElements = getNonDeletedElements(elements); | ||||||
| @@ -116,6 +118,7 @@ const getFormValue = function <T>( | |||||||
|           nonDeletedElements, |           nonDeletedElements, | ||||||
|           appState, |           appState, | ||||||
|           getAttribute, |           getAttribute, | ||||||
|  |           onlyBoundTextElements, | ||||||
|         ) |         ) | ||||||
|       : defaultValue) ?? |       : defaultValue) ?? | ||||||
|     null |     null | ||||||
| @@ -196,8 +199,8 @@ const changeFontSize = ( | |||||||
|  |  | ||||||
| // ----------------------------------------------------------------------------- | // ----------------------------------------------------------------------------- | ||||||
|  |  | ||||||
| export const actionChangeStrokeColor = register({ | export const actionChangeFontColor = register({ | ||||||
|   name: "changeStrokeColor", |   name: "changeFontColor", | ||||||
|   perform: (elements, appState, value) => { |   perform: (elements, appState, value) => { | ||||||
|     return { |     return { | ||||||
|       ...(value.currentItemStrokeColor && { |       ...(value.currentItemStrokeColor && { | ||||||
| @@ -205,7 +208,7 @@ export const actionChangeStrokeColor = register({ | |||||||
|           elements, |           elements, | ||||||
|           appState, |           appState, | ||||||
|           (el) => { |           (el) => { | ||||||
|             return hasStrokeColor(el.type) |             return isTextElement(el) | ||||||
|               ? newElementWith(el, { |               ? newElementWith(el, { | ||||||
|                   strokeColor: value.currentItemStrokeColor, |                   strokeColor: value.currentItemStrokeColor, | ||||||
|                 }) |                 }) | ||||||
| @@ -221,28 +224,107 @@ export const actionChangeStrokeColor = register({ | |||||||
|       commitToHistory: !!value.currentItemStrokeColor, |       commitToHistory: !!value.currentItemStrokeColor, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   PanelComponent: ({ elements, appState, updateData }) => ( |   PanelComponent: ({ elements, appState, updateData }) => { | ||||||
|     <> |     return ( | ||||||
|       <h3 aria-hidden="true">{t("labels.stroke")}</h3> |       <> | ||||||
|       <ColorPicker |         <h3 aria-hidden="true">{t("labels.fontColor")}</h3> | ||||||
|         type="elementStroke" |         <ColorPicker | ||||||
|         label={t("labels.stroke")} |           type="elementFontColor" | ||||||
|         color={getFormValue( |           label={t("labels.fontColor")} | ||||||
|  |           color={getFormValue( | ||||||
|  |             elements, | ||||||
|  |             appState, | ||||||
|  |             (element) => element.strokeColor, | ||||||
|  |             appState.currentItemStrokeColor, | ||||||
|  |             true, | ||||||
|  |           )} | ||||||
|  |           onChange={(color) => updateData({ currentItemStrokeColor: color })} | ||||||
|  |           isActive={appState.openPopup === "fontColorPicker"} | ||||||
|  |           setActive={(active) => | ||||||
|  |             updateData({ openPopup: active ? "fontColorPicker" : null }) | ||||||
|  |           } | ||||||
|  |           elements={elements} | ||||||
|  |           appState={appState} | ||||||
|  |         /> | ||||||
|  |       </> | ||||||
|  |     ); | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | export const actionChangeStrokeColor = register({ | ||||||
|  |   name: "changeStrokeColor", | ||||||
|  |   perform: (elements, appState, value) => { | ||||||
|  |     const targetElements = getTargetElements( | ||||||
|  |       getNonDeletedElements(elements), | ||||||
|  |       appState, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const hasOnlyContainersWithBoundText = | ||||||
|  |       targetElements.length > 1 && | ||||||
|  |       targetElements.every( | ||||||
|  |         (element) => | ||||||
|  |           hasBoundTextElement(element) || isBoundToContainer(element), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       ...(value.currentItemStrokeColor && { | ||||||
|  |         elements: changeProperty( | ||||||
|           elements, |           elements, | ||||||
|           appState, |           appState, | ||||||
|           (element) => element.strokeColor, |           (el) => { | ||||||
|           appState.currentItemStrokeColor, |             return (hasStrokeColor(el.type) && | ||||||
|         )} |               !hasOnlyContainersWithBoundText) || | ||||||
|         onChange={(color) => updateData({ currentItemStrokeColor: color })} |               !isBoundToContainer(el) | ||||||
|         isActive={appState.openPopup === "strokeColorPicker"} |               ? newElementWith(el, { | ||||||
|         setActive={(active) => |                   strokeColor: value.currentItemStrokeColor, | ||||||
|           updateData({ openPopup: active ? "strokeColorPicker" : null }) |                 }) | ||||||
|         } |               : el; | ||||||
|         elements={elements} |           }, | ||||||
|         appState={appState} |           true, | ||||||
|       /> |         ), | ||||||
|     </> |       }), | ||||||
|   ), |       appState: { | ||||||
|  |         ...appState, | ||||||
|  |         ...value, | ||||||
|  |       }, | ||||||
|  |       commitToHistory: !!value.currentItemStrokeColor, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   PanelComponent: ({ elements, appState, updateData }) => { | ||||||
|  |     const targetElements = getTargetElements( | ||||||
|  |       getNonDeletedElements(elements), | ||||||
|  |       appState, | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const hasOnlyContainersWithBoundText = targetElements.every( | ||||||
|  |       (element) => hasBoundTextElement(element) || isBoundToContainer(element), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |       <> | ||||||
|  |         <h3 aria-hidden="true">{t("labels.stroke")}</h3> | ||||||
|  |         <ColorPicker | ||||||
|  |           type="elementStroke" | ||||||
|  |           label={t("labels.stroke")} | ||||||
|  |           color={getFormValue( | ||||||
|  |             hasOnlyContainersWithBoundText | ||||||
|  |               ? elements.filter((element) => !isTextElement(element)) | ||||||
|  |               : elements, | ||||||
|  |             appState, | ||||||
|  |             (element) => element.strokeColor, | ||||||
|  |             appState.currentItemStrokeColor, | ||||||
|  |           )} | ||||||
|  |           onChange={(color) => updateData({ currentItemStrokeColor: color })} | ||||||
|  |           isActive={appState.openPopup === "strokeColorPicker"} | ||||||
|  |           setActive={(active) => | ||||||
|  |             updateData({ openPopup: active ? "strokeColorPicker" : null }) | ||||||
|  |           } | ||||||
|  |           elements={elements} | ||||||
|  |           appState={appState} | ||||||
|  |         /> | ||||||
|  |       </> | ||||||
|  |     ); | ||||||
|  |   }, | ||||||
| }); | }); | ||||||
|  |  | ||||||
| export const actionChangeBackgroundColor = register({ | export const actionChangeBackgroundColor = register({ | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ export { | |||||||
| export { actionSelectAll } from "./actionSelectAll"; | export { actionSelectAll } from "./actionSelectAll"; | ||||||
| export { actionDuplicateSelection } from "./actionDuplicateSelection"; | export { actionDuplicateSelection } from "./actionDuplicateSelection"; | ||||||
| export { | export { | ||||||
|  |   actionChangeFontColor, | ||||||
|   actionChangeStrokeColor, |   actionChangeStrokeColor, | ||||||
|   actionChangeBackgroundColor, |   actionChangeBackgroundColor, | ||||||
|   actionChangeStrokeWidth, |   actionChangeStrokeWidth, | ||||||
|   | |||||||
| @@ -49,6 +49,7 @@ export type ActionName = | |||||||
|   | "gridMode" |   | "gridMode" | ||||||
|   | "zenMode" |   | "zenMode" | ||||||
|   | "stats" |   | "stats" | ||||||
|  |   | "changeFontColor" | ||||||
|   | "changeStrokeColor" |   | "changeStrokeColor" | ||||||
|   | "changeBackgroundColor" |   | "changeBackgroundColor" | ||||||
|   | "changeFillStyle" |   | "changeFillStyle" | ||||||
| @@ -106,7 +107,8 @@ export type ActionName = | |||||||
|   | "increaseFontSize" |   | "increaseFontSize" | ||||||
|   | "decreaseFontSize" |   | "decreaseFontSize" | ||||||
|   | "unbindText" |   | "unbindText" | ||||||
|   | "hyperlink"; |   | "hyperlink" | ||||||
|  |   | "eraser"; | ||||||
|  |  | ||||||
| export type PanelComponentProps = { | export type PanelComponentProps = { | ||||||
|   elements: readonly ExcalidrawElement[]; |   elements: readonly ExcalidrawElement[]; | ||||||
|   | |||||||
| @@ -213,3 +213,9 @@ export const cleanAppStateForExport = (appState: Partial<AppState>) => { | |||||||
| export const clearAppStateForDatabase = (appState: Partial<AppState>) => { | export const clearAppStateForDatabase = (appState: Partial<AppState>) => { | ||||||
|   return _clearAppStateForStorage(appState, "server"); |   return _clearAppStateForStorage(appState, "server"); | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const isEraserActive = ({ | ||||||
|  |   elementType, | ||||||
|  | }: { | ||||||
|  |   elementType: AppState["elementType"]; | ||||||
|  | }) => elementType === "eraser"; | ||||||
|   | |||||||
| @@ -30,7 +30,7 @@ export const SelectedShapeActions = ({ | |||||||
|   appState: AppState; |   appState: AppState; | ||||||
|   elements: readonly ExcalidrawElement[]; |   elements: readonly ExcalidrawElement[]; | ||||||
|   renderAction: ActionManager["renderAction"]; |   renderAction: ActionManager["renderAction"]; | ||||||
|   elementType: ExcalidrawElement["type"]; |   elementType: AppState["elementType"]; | ||||||
| }) => { | }) => { | ||||||
|   const targetElements = getTargetElements( |   const targetElements = getTargetElements( | ||||||
|     getNonDeletedElements(elements), |     getNonDeletedElements(elements), | ||||||
| @@ -68,8 +68,15 @@ export const SelectedShapeActions = ({ | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   const hasOnlyContainersWithBoundText = | ||||||
|  |     targetElements.length > 1 && | ||||||
|  |     targetElements.every( | ||||||
|  |       (element) => hasBoundTextElement(element) || isBoundToContainer(element), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <div className="panelColumn"> |     <div className="panelColumn"> | ||||||
|  |       {hasOnlyContainersWithBoundText && renderAction("changeFontColor")} | ||||||
|       {((hasStrokeColor(elementType) && |       {((hasStrokeColor(elementType) && | ||||||
|         elementType !== "image" && |         elementType !== "image" && | ||||||
|         commonSelectedType !== "image") || |         commonSelectedType !== "image") || | ||||||
| @@ -187,7 +194,7 @@ export const ShapesSwitcher = ({ | |||||||
|   onImageAction, |   onImageAction, | ||||||
| }: { | }: { | ||||||
|   canvas: HTMLCanvasElement | null; |   canvas: HTMLCanvasElement | null; | ||||||
|   elementType: ExcalidrawElement["type"]; |   elementType: AppState["elementType"]; | ||||||
|   setAppState: React.Component<any, AppState>["setState"]; |   setAppState: React.Component<any, AppState>["setState"]; | ||||||
|   onImageAction: (data: { pointerType: PointerType | null }) => void; |   onImageAction: (data: { pointerType: PointerType | null }) => void; | ||||||
| }) => ( | }) => ( | ||||||
|   | |||||||
| @@ -35,7 +35,7 @@ import { ActionManager } from "../actions/manager"; | |||||||
| import { actions } from "../actions/register"; | import { actions } from "../actions/register"; | ||||||
| import { ActionResult } from "../actions/types"; | import { ActionResult } from "../actions/types"; | ||||||
| import { trackEvent } from "../analytics"; | import { trackEvent } from "../analytics"; | ||||||
| import { getDefaultAppState } from "../appState"; | import { getDefaultAppState, isEraserActive } from "../appState"; | ||||||
| import { | import { | ||||||
|   copyToClipboard, |   copyToClipboard, | ||||||
|   parseClipboard, |   parseClipboard, | ||||||
| @@ -314,6 +314,7 @@ class App extends React.Component<AppProps, AppState> { | |||||||
|   lastPointerDown: React.PointerEvent<HTMLCanvasElement> | null = null; |   lastPointerDown: React.PointerEvent<HTMLCanvasElement> | null = null; | ||||||
|   lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null; |   lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null; | ||||||
|   contextMenuOpen: boolean = false; |   contextMenuOpen: boolean = false; | ||||||
|  |   lastScenePointer: { x: number; y: number } | null = null; | ||||||
|  |  | ||||||
|   constructor(props: AppProps) { |   constructor(props: AppProps) { | ||||||
|     super(props); |     super(props); | ||||||
| @@ -1044,6 +1045,12 @@ class App extends React.Component<AppProps, AppState> { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   componentDidUpdate(prevProps: AppProps, prevState: AppState) { |   componentDidUpdate(prevProps: AppProps, prevState: AppState) { | ||||||
|  |     if ( | ||||||
|  |       Object.keys(this.state.selectedElementIds).length && | ||||||
|  |       isEraserActive(this.state) | ||||||
|  |     ) { | ||||||
|  |       this.setState({ elementType: "selection" }); | ||||||
|  |     } | ||||||
|     // Hide hyperlink popup if shown when element type is not selection |     // Hide hyperlink popup if shown when element type is not selection | ||||||
|     if ( |     if ( | ||||||
|       prevState.elementType === "selection" && |       prevState.elementType === "selection" && | ||||||
| @@ -1833,7 +1840,11 @@ class App extends React.Component<AppProps, AppState> { | |||||||
|         event.preventDefault(); |         event.preventDefault(); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (event.key === KEYS.G || event.key === KEYS.S) { |       if ( | ||||||
|  |         event.key === KEYS.G || | ||||||
|  |         event.key === KEYS.S || | ||||||
|  |         event.key === KEYS.C | ||||||
|  |       ) { | ||||||
|         const selectedElements = getSelectedElements( |         const selectedElements = getSelectedElements( | ||||||
|           this.scene.getElements(), |           this.scene.getElements(), | ||||||
|           this.state, |           this.state, | ||||||
| @@ -1855,6 +1866,9 @@ class App extends React.Component<AppProps, AppState> { | |||||||
|         if (event.key === KEYS.S) { |         if (event.key === KEYS.S) { | ||||||
|           this.setState({ openPopup: "strokeColorPicker" }); |           this.setState({ openPopup: "strokeColorPicker" }); | ||||||
|         } |         } | ||||||
|  |         if (event.key === KEYS.C) { | ||||||
|  |           this.setState({ openPopup: "fontColorPicker" }); | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|   ); |   ); | ||||||
| @@ -2450,7 +2464,6 @@ class App extends React.Component<AppProps, AppState> { | |||||||
|     event: React.PointerEvent<HTMLCanvasElement>, |     event: React.PointerEvent<HTMLCanvasElement>, | ||||||
|   ) => { |   ) => { | ||||||
|     this.savePointer(event.clientX, event.clientY, this.state.cursorButton); |     this.savePointer(event.clientX, event.clientY, this.state.cursorButton); | ||||||
|  |  | ||||||
|     if (gesture.pointers.has(event.pointerId)) { |     if (gesture.pointers.has(event.pointerId)) { | ||||||
|       gesture.pointers.set(event.pointerId, { |       gesture.pointers.set(event.pointerId, { | ||||||
|         x: event.clientX, |         x: event.clientX, | ||||||
| @@ -2624,7 +2637,8 @@ class App extends React.Component<AppProps, AppState> { | |||||||
|     if ( |     if ( | ||||||
|       hasDeselectedButton || |       hasDeselectedButton || | ||||||
|       (this.state.elementType !== "selection" && |       (this.state.elementType !== "selection" && | ||||||
|         this.state.elementType !== "text") |         this.state.elementType !== "text" && | ||||||
|  |         this.state.elementType !== "eraser") | ||||||
|     ) { |     ) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| @@ -2699,8 +2713,9 @@ class App extends React.Component<AppProps, AppState> { | |||||||
|         !this.state.showHyperlinkPopup |         !this.state.showHyperlinkPopup | ||||||
|       ) { |       ) { | ||||||
|         this.setState({ showHyperlinkPopup: "info" }); |         this.setState({ showHyperlinkPopup: "info" }); | ||||||
|       } |       } else if (isEraserActive(this.state)) { | ||||||
|       if (this.state.elementType === "text") { |         setCursor(this.canvas, CURSOR_TYPE.AUTO); | ||||||
|  |       } else if (this.state.elementType === "text") { | ||||||
|         setCursor( |         setCursor( | ||||||
|           this.canvas, |           this.canvas, | ||||||
|           isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR, |           isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR, | ||||||
| @@ -2741,6 +2756,80 @@ class App extends React.Component<AppProps, AppState> { | |||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   private handleEraser = ( | ||||||
|  |     event: PointerEvent, | ||||||
|  |     pointerDownState: PointerDownState, | ||||||
|  |     scenePointer: { x: number; y: number }, | ||||||
|  |   ) => { | ||||||
|  |     const updateElementIds = (elements: ExcalidrawElement[]) => { | ||||||
|  |       elements.forEach((element) => { | ||||||
|  |         idsToUpdate.push(element.id); | ||||||
|  |         if (event.altKey) { | ||||||
|  |           if (pointerDownState.elementIdsToErase[element.id]) { | ||||||
|  |             pointerDownState.elementIdsToErase[element.id] = false; | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           pointerDownState.elementIdsToErase[element.id] = true; | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const idsToUpdate: Array<string> = []; | ||||||
|  |  | ||||||
|  |     const distance = distance2d( | ||||||
|  |       pointerDownState.lastCoords.x, | ||||||
|  |       pointerDownState.lastCoords.y, | ||||||
|  |       scenePointer.x, | ||||||
|  |       scenePointer.y, | ||||||
|  |     ); | ||||||
|  |     const threshold = 10 / this.state.zoom.value; | ||||||
|  |     const point = { ...pointerDownState.lastCoords }; | ||||||
|  |     let samplingInterval = 0; | ||||||
|  |     while (samplingInterval <= distance) { | ||||||
|  |       const hitElements = this.getElementsAtPosition(point.x, point.y); | ||||||
|  |       updateElementIds(hitElements); | ||||||
|  |  | ||||||
|  |       // Exit since we reached current point | ||||||
|  |       if (samplingInterval === distance) { | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Calculate next point in the line at a distance of sampling interval | ||||||
|  |       samplingInterval = Math.min(samplingInterval + threshold, distance); | ||||||
|  |  | ||||||
|  |       const distanceRatio = samplingInterval / distance; | ||||||
|  |       const nextX = | ||||||
|  |         (1 - distanceRatio) * point.x + distanceRatio * scenePointer.x; | ||||||
|  |       const nextY = | ||||||
|  |         (1 - distanceRatio) * point.y + distanceRatio * scenePointer.y; | ||||||
|  |       point.x = nextX; | ||||||
|  |       point.y = nextY; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const elements = this.scene.getElements().map((ele) => { | ||||||
|  |       const id = | ||||||
|  |         isBoundToContainer(ele) && idsToUpdate.includes(ele.containerId) | ||||||
|  |           ? ele.containerId | ||||||
|  |           : ele.id; | ||||||
|  |       if (idsToUpdate.includes(id)) { | ||||||
|  |         if (event.altKey) { | ||||||
|  |           if (pointerDownState.elementIdsToErase[id] === false) { | ||||||
|  |             return newElementWith(ele, { | ||||||
|  |               opacity: this.state.currentItemOpacity, | ||||||
|  |             }); | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           return newElementWith(ele, { opacity: 20 }); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       return ele; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     this.scene.replaceAllElements(elements); | ||||||
|  |  | ||||||
|  |     pointerDownState.lastCoords.x = scenePointer.x; | ||||||
|  |     pointerDownState.lastCoords.y = scenePointer.y; | ||||||
|  |   }; | ||||||
|   // set touch moving for mobile context menu |   // set touch moving for mobile context menu | ||||||
|   private handleTouchMove = (event: React.TouchEvent<HTMLCanvasElement>) => { |   private handleTouchMove = (event: React.TouchEvent<HTMLCanvasElement>) => { | ||||||
|     invalidateContextMenu = true; |     invalidateContextMenu = true; | ||||||
| @@ -2773,6 +2862,7 @@ class App extends React.Component<AppProps, AppState> { | |||||||
|     if (isPanning) { |     if (isPanning) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     this.lastPointerDown = event; |     this.lastPointerDown = event; | ||||||
|     this.setState({ |     this.setState({ | ||||||
|       lastPointerDownWith: event.pointerType, |       lastPointerDownWith: event.pointerType, | ||||||
| @@ -2865,7 +2955,7 @@ class App extends React.Component<AppProps, AppState> { | |||||||
|         this.state.elementType, |         this.state.elementType, | ||||||
|         pointerDownState, |         pointerDownState, | ||||||
|       ); |       ); | ||||||
|     } else { |     } else if (this.state.elementType !== "eraser") { | ||||||
|       this.createGenericElementOnPointerDown( |       this.createGenericElementOnPointerDown( | ||||||
|         this.state.elementType, |         this.state.elementType, | ||||||
|         pointerDownState, |         pointerDownState, | ||||||
| @@ -2900,6 +2990,7 @@ class App extends React.Component<AppProps, AppState> { | |||||||
|   ) => { |   ) => { | ||||||
|     this.lastPointerUp = event; |     this.lastPointerUp = event; | ||||||
|     const isTouchScreen = ["pen", "touch"].includes(event.pointerType); |     const isTouchScreen = ["pen", "touch"].includes(event.pointerType); | ||||||
|  |  | ||||||
|     if (isTouchScreen) { |     if (isTouchScreen) { | ||||||
|       const scenePointer = viewportCoordsToSceneCoords( |       const scenePointer = viewportCoordsToSceneCoords( | ||||||
|         { clientX: event.clientX, clientY: event.clientY }, |         { clientX: event.clientX, clientY: event.clientY }, | ||||||
| @@ -2914,6 +3005,8 @@ class App extends React.Component<AppProps, AppState> { | |||||||
|         hitElement, |         hitElement, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |     if (isEraserActive(this.state)) { | ||||||
|  |     } | ||||||
|     if ( |     if ( | ||||||
|       this.hitLinkElement && |       this.hitLinkElement && | ||||||
|       !this.state.selectedElementIds[this.hitLinkElement.id] |       !this.state.selectedElementIds[this.hitLinkElement.id] | ||||||
| @@ -3139,6 +3232,7 @@ class App extends React.Component<AppProps, AppState> { | |||||||
|       boxSelection: { |       boxSelection: { | ||||||
|         hasOccurred: false, |         hasOccurred: false, | ||||||
|       }, |       }, | ||||||
|  |       elementIdsToErase: {}, | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -3727,7 +3821,6 @@ class App extends React.Component<AppProps, AppState> { | |||||||
|           ), |           ), | ||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       const target = event.target; |       const target = event.target; | ||||||
|       if (!(target instanceof HTMLElement)) { |       if (!(target instanceof HTMLElement)) { | ||||||
|         return; |         return; | ||||||
| @@ -3738,6 +3831,12 @@ class App extends React.Component<AppProps, AppState> { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       const pointerCoords = viewportCoordsToSceneCoords(event, this.state); |       const pointerCoords = viewportCoordsToSceneCoords(event, this.state); | ||||||
|  |  | ||||||
|  |       if (isEraserActive(this.state)) { | ||||||
|  |         this.handleEraser(event, pointerDownState, pointerCoords); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       const [gridX, gridY] = getGridPoint( |       const [gridX, gridY] = getGridPoint( | ||||||
|         pointerCoords.x, |         pointerCoords.x, | ||||||
|         pointerCoords.y, |         pointerCoords.y, | ||||||
| @@ -4090,7 +4189,6 @@ class App extends React.Component<AppProps, AppState> { | |||||||
|         isResizing, |         isResizing, | ||||||
|         isRotating, |         isRotating, | ||||||
|       } = this.state; |       } = this.state; | ||||||
|  |  | ||||||
|       this.setState({ |       this.setState({ | ||||||
|         isResizing: false, |         isResizing: false, | ||||||
|         isRotating: false, |         isRotating: false, | ||||||
| @@ -4311,6 +4409,33 @@ class App extends React.Component<AppProps, AppState> { | |||||||
|       // Code below handles selection when element(s) weren't |       // Code below handles selection when element(s) weren't | ||||||
|       // drag or added to selection on pointer down phase. |       // drag or added to selection on pointer down phase. | ||||||
|       const hitElement = pointerDownState.hit.element; |       const hitElement = pointerDownState.hit.element; | ||||||
|  |       if (isEraserActive(this.state)) { | ||||||
|  |         const draggedDistance = distance2d( | ||||||
|  |           this.lastPointerDown!.clientX, | ||||||
|  |           this.lastPointerDown!.clientY, | ||||||
|  |           this.lastPointerUp!.clientX, | ||||||
|  |           this.lastPointerUp!.clientY, | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         if (draggedDistance === 0) { | ||||||
|  |           const scenePointer = viewportCoordsToSceneCoords( | ||||||
|  |             { | ||||||
|  |               clientX: this.lastPointerUp!.clientX, | ||||||
|  |               clientY: this.lastPointerUp!.clientY, | ||||||
|  |             }, | ||||||
|  |             this.state, | ||||||
|  |           ); | ||||||
|  |           const hitElement = this.getElementAtPosition( | ||||||
|  |             scenePointer.x, | ||||||
|  |             scenePointer.y, | ||||||
|  |           ); | ||||||
|  |  | ||||||
|  |           pointerDownState.hit.element = hitElement; | ||||||
|  |         } | ||||||
|  |         this.eraseElements(pointerDownState); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       if ( |       if ( | ||||||
|         hitElement && |         hitElement && | ||||||
|         !pointerDownState.drag.hasOccurred && |         !pointerDownState.drag.hasOccurred && | ||||||
| @@ -4450,6 +4575,27 @@ class App extends React.Component<AppProps, AppState> { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private eraseElements = (pointerDownState: PointerDownState) => { | ||||||
|  |     const hitElement = pointerDownState.hit.element; | ||||||
|  |     const elements = this.scene.getElements().map((ele) => { | ||||||
|  |       if (pointerDownState.elementIdsToErase[ele.id]) { | ||||||
|  |         return newElementWith(ele, { isDeleted: true }); | ||||||
|  |       } else if (hitElement && ele.id === hitElement.id) { | ||||||
|  |         return newElementWith(ele, { isDeleted: true }); | ||||||
|  |       } else if ( | ||||||
|  |         isBoundToContainer(ele) && | ||||||
|  |         (pointerDownState.elementIdsToErase[ele.containerId] || | ||||||
|  |           (hitElement && ele.containerId === hitElement.id)) | ||||||
|  |       ) { | ||||||
|  |         return newElementWith(ele, { isDeleted: true }); | ||||||
|  |       } | ||||||
|  |       return ele; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     this.history.resumeRecording(); | ||||||
|  |     this.scene.replaceAllElements(elements); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   private initializeImage = async ({ |   private initializeImage = async ({ | ||||||
|     imageFile, |     imageFile, | ||||||
|     imageElement: _imageElement, |     imageElement: _imageElement, | ||||||
|   | |||||||
| @@ -255,7 +255,8 @@ | |||||||
|     color: #aaa; |     color: #aaa; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .color-picker-type-elementStroke .color-picker-keybinding { |   .color-picker-type-elementStroke .color-picker-keybinding, | ||||||
|  |   .color-picker-type-elementFontColor .color-picker-keybinding { | ||||||
|     color: #d4d4d4; |     color: #d4d4d4; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -101,19 +101,24 @@ const Picker = ({ | |||||||
|   onClose: () => void; |   onClose: () => void; | ||||||
|   label: string; |   label: string; | ||||||
|   showInput: boolean; |   showInput: boolean; | ||||||
|   type: "canvasBackground" | "elementBackground" | "elementStroke"; |   type: | ||||||
|  |     | "canvasBackground" | ||||||
|  |     | "elementBackground" | ||||||
|  |     | "elementStroke" | ||||||
|  |     | "elementFontColor"; | ||||||
|   elements: readonly ExcalidrawElement[]; |   elements: readonly ExcalidrawElement[]; | ||||||
| }) => { | }) => { | ||||||
|   const firstItem = React.useRef<HTMLButtonElement>(); |   const firstItem = React.useRef<HTMLButtonElement>(); | ||||||
|   const activeItem = React.useRef<HTMLButtonElement>(); |   const activeItem = React.useRef<HTMLButtonElement>(); | ||||||
|   const gallery = React.useRef<HTMLDivElement>(); |   const gallery = React.useRef<HTMLDivElement>(); | ||||||
|   const colorInput = React.useRef<HTMLInputElement>(); |   const colorInput = React.useRef<HTMLInputElement>(); | ||||||
|  |   const colorType = type === "elementFontColor" ? "elementStroke" : type; | ||||||
|  |  | ||||||
|   const [customColors] = React.useState(() => { |   const [customColors] = React.useState(() => { | ||||||
|     if (type === "canvasBackground") { |     if (colorType === "canvasBackground") { | ||||||
|       return []; |       return []; | ||||||
|     } |     } | ||||||
|     return getCustomColors(elements, type); |     return getCustomColors(elements, colorType); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   React.useEffect(() => { |   React.useEffect(() => { | ||||||
| @@ -356,7 +361,11 @@ export const ColorPicker = ({ | |||||||
|   elements, |   elements, | ||||||
|   appState, |   appState, | ||||||
| }: { | }: { | ||||||
|   type: "canvasBackground" | "elementBackground" | "elementStroke"; |   type: | ||||||
|  |     | "canvasBackground" | ||||||
|  |     | "elementBackground" | ||||||
|  |     | "elementStroke" | ||||||
|  |     | "elementFontColor"; | ||||||
|   color: string | null; |   color: string | null; | ||||||
|   onChange: (color: string) => void; |   onChange: (color: string) => void; | ||||||
|   label: string; |   label: string; | ||||||
| @@ -366,7 +375,7 @@ export const ColorPicker = ({ | |||||||
|   appState: AppState; |   appState: AppState; | ||||||
| }) => { | }) => { | ||||||
|   const pickerButton = React.useRef<HTMLButtonElement>(null); |   const pickerButton = React.useRef<HTMLButtonElement>(null); | ||||||
|  |   const colorType = type === "elementFontColor" ? "elementStroke" : type; | ||||||
|   return ( |   return ( | ||||||
|     <div> |     <div> | ||||||
|       <div className="color-picker-control-container"> |       <div className="color-picker-control-container"> | ||||||
| @@ -393,7 +402,7 @@ export const ColorPicker = ({ | |||||||
|             } |             } | ||||||
|           > |           > | ||||||
|             <Picker |             <Picker | ||||||
|               colors={colors[type]} |               colors={colors[colorType]} | ||||||
|               color={color || null} |               color={color || null} | ||||||
|               onChange={(changedColor) => { |               onChange={(changedColor) => { | ||||||
|                 onChange(changedColor); |                 onChange(changedColor); | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ import { | |||||||
|   isTextElement, |   isTextElement, | ||||||
| } from "../element/typeChecks"; | } from "../element/typeChecks"; | ||||||
| import { getShortcutKey } from "../utils"; | import { getShortcutKey } from "../utils"; | ||||||
|  | import { isEraserActive } from "../appState"; | ||||||
|  |  | ||||||
| interface HintViewerProps { | interface HintViewerProps { | ||||||
|   appState: AppState; |   appState: AppState; | ||||||
| @@ -22,6 +23,9 @@ const getHints = ({ appState, elements, isMobile }: HintViewerProps) => { | |||||||
|   const { elementType, isResizing, isRotating, lastPointerDownWith } = appState; |   const { elementType, isResizing, isRotating, lastPointerDownWith } = appState; | ||||||
|   const multiMode = appState.multiElement !== null; |   const multiMode = appState.multiElement !== null; | ||||||
|  |  | ||||||
|  |   if (isEraserActive(appState)) { | ||||||
|  |     return t("hints.eraserRevert"); | ||||||
|  |   } | ||||||
|   if (elementType === "arrow" || elementType === "line") { |   if (elementType === "arrow" || elementType === "line") { | ||||||
|     if (!multiMode) { |     if (!multiMode) { | ||||||
|       return t("hints.linearElement"); |       return t("hints.linearElement"); | ||||||
|   | |||||||
| @@ -428,6 +428,14 @@ const LayerUI = ({ | |||||||
|                   {actionManager.renderAction("redo", { size: "small" })} |                   {actionManager.renderAction("redo", { size: "small" })} | ||||||
|                 </div> |                 </div> | ||||||
|               )} |               )} | ||||||
|  |               <div | ||||||
|  |                 className={clsx("eraser-buttons zen-mode-transition", { | ||||||
|  |                   "layer-ui__wrapper__footer-left--transition-left": | ||||||
|  |                     zenModeEnabled, | ||||||
|  |                 })} | ||||||
|  |               > | ||||||
|  |                 {actionManager.renderAction("eraser", { size: "small" })} | ||||||
|  |               </div> | ||||||
|             </Section> |             </Section> | ||||||
|           </Stack.Col> |           </Stack.Col> | ||||||
|         </div> |         </div> | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ import { NonDeletedExcalidrawElement } from "../element/types"; | |||||||
| import { FixedSideContainer } from "./FixedSideContainer"; | import { FixedSideContainer } from "./FixedSideContainer"; | ||||||
| import { Island } from "./Island"; | import { Island } from "./Island"; | ||||||
| import { HintViewer } from "./HintViewer"; | import { HintViewer } from "./HintViewer"; | ||||||
| import { calculateScrollCenter } from "../scene"; | import { calculateScrollCenter, getSelectedElements } from "../scene"; | ||||||
| import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; | import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; | ||||||
| import { Section } from "./Section"; | import { Section } from "./Section"; | ||||||
| import CollabButton from "./CollabButton"; | import CollabButton from "./CollabButton"; | ||||||
| @@ -113,6 +113,12 @@ export const MobileMenu = ({ | |||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const renderAppToolbar = () => { |   const renderAppToolbar = () => { | ||||||
|  |     // Render eraser conditionally in mobile | ||||||
|  |     const showEraser = | ||||||
|  |       !appState.viewModeEnabled && | ||||||
|  |       !appState.editingElement && | ||||||
|  |       getSelectedElements(elements, appState).length === 0; | ||||||
|  |  | ||||||
|     if (viewModeEnabled) { |     if (viewModeEnabled) { | ||||||
|       return ( |       return ( | ||||||
|         <div className="App-toolbar-content"> |         <div className="App-toolbar-content"> | ||||||
| @@ -120,12 +126,16 @@ export const MobileMenu = ({ | |||||||
|         </div> |         </div> | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|       <div className="App-toolbar-content"> |       <div className="App-toolbar-content"> | ||||||
|         {actionManager.renderAction("toggleCanvasMenu")} |         {actionManager.renderAction("toggleCanvasMenu")} | ||||||
|         {actionManager.renderAction("toggleEditMenu")} |         {actionManager.renderAction("toggleEditMenu")} | ||||||
|  |  | ||||||
|         {actionManager.renderAction("undo")} |         {actionManager.renderAction("undo")} | ||||||
|         {actionManager.renderAction("redo")} |         {actionManager.renderAction("redo")} | ||||||
|  |         {showEraser && actionManager.renderAction("eraser")} | ||||||
|  |  | ||||||
|         {actionManager.renderAction( |         {actionManager.renderAction( | ||||||
|           appState.multiElement ? "finalize" : "duplicateSelection", |           appState.multiElement ? "finalize" : "duplicateSelection", | ||||||
|         )} |         )} | ||||||
|   | |||||||
| @@ -934,3 +934,7 @@ export const editIcon = createIcon( | |||||||
|   ></path>, |   ></path>, | ||||||
|   { width: 640, height: 512 }, |   { width: 640, height: 512 }, | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | export const eraser = createIcon( | ||||||
|  |   <path d="M480 416C497.7 416 512 430.3 512 448C512 465.7 497.7 480 480 480H150.6C133.7 480 117.4 473.3 105.4 461.3L25.37 381.3C.3786 356.3 .3786 315.7 25.37 290.7L258.7 57.37C283.7 32.38 324.3 32.38 349.3 57.37L486.6 194.7C511.6 219.7 511.6 260.3 486.6 285.3L355.9 416H480zM265.4 416L332.7 348.7L195.3 211.3L70.63 336L150.6 416L265.4 416z" />, | ||||||
|  | ); | ||||||
|   | |||||||
| @@ -63,8 +63,6 @@ export const ENV = { | |||||||
|  |  | ||||||
| export const CLASSES = { | export const CLASSES = { | ||||||
|   SHAPE_ACTIONS_MENU: "App-menu__left", |   SHAPE_ACTIONS_MENU: "App-menu__left", | ||||||
|   SHAPE_ACTIONS_MOBILE_MENU: "App-mobile-menu", |  | ||||||
|   MOBILE_TOOLBAR: "App-toolbar-content", |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| // 1-based in case we ever do `if(element.fontFamily)` | // 1-based in case we ever do `if(element.fontFamily)` | ||||||
|   | |||||||
| @@ -290,6 +290,16 @@ | |||||||
|     width: 100%; |     width: 100%; | ||||||
|  |  | ||||||
|     box-sizing: border-box; |     box-sizing: border-box; | ||||||
|  |  | ||||||
|  |     .eraser { | ||||||
|  |       &.ToolIcon:hover { | ||||||
|  |         --icon-fill-color: #fff; | ||||||
|  |         --keybinding-color: #fff; | ||||||
|  |       } | ||||||
|  |       &.active { | ||||||
|  |         background-color: var(--color-primary); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .App-toolbar-content { |   .App-toolbar-content { | ||||||
| @@ -467,7 +477,8 @@ | |||||||
|     font-family: var(--ui-font); |     font-family: var(--ui-font); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .undo-redo-buttons { |   .undo-redo-buttons, | ||||||
|  |   .eraser-buttons { | ||||||
|     display: grid; |     display: grid; | ||||||
|     grid-auto-flow: column; |     grid-auto-flow: column; | ||||||
|     gap: 0.4em; |     gap: 0.4em; | ||||||
|   | |||||||
| @@ -31,8 +31,8 @@ type RestoredAppState = Omit< | |||||||
| >; | >; | ||||||
|  |  | ||||||
| export const AllowedExcalidrawElementTypes: Record< | export const AllowedExcalidrawElementTypes: Record< | ||||||
|   ExcalidrawElement["type"], |   AppState["elementType"], | ||||||
|   true |   boolean | ||||||
| > = { | > = { | ||||||
|   selection: true, |   selection: true, | ||||||
|   text: true, |   text: true, | ||||||
| @@ -43,6 +43,7 @@ export const AllowedExcalidrawElementTypes: Record< | |||||||
|   image: true, |   image: true, | ||||||
|   arrow: true, |   arrow: true, | ||||||
|   freedraw: true, |   freedraw: true, | ||||||
|  |   eraser: false, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export type RestoredDataState = { | export type RestoredDataState = { | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| import { SHAPES } from "../shapes"; |  | ||||||
| import { updateBoundElements } from "./binding"; | import { updateBoundElements } from "./binding"; | ||||||
| import { getCommonBounds } from "./bounds"; | import { getCommonBounds } from "./bounds"; | ||||||
| import { mutateElement } from "./mutateElement"; | import { mutateElement } from "./mutateElement"; | ||||||
| @@ -93,7 +92,7 @@ export const getDragOffsetXY = ( | |||||||
|  |  | ||||||
| export const dragNewElement = ( | export const dragNewElement = ( | ||||||
|   draggingElement: NonDeletedExcalidrawElement, |   draggingElement: NonDeletedExcalidrawElement, | ||||||
|   elementType: typeof SHAPES[number]["value"], |   elementType: AppState["elementType"], | ||||||
|   originX: number, |   originX: number, | ||||||
|   originY: number, |   originY: number, | ||||||
|   x: number, |   x: number, | ||||||
|   | |||||||
| @@ -10,5 +10,6 @@ export const showSelectedShapeActions = ( | |||||||
|     !appState.viewModeEnabled && |     !appState.viewModeEnabled && | ||||||
|       (appState.editingElement || |       (appState.editingElement || | ||||||
|         getSelectedElements(elements, appState).length || |         getSelectedElements(elements, appState).length || | ||||||
|         appState.elementType !== "selection"), |         (appState.elementType !== "selection" && | ||||||
|  |           appState.elementType !== "eraser")), | ||||||
|   ); |   ); | ||||||
|   | |||||||
| @@ -542,29 +542,9 @@ export const textWysiwyg = ({ | |||||||
|       target.closest(".color-picker-input") && |       target.closest(".color-picker-input") && | ||||||
|       isWritableElement(target); |       isWritableElement(target); | ||||||
|  |  | ||||||
|     const isShapeActionsPanel = |  | ||||||
|       (target instanceof HTMLElement || target instanceof SVGElement) && |  | ||||||
|       (target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) || |  | ||||||
|         target.closest(`.${CLASSES.SHAPE_ACTIONS_MOBILE_MENU}`) || |  | ||||||
|         target.closest(`.${CLASSES.MOBILE_TOOLBAR}`)); |  | ||||||
|  |  | ||||||
|     setTimeout(() => { |     setTimeout(() => { | ||||||
|       editable.onblur = () => { |       editable.onblur = handleSubmit; | ||||||
|         app.setState({ |       if (target && isTargetColorPicker) { | ||||||
|           toastMessage: |  | ||||||
|             target instanceof HTMLElement |  | ||||||
|               ? target.tagName ?? "no tagName" |  | ||||||
|               : "not an HTMLElement", |  | ||||||
|         }); |  | ||||||
|         if (isShapeActionsPanel) { |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|         app.setState({ |  | ||||||
|           toastMessage: "debug: onblur", |  | ||||||
|         }); |  | ||||||
|         handleSubmit(); |  | ||||||
|       }; |  | ||||||
|       if (target && (isTargetColorPicker || isShapeActionsPanel)) { |  | ||||||
|         target.onblur = () => { |         target.onblur = () => { | ||||||
|           editable.focus(); |           editable.focus(); | ||||||
|         }; |         }; | ||||||
| @@ -582,22 +562,13 @@ export const textWysiwyg = ({ | |||||||
|       event.target instanceof HTMLInputElement && |       event.target instanceof HTMLInputElement && | ||||||
|       event.target.closest(".color-picker-input") && |       event.target.closest(".color-picker-input") && | ||||||
|       isWritableElement(event.target); |       isWritableElement(event.target); | ||||||
|     const isShapeActionsPanel = |  | ||||||
|       (event.target instanceof HTMLElement || |  | ||||||
|         event.target instanceof SVGElement) && |  | ||||||
|       (event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) || |  | ||||||
|         event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MOBILE_MENU}`) || |  | ||||||
|         event.target.closest(`.${CLASSES.MOBILE_TOOLBAR}`)); |  | ||||||
|     if ( |     if ( | ||||||
|       ((event.target instanceof HTMLElement || |       ((event.target instanceof HTMLElement || | ||||||
|         event.target instanceof SVGElement) && |         event.target instanceof SVGElement) && | ||||||
|         isShapeActionsPanel && |         event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) && | ||||||
|         !isWritableElement(event.target)) || |         !isWritableElement(event.target)) || | ||||||
|       isTargetColorPicker |       isTargetColorPicker | ||||||
|     ) { |     ) { | ||||||
|       app.setState({ |  | ||||||
|         toastMessage: "debug: onPointerDown", |  | ||||||
|       }); |  | ||||||
|       editable.onblur = null; |       editable.onblur = null; | ||||||
|       window.addEventListener("pointerup", bindBlurEvent); |       window.addEventListener("pointerup", bindBlurEvent); | ||||||
|       // handle edge-case where pointerup doesn't fire e.g. due to user |       // handle edge-case where pointerup doesn't fire e.g. due to user | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import { AppState } from "../types"; | ||||||
| import { | import { | ||||||
|   ExcalidrawElement, |   ExcalidrawElement, | ||||||
|   ExcalidrawTextElement, |   ExcalidrawTextElement, | ||||||
| @@ -60,7 +61,7 @@ export const isLinearElement = ( | |||||||
| }; | }; | ||||||
|  |  | ||||||
| export const isLinearElementType = ( | export const isLinearElementType = ( | ||||||
|   elementType: ExcalidrawElement["type"], |   elementType: AppState["elementType"], | ||||||
| ): boolean => { | ): boolean => { | ||||||
|   return ( |   return ( | ||||||
|     elementType === "arrow" || elementType === "line" // || elementType === "freedraw" |     elementType === "arrow" || elementType === "line" // || elementType === "freedraw" | ||||||
| @@ -74,7 +75,7 @@ export const isBindingElement = ( | |||||||
| }; | }; | ||||||
|  |  | ||||||
| export const isBindingElementType = ( | export const isBindingElementType = ( | ||||||
|   elementType: ExcalidrawElement["type"], |   elementType: AppState["elementType"], | ||||||
| ): boolean => { | ): boolean => { | ||||||
|   return elementType === "arrow"; |   return elementType === "arrow"; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -47,6 +47,7 @@ export const KEYS = { | |||||||
|   COMMA: ",", |   COMMA: ",", | ||||||
|  |  | ||||||
|   A: "a", |   A: "a", | ||||||
|  |   C: "c", | ||||||
|   D: "d", |   D: "d", | ||||||
|   E: "e", |   E: "e", | ||||||
|   G: "g", |   G: "g", | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ | |||||||
|     "delete": "Delete", |     "delete": "Delete", | ||||||
|     "copyStyles": "Copy styles", |     "copyStyles": "Copy styles", | ||||||
|     "pasteStyles": "Paste styles", |     "pasteStyles": "Paste styles", | ||||||
|  |     "fontColor": "Font color", | ||||||
|     "stroke": "Stroke", |     "stroke": "Stroke", | ||||||
|     "background": "Background", |     "background": "Background", | ||||||
|     "fill": "Fill", |     "fill": "Fill", | ||||||
| @@ -196,7 +197,8 @@ | |||||||
|     "library": "Library", |     "library": "Library", | ||||||
|     "lock": "Keep selected tool active after drawing", |     "lock": "Keep selected tool active after drawing", | ||||||
|     "penMode": "Prevent pinch-zoom and accept freedraw input only from pen", |     "penMode": "Prevent pinch-zoom and accept freedraw input only from pen", | ||||||
|     "link": "Add/ Update link for a selected shape" |     "link": "Add/ Update link for a selected shape", | ||||||
|  |     "eraser": "Eraser" | ||||||
|   }, |   }, | ||||||
|   "headings": { |   "headings": { | ||||||
|     "canvasActions": "Canvas actions", |     "canvasActions": "Canvas actions", | ||||||
| @@ -221,7 +223,8 @@ | |||||||
|     "placeImage": "Click to place the image, or click and drag to set its size manually", |     "placeImage": "Click to place the image, or click and drag to set its size manually", | ||||||
|     "publishLibrary": "Publish your own library", |     "publishLibrary": "Publish your own library", | ||||||
|     "bindTextToElement": "Press enter to add text", |     "bindTextToElement": "Press enter to add text", | ||||||
|     "deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging" |     "deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging", | ||||||
|  |     "eraserRevert": "Hold Alt to revert the elements marked for deletion" | ||||||
|   }, |   }, | ||||||
|   "canvasError": { |   "canvasError": { | ||||||
|     "cannotShowPreview": "Cannot show preview", |     "cannotShowPreview": "Cannot show preview", | ||||||
|   | |||||||
| @@ -56,7 +56,7 @@ | |||||||
|     "babel-loader": "8.2.3", |     "babel-loader": "8.2.3", | ||||||
|     "babel-plugin-transform-class-properties": "6.24.1", |     "babel-plugin-transform-class-properties": "6.24.1", | ||||||
|     "cross-env": "7.0.3", |     "cross-env": "7.0.3", | ||||||
|     "css-loader": "6.6.0", |     "css-loader": "6.7.1", | ||||||
|     "mini-css-extract-plugin": "2.4.6", |     "mini-css-extract-plugin": "2.4.6", | ||||||
|     "postcss-loader": "6.2.1", |     "postcss-loader": "6.2.1", | ||||||
|     "sass-loader": "12.4.0", |     "sass-loader": "12.4.0", | ||||||
|   | |||||||
| @@ -2069,13 +2069,13 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.3: | |||||||
|     shebang-command "^2.0.0" |     shebang-command "^2.0.0" | ||||||
|     which "^2.0.1" |     which "^2.0.1" | ||||||
|  |  | ||||||
| css-loader@6.6.0: | css-loader@6.7.1: | ||||||
|   version "6.6.0" |   version "6.7.1" | ||||||
|   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.6.0.tgz#c792ad5510bd1712618b49381bd0310574fafbd3" |   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.1.tgz#e98106f154f6e1baf3fc3bc455cb9981c1d5fd2e" | ||||||
|   integrity sha512-FK7H2lisOixPT406s5gZM1S3l8GrfhEBT3ZiL2UX1Ng1XWs0y2GPllz/OTyvbaHe12VgQrIXIzuEGVlbUhodqg== |   integrity sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw== | ||||||
|   dependencies: |   dependencies: | ||||||
|     icss-utils "^5.1.0" |     icss-utils "^5.1.0" | ||||||
|     postcss "^8.4.5" |     postcss "^8.4.7" | ||||||
|     postcss-modules-extract-imports "^3.0.0" |     postcss-modules-extract-imports "^3.0.0" | ||||||
|     postcss-modules-local-by-default "^4.0.0" |     postcss-modules-local-by-default "^4.0.0" | ||||||
|     postcss-modules-scope "^3.0.0" |     postcss-modules-scope "^3.0.0" | ||||||
| @@ -3150,10 +3150,10 @@ multicast-dns@^6.0.1: | |||||||
|     dns-packet "^1.3.1" |     dns-packet "^1.3.1" | ||||||
|     thunky "^1.0.2" |     thunky "^1.0.2" | ||||||
|  |  | ||||||
| nanoid@^3.2.0: | nanoid@^3.3.1: | ||||||
|   version "3.2.0" |   version "3.3.1" | ||||||
|   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" |   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" | ||||||
|   integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== |   integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== | ||||||
|  |  | ||||||
| negotiator@0.6.2: | negotiator@0.6.2: | ||||||
|   version "0.6.2" |   version "0.6.2" | ||||||
| @@ -3429,12 +3429,12 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: | |||||||
|   resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" |   resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" | ||||||
|   integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== |   integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== | ||||||
|  |  | ||||||
| postcss@^8.4.5: | postcss@^8.4.7: | ||||||
|   version "8.4.6" |   version "8.4.8" | ||||||
|   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.6.tgz#c5ff3c3c457a23864f32cb45ac9b741498a09ae1" |   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.8.tgz#dad963a76e82c081a0657d3a2f3602ce10c2e032" | ||||||
|   integrity sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA== |   integrity sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ== | ||||||
|   dependencies: |   dependencies: | ||||||
|     nanoid "^3.2.0" |     nanoid "^3.3.1" | ||||||
|     picocolors "^1.0.0" |     picocolors "^1.0.0" | ||||||
|     source-map-js "^1.0.2" |     source-map-js "^1.0.2" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -44,10 +44,10 @@ | |||||||
|     "babel-loader": "8.2.3", |     "babel-loader": "8.2.3", | ||||||
|     "babel-plugin-transform-class-properties": "6.24.1", |     "babel-plugin-transform-class-properties": "6.24.1", | ||||||
|     "cross-env": "7.0.3", |     "cross-env": "7.0.3", | ||||||
|     "css-loader": "6.5.1", |     "css-loader": "6.7.1", | ||||||
|     "file-loader": "6.2.0", |     "file-loader": "6.2.0", | ||||||
|     "sass-loader": "12.4.0", |     "sass-loader": "12.4.0", | ||||||
|     "ts-loader": "9.2.6", |     "ts-loader": "9.2.8", | ||||||
|     "webpack": "5.66.0", |     "webpack": "5.66.0", | ||||||
|     "webpack-bundle-analyzer": "4.5.0", |     "webpack-bundle-analyzer": "4.5.0", | ||||||
|     "webpack-cli": "4.9.2" |     "webpack-cli": "4.9.2" | ||||||
|   | |||||||
| @@ -1565,11 +1565,6 @@ color-name@~1.1.4: | |||||||
|   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" |   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" | ||||||
|   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== |   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== | ||||||
|  |  | ||||||
| colorette@^1.2.2: |  | ||||||
|   version "1.2.2" |  | ||||||
|   resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" |  | ||||||
|   integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== |  | ||||||
|  |  | ||||||
| colorette@^2.0.14: | colorette@^2.0.14: | ||||||
|   version "2.0.16" |   version "2.0.16" | ||||||
|   resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" |   resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" | ||||||
| @@ -1634,18 +1629,18 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.3: | |||||||
|     shebang-command "^2.0.0" |     shebang-command "^2.0.0" | ||||||
|     which "^2.0.1" |     which "^2.0.1" | ||||||
|  |  | ||||||
| css-loader@6.5.1: | css-loader@6.7.1: | ||||||
|   version "6.5.1" |   version "6.7.1" | ||||||
|   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.5.1.tgz#0c43d4fbe0d97f699c91e9818cb585759091d1b1" |   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.1.tgz#e98106f154f6e1baf3fc3bc455cb9981c1d5fd2e" | ||||||
|   integrity sha512-gEy2w9AnJNnD9Kuo4XAP9VflW/ujKoS9c/syO+uWMlm5igc7LysKzPXaDoR2vroROkSwsTS2tGr1yGGEbZOYZQ== |   integrity sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw== | ||||||
|   dependencies: |   dependencies: | ||||||
|     icss-utils "^5.1.0" |     icss-utils "^5.1.0" | ||||||
|     postcss "^8.2.15" |     postcss "^8.4.7" | ||||||
|     postcss-modules-extract-imports "^3.0.0" |     postcss-modules-extract-imports "^3.0.0" | ||||||
|     postcss-modules-local-by-default "^4.0.0" |     postcss-modules-local-by-default "^4.0.0" | ||||||
|     postcss-modules-scope "^3.0.0" |     postcss-modules-scope "^3.0.0" | ||||||
|     postcss-modules-values "^4.0.0" |     postcss-modules-values "^4.0.0" | ||||||
|     postcss-value-parser "^4.1.0" |     postcss-value-parser "^4.2.0" | ||||||
|     semver "^7.3.5" |     semver "^7.3.5" | ||||||
|  |  | ||||||
| cssesc@^3.0.0: | cssesc@^3.0.0: | ||||||
| @@ -2142,10 +2137,10 @@ ms@2.1.2: | |||||||
|   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" |   resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" | ||||||
|   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== |   integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== | ||||||
|  |  | ||||||
| nanoid@^3.1.23: | nanoid@^3.3.1: | ||||||
|   version "3.2.0" |   version "3.3.1" | ||||||
|   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" |   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" | ||||||
|   integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== |   integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== | ||||||
|  |  | ||||||
| neo-async@^2.6.2: | neo-async@^2.6.2: | ||||||
|   version "2.6.2" |   version "2.6.2" | ||||||
| @@ -2292,19 +2287,19 @@ postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: | |||||||
|     uniq "^1.0.1" |     uniq "^1.0.1" | ||||||
|     util-deprecate "^1.0.2" |     util-deprecate "^1.0.2" | ||||||
|  |  | ||||||
| postcss-value-parser@^4.1.0: | postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: | ||||||
|   version "4.1.0" |   version "4.2.0" | ||||||
|   resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" |   resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" | ||||||
|   integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== |   integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== | ||||||
|  |  | ||||||
| postcss@^8.2.15: | postcss@^8.4.7: | ||||||
|   version "8.3.0" |   version "8.4.8" | ||||||
|   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.0.tgz#b1a713f6172ca427e3f05ef1303de8b65683325f" |   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.8.tgz#dad963a76e82c081a0657d3a2f3602ce10c2e032" | ||||||
|   integrity sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ== |   integrity sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ== | ||||||
|   dependencies: |   dependencies: | ||||||
|     colorette "^1.2.2" |     nanoid "^3.3.1" | ||||||
|     nanoid "^3.1.23" |     picocolors "^1.0.0" | ||||||
|     source-map-js "^0.6.2" |     source-map-js "^1.0.2" | ||||||
|  |  | ||||||
| punycode@^2.1.0: | punycode@^2.1.0: | ||||||
|   version "2.1.1" |   version "2.1.1" | ||||||
| @@ -2491,10 +2486,10 @@ sirv@^1.0.7: | |||||||
|     mime "^2.3.1" |     mime "^2.3.1" | ||||||
|     totalist "^1.0.0" |     totalist "^1.0.0" | ||||||
|  |  | ||||||
| source-map-js@^0.6.2: | source-map-js@^1.0.2: | ||||||
|   version "0.6.2" |   version "1.0.2" | ||||||
|   resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e" |   resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" | ||||||
|   integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== |   integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== | ||||||
|  |  | ||||||
| source-map-support@~0.5.19: | source-map-support@~0.5.19: | ||||||
|   version "0.5.19" |   version "0.5.19" | ||||||
| @@ -2605,10 +2600,10 @@ totalist@^1.0.0: | |||||||
|   resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" |   resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df" | ||||||
|   integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== |   integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== | ||||||
|  |  | ||||||
| ts-loader@9.2.6: | ts-loader@9.2.8: | ||||||
|   version "9.2.6" |   version "9.2.8" | ||||||
|   resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.6.tgz#9937c4dd0a1e3dbbb5e433f8102a6601c6615d74" |   resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.8.tgz#e89aa32fa829c5cad0a1d023d6b3adecd51d5a48" | ||||||
|   integrity sha512-QMTC4UFzHmu9wU2VHZEmWWE9cUajjfcdcws+Gh7FhiO+Dy0RnR1bNz0YCHqhI0yRowCE9arVnNxYHqELOy9Hjw== |   integrity sha512-gxSak7IHUuRtwKf3FIPSW1VpZcqF9+MBrHOvBp9cjHh+525SjtCIJKVGjRKIAfxBwDGDGCFF00rTfzB1quxdSw== | ||||||
|   dependencies: |   dependencies: | ||||||
|     chalk "^4.1.0" |     chalk "^4.1.0" | ||||||
|     enhanced-resolve "^5.0.0" |     enhanced-resolve "^5.0.0" | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ import { | |||||||
| } from "../element/types"; | } from "../element/types"; | ||||||
| import { getElementAbsoluteCoords, getElementBounds } from "../element"; | import { getElementAbsoluteCoords, getElementBounds } from "../element"; | ||||||
| import { AppState } from "../types"; | import { AppState } from "../types"; | ||||||
| import { isBoundToContainer } from "../element/typeChecks"; | import { isBoundToContainer, isTextElement } from "../element/typeChecks"; | ||||||
|  |  | ||||||
| export const getElementsWithinSelection = ( | export const getElementsWithinSelection = ( | ||||||
|   elements: readonly NonDeletedExcalidrawElement[], |   elements: readonly NonDeletedExcalidrawElement[], | ||||||
| @@ -41,12 +41,15 @@ export const getCommonAttributeOfSelectedElements = <T>( | |||||||
|   elements: readonly NonDeletedExcalidrawElement[], |   elements: readonly NonDeletedExcalidrawElement[], | ||||||
|   appState: AppState, |   appState: AppState, | ||||||
|   getAttribute: (element: ExcalidrawElement) => T, |   getAttribute: (element: ExcalidrawElement) => T, | ||||||
|  |   onlyBoundTextElements: boolean = false, | ||||||
| ): T | null => { | ): T | null => { | ||||||
|   const attributes = Array.from( |   const attributes = Array.from( | ||||||
|     new Set( |     new Set( | ||||||
|       getSelectedElements(elements, appState).map((element) => |       getSelectedElements(elements, appState, onlyBoundTextElements) | ||||||
|         getAttribute(element), |         .filter((element) => | ||||||
|       ), |           onlyBoundTextElements ? isTextElement(element) : true, | ||||||
|  |         ) | ||||||
|  |         .map((element) => getAttribute(element)), | ||||||
|     ), |     ), | ||||||
|   ); |   ); | ||||||
|   return attributes.length === 1 ? attributes[0] : null; |   return attributes.length === 1 ? attributes[0] : null; | ||||||
|   | |||||||
| @@ -17592,4 +17592,4 @@ Object { | |||||||
|  |  | ||||||
| exports[`regression tests zoom hotkeys: [end of test] number of elements 1`] = `0`; | exports[`regression tests zoom hotkeys: [end of test] number of elements 1`] = `0`; | ||||||
|  |  | ||||||
| exports[`regression tests zoom hotkeys: [end of test] number of renders 1`] = `6`; | exports[`regression tests zoom hotkeys: [end of test] number of renders 1`] = `6`; | ||||||
|   | |||||||
| @@ -77,7 +77,7 @@ export type AppState = { | |||||||
|   // (e.g. text element when typing into the input) |   // (e.g. text element when typing into the input) | ||||||
|   editingElement: NonDeletedExcalidrawElement | null; |   editingElement: NonDeletedExcalidrawElement | null; | ||||||
|   editingLinearElement: LinearElementEditor | null; |   editingLinearElement: LinearElementEditor | null; | ||||||
|   elementType: typeof SHAPES[number]["value"]; |   elementType: typeof SHAPES[number]["value"] | "eraser"; | ||||||
|   elementLocked: boolean; |   elementLocked: boolean; | ||||||
|   penMode: boolean; |   penMode: boolean; | ||||||
|   penDetected: boolean; |   penDetected: boolean; | ||||||
| @@ -113,6 +113,7 @@ export type AppState = { | |||||||
|     | "canvasColorPicker" |     | "canvasColorPicker" | ||||||
|     | "backgroundColorPicker" |     | "backgroundColorPicker" | ||||||
|     | "strokeColorPicker" |     | "strokeColorPicker" | ||||||
|  |     | "fontColorPicker" | ||||||
|     | null; |     | null; | ||||||
|   lastPointerDownWith: PointerType; |   lastPointerDownWith: PointerType; | ||||||
|   selectedElementIds: { [id: string]: boolean }; |   selectedElementIds: { [id: string]: boolean }; | ||||||
| @@ -384,6 +385,7 @@ export type PointerDownState = Readonly<{ | |||||||
|   boxSelection: { |   boxSelection: { | ||||||
|     hasOccurred: boolean; |     hasOccurred: boolean; | ||||||
|   }; |   }; | ||||||
|  |   elementIdsToErase: { [key: ExcalidrawElement["id"]]: boolean }; | ||||||
| }>; | }>; | ||||||
|  |  | ||||||
| export type ExcalidrawImperativeAPI = { | export type ExcalidrawImperativeAPI = { | ||||||
|   | |||||||
| @@ -224,6 +224,9 @@ export const setCursorForShape = ( | |||||||
|   } |   } | ||||||
|   if (shape === "selection") { |   if (shape === "selection") { | ||||||
|     resetCursor(canvas); |     resetCursor(canvas); | ||||||
|  |   } else if (shape === "eraser") { | ||||||
|  |     resetCursor(canvas); | ||||||
|  |  | ||||||
|     // do nothing if image tool is selected which suggests there's |     // do nothing if image tool is selected which suggests there's | ||||||
|     // a image-preview set as the cursor |     // a image-preview set as the cursor | ||||||
|   } else if (shape !== "image") { |   } else if (shape !== "image") { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user