@excalidraw/excalidraw 0.17.1-7441-4e2c539 → 0.17.1-7500-ac247a0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/browser/dev/excalidraw-assets-dev/{chunk-SUHLFFEF.js → chunk-KGZXLFLR.js} +12342 -12294
  2. package/dist/browser/dev/excalidraw-assets-dev/chunk-KGZXLFLR.js.map +7 -0
  3. package/dist/browser/dev/excalidraw-assets-dev/{image-NOPDRTTM.css → image-3MFRCKYM.css} +3929 -3929
  4. package/dist/browser/dev/excalidraw-assets-dev/image-3MFRCKYM.css.map +7 -0
  5. package/dist/browser/dev/excalidraw-assets-dev/{image-HYNUJ3XL.js → image-5TVMINCA.js} +2 -2
  6. package/dist/browser/dev/index.js +17 -7
  7. package/dist/browser/prod/excalidraw-assets/chunk-4YN2HN3S.js +257 -0
  8. package/dist/browser/prod/excalidraw-assets/{image-DZ6B4AID.js → image-LTLHTTSE.js} +1 -1
  9. package/dist/browser/prod/excalidraw-assets/image-QBL334OA.css +1 -0
  10. package/dist/browser/prod/index.js +1 -1
  11. package/dist/dev/index.js +785 -740
  12. package/dist/dev/index.js.map +4 -4
  13. package/dist/excalidraw/actions/actionAddToLibrary.d.ts +3 -3
  14. package/dist/excalidraw/actions/actionBoundText.d.ts +2 -2
  15. package/dist/excalidraw/actions/actionCanvas.d.ts +12 -12
  16. package/dist/excalidraw/actions/actionClipboard.d.ts +7 -7
  17. package/dist/excalidraw/actions/actionDeleteSelected.d.ts +3 -3
  18. package/dist/excalidraw/actions/actionElementLock.d.ts +2 -2
  19. package/dist/excalidraw/actions/actionExport.d.ts +8 -8
  20. package/dist/excalidraw/actions/actionFinalize.d.ts +2 -2
  21. package/dist/excalidraw/actions/actionFrame.d.ts +3 -3
  22. package/dist/excalidraw/actions/actionGroup.d.ts +2 -2
  23. package/dist/excalidraw/actions/actionLinearEditor.d.ts +1 -1
  24. package/dist/excalidraw/actions/actionMenu.d.ts +2 -2
  25. package/dist/excalidraw/actions/actionNavigate.d.ts +2 -2
  26. package/dist/excalidraw/actions/actionProperties.d.ts +13 -13
  27. package/dist/excalidraw/actions/actionSelectAll.d.ts +1 -1
  28. package/dist/excalidraw/actions/actionStyles.d.ts +1 -1
  29. package/dist/excalidraw/actions/actionToggleGridMode.d.ts +1 -1
  30. package/dist/excalidraw/actions/actionToggleObjectsSnapMode.d.ts +1 -1
  31. package/dist/excalidraw/actions/actionToggleStats.d.ts +1 -1
  32. package/dist/excalidraw/actions/actionToggleViewMode.d.ts +1 -1
  33. package/dist/excalidraw/actions/actionToggleZenMode.d.ts +1 -1
  34. package/dist/excalidraw/components/App.d.ts +9 -1
  35. package/dist/excalidraw/components/App.js +91 -71
  36. package/dist/excalidraw/components/ImageExportDialog.js +1 -1
  37. package/dist/excalidraw/components/PublishLibrary.js +1 -1
  38. package/dist/excalidraw/components/Sidebar/Sidebar.d.ts +1 -1
  39. package/dist/excalidraw/components/dropdownMenu/common.d.ts +1 -1
  40. package/dist/excalidraw/constants.d.ts +2 -0
  41. package/dist/excalidraw/constants.js +4 -0
  42. package/dist/excalidraw/data/index.js +1 -1
  43. package/dist/excalidraw/element/Hyperlink.d.ts +1 -1
  44. package/dist/excalidraw/element/embeddable.d.ts +1 -1
  45. package/dist/excalidraw/element/linearElementEditor.d.ts +1 -1
  46. package/dist/excalidraw/emitter.d.ts +5 -9
  47. package/dist/excalidraw/emitter.js +12 -12
  48. package/dist/excalidraw/frame.js +1 -1
  49. package/dist/excalidraw/hooks/useLibraryItemSvg.js +1 -1
  50. package/dist/excalidraw/index.d.ts +9 -4
  51. package/dist/excalidraw/index.js +9 -4
  52. package/dist/excalidraw/scene/export.js +1 -1
  53. package/dist/excalidraw/types.d.ts +1 -1
  54. package/dist/excalidraw/utils.d.ts +7 -1
  55. package/dist/excalidraw/utils.js +15 -0
  56. package/dist/prod/index.js +30 -30
  57. package/dist/utils/bbox.d.ts +2 -2
  58. package/dist/utils/export.d.ts +3 -9
  59. package/dist/utils/export.js +13 -9
  60. package/dist/utils/index.d.ts +3 -0
  61. package/dist/utils/index.js +3 -0
  62. package/dist/utils/withinBounds.d.ts +1 -1
  63. package/dist/utils/withinBounds.js +1 -3
  64. package/package.json +1 -1
  65. package/dist/browser/dev/excalidraw-assets-dev/chunk-SUHLFFEF.js.map +0 -7
  66. package/dist/browser/dev/excalidraw-assets-dev/image-NOPDRTTM.css.map +0 -7
  67. package/dist/browser/prod/excalidraw-assets/chunk-HE2P7BQ6.js +0 -257
  68. package/dist/browser/prod/excalidraw-assets/image-J2QCCYAR.css +0 -1
  69. /package/dist/browser/dev/excalidraw-assets-dev/{image-HYNUJ3XL.js.map → image-5TVMINCA.js.map} +0 -0
@@ -61,6 +61,7 @@ declare class App extends React.Component<AppProps, AppState> {
61
61
  hitLinkElement?: NonDeletedExcalidrawElement;
62
62
  lastPointerDownEvent: React.PointerEvent<HTMLElement> | null;
63
63
  lastPointerUpEvent: React.PointerEvent<HTMLElement> | PointerEvent | null;
64
+ lastPointerMoveEvent: PointerEvent | null;
64
65
  lastViewportPosition: {
65
66
  x: number;
66
67
  y: number;
@@ -211,6 +212,8 @@ declare class App extends React.Component<AppProps, AppState> {
211
212
  onScrollChangeEmitter: Emitter<[scrollX: number, scrollY: number, zoom: Readonly<{
212
213
  value: import("../types").NormalizedZoomValue;
213
214
  }>]>;
215
+ missingPointerEventCleanupEmitter: Emitter<[event: PointerEvent | null]>;
216
+ onRemoveEventListenersEmitter: Emitter<[]>;
214
217
  constructor(props: AppProps);
215
218
  private onWindowMessage;
216
219
  private cacheEmbeddableRef;
@@ -258,9 +261,9 @@ declare class App extends React.Component<AppProps, AppState> {
258
261
  componentDidMount(): Promise<void>;
259
262
  componentWillUnmount(): void;
260
263
  private onResize;
261
- private removeEventListeners;
262
264
  /** generally invoked only if fullscreen was invoked programmatically */
263
265
  private onFullscreenChange;
266
+ private removeEventListeners;
264
267
  private addEventListeners;
265
268
  componentDidUpdate(prevProps: AppProps, prevState: AppState): void;
266
269
  private renderInteractiveSceneCallback;
@@ -388,6 +391,11 @@ declare class App extends React.Component<AppProps, AppState> {
388
391
  private handleCanvasPointerUp;
389
392
  private maybeOpenContextMenuAfterPointerDownOnTouchDevices;
390
393
  private resetContextMenuTimer;
394
+ /**
395
+ * pointerup may not fire in certian cases (user tabs away...), so in order
396
+ * to properly cleanup pointerdown state, we need to fire any hanging
397
+ * pointerup handlers manually
398
+ */
391
399
  private maybeCleanupAfterMissingPointerUp;
392
400
  private handleCanvasPanUsingWheelOrSpaceDrag;
393
401
  private updateGestureOnPointerDown;
@@ -11,7 +11,7 @@ import { actions } from "../actions/register";
11
11
  import { trackEvent } from "../analytics";
12
12
  import { getDefaultAppState, isEraserActive, isHandToolActive, } from "../appState";
13
13
  import { copyTextToSystemClipboard, parseClipboard, } from "../clipboard";
14
- import { APP_NAME, CURSOR_TYPE, DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT, DEFAULT_VERTICAL_ALIGN, DRAGGING_THRESHOLD, ELEMENT_READY_TO_ERASE_OPACITY, ELEMENT_SHIFT_TRANSLATE_AMOUNT, ELEMENT_TRANSLATE_AMOUNT, ENV, EVENT, FRAME_STYLE, GRID_SIZE, IMAGE_MIME_TYPES, IMAGE_RENDER_TIMEOUT, isAndroid, isBrave, LINE_CONFIRM_THRESHOLD, MAX_ALLOWED_FILE_BYTES, MIME_TYPES, MQ_MAX_HEIGHT_LANDSCAPE, MQ_MAX_WIDTH_LANDSCAPE, MQ_MAX_WIDTH_PORTRAIT, MQ_RIGHT_SIDEBAR_MIN_WIDTH, POINTER_BUTTON, ROUNDNESS, SCROLL_TIMEOUT, TAP_TWICE_TIMEOUT, TEXT_TO_CENTER_SNAP_THRESHOLD, THEME, THEME_FILTER, TOUCH_CTX_MENU_TIMEOUT, VERTICAL_ALIGN, YOUTUBE_STATES, ZOOM_STEP, POINTER_EVENTS, TOOL_TYPE, EDITOR_LS_KEYS, } from "../constants";
14
+ import { APP_NAME, CURSOR_TYPE, DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT, DEFAULT_VERTICAL_ALIGN, DRAGGING_THRESHOLD, ELEMENT_READY_TO_ERASE_OPACITY, ELEMENT_SHIFT_TRANSLATE_AMOUNT, ELEMENT_TRANSLATE_AMOUNT, ENV, EVENT, FRAME_STYLE, GRID_SIZE, IMAGE_MIME_TYPES, IMAGE_RENDER_TIMEOUT, isBrave, LINE_CONFIRM_THRESHOLD, MAX_ALLOWED_FILE_BYTES, MIME_TYPES, MQ_MAX_HEIGHT_LANDSCAPE, MQ_MAX_WIDTH_LANDSCAPE, MQ_MAX_WIDTH_PORTRAIT, MQ_RIGHT_SIDEBAR_MIN_WIDTH, POINTER_BUTTON, ROUNDNESS, SCROLL_TIMEOUT, TAP_TWICE_TIMEOUT, TEXT_TO_CENTER_SNAP_THRESHOLD, THEME, THEME_FILTER, TOUCH_CTX_MENU_TIMEOUT, VERTICAL_ALIGN, YOUTUBE_STATES, ZOOM_STEP, POINTER_EVENTS, TOOL_TYPE, EDITOR_LS_KEYS, isIOS, } from "../constants";
15
15
  import { exportCanvas, loadFromBlob } from "../data";
16
16
  import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
17
17
  import { restore, restoreElements } from "../data/restore";
@@ -32,7 +32,7 @@ import { calculateScrollCenter, getElementsAtPosition, getElementsWithinSelectio
32
32
  import Scene from "../scene/Scene";
33
33
  import { getStateForZoom } from "../scene/zoom";
34
34
  import { findShapeByKey } from "../shapes";
35
- import { debounce, distance, getFontString, getNearestScrollableContainer, isInputLike, isToolIcon, isWritableElement, sceneCoordsToViewportCoords, tupleToCoors, viewportCoordsToSceneCoords, withBatchedUpdates, wrapEvent, withBatchedUpdatesThrottled, updateObject, updateActiveTool, getShortcutKey, isTransparent, easeToValuesRAF, muteFSAbortError, isTestEnv, easeOut, updateStable, } from "../utils";
35
+ import { debounce, distance, getFontString, getNearestScrollableContainer, isInputLike, isToolIcon, isWritableElement, sceneCoordsToViewportCoords, tupleToCoors, viewportCoordsToSceneCoords, withBatchedUpdates, wrapEvent, withBatchedUpdatesThrottled, updateObject, updateActiveTool, getShortcutKey, isTransparent, easeToValuesRAF, muteFSAbortError, isTestEnv, easeOut, updateStable, addEventListener, } from "../utils";
36
36
  import { createSrcDoc, embeddableURLValidator, extractSrc, getEmbedLink, } from "../element/embeddable";
37
37
  import { ContextMenu, CONTEXT_MENU_SEPARATOR, } from "./ContextMenu";
38
38
  import LayerUI from "./LayerUI";
@@ -72,7 +72,7 @@ import { setEraserCursor, setCursor, resetCursor, setCursorForShape, } from "../
72
72
  import { Emitter } from "../emitter";
73
73
  import { ElementCanvasButtons } from "../element/ElementCanvasButtons";
74
74
  import { diagramToHTML } from "../data/magic";
75
- import { elementsOverlappingBBox, exportToBlob } from "../../utils/export";
75
+ import { elementsOverlappingBBox, exportToBlob } from "../../utils/index";
76
76
  import { COLOR_PALETTE } from "../colors";
77
77
  import { ElementCanvasButton } from "./MagicButton";
78
78
  import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
@@ -166,6 +166,7 @@ class App extends React.Component {
166
166
  hitLinkElement;
167
167
  lastPointerDownEvent = null;
168
168
  lastPointerUpEvent = null;
169
+ lastPointerMoveEvent = null;
169
170
  lastViewportPosition = { x: 0, y: 0 };
170
171
  laserPathManager = new LaserPathManager(this);
171
172
  onChangeEmitter = new Emitter();
@@ -173,6 +174,8 @@ class App extends React.Component {
173
174
  onPointerUpEmitter = new Emitter();
174
175
  onUserFollowEmitter = new Emitter();
175
176
  onScrollChangeEmitter = new Emitter();
177
+ missingPointerEventCleanupEmitter = new Emitter();
178
+ onRemoveEventListenersEmitter = new Emitter();
176
179
  constructor(props) {
177
180
  super(props);
178
181
  const defaultAppState = getDefaultAppState();
@@ -1339,6 +1342,7 @@ class App extends React.Component {
1339
1342
  return false;
1340
1343
  };
1341
1344
  async componentDidMount() {
1345
+ console.log("HELLO IS");
1342
1346
  this.unmounted = false;
1343
1347
  this.excalidrawContainerValue.container =
1344
1348
  this.excalidrawContainerRef.current;
@@ -1414,7 +1418,7 @@ class App extends React.Component {
1414
1418
  this.scene.destroy();
1415
1419
  this.library.destroy();
1416
1420
  this.laserPathManager.destroy();
1417
- this.onChangeEmitter.destroy();
1421
+ this.onChangeEmitter.clear();
1418
1422
  ShapeCache.destroy();
1419
1423
  SnapCache.destroy();
1420
1424
  clearTimeout(touchTimeout);
@@ -1433,27 +1437,6 @@ class App extends React.Component {
1433
1437
  }
1434
1438
  this.setState({});
1435
1439
  });
1436
- removeEventListeners() {
1437
- document.removeEventListener(EVENT.POINTER_UP, this.removePointer);
1438
- document.removeEventListener(EVENT.COPY, this.onCopy);
1439
- document.removeEventListener(EVENT.PASTE, this.pasteFromClipboard);
1440
- document.removeEventListener(EVENT.CUT, this.onCut);
1441
- this.excalidrawContainerRef.current?.removeEventListener(EVENT.WHEEL, this.onWheel);
1442
- this.nearestScrollableContainer?.removeEventListener(EVENT.SCROLL, this.onScroll);
1443
- document.removeEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
1444
- document.removeEventListener(EVENT.MOUSE_MOVE, this.updateCurrentCursorPosition, false);
1445
- document.removeEventListener(EVENT.KEYUP, this.onKeyUp);
1446
- window.removeEventListener(EVENT.RESIZE, this.onResize, false);
1447
- window.removeEventListener(EVENT.UNLOAD, this.onUnload, false);
1448
- window.removeEventListener(EVENT.BLUR, this.onBlur, false);
1449
- this.excalidrawContainerRef.current?.removeEventListener(EVENT.DRAG_OVER, this.disableEvent, false);
1450
- this.excalidrawContainerRef.current?.removeEventListener(EVENT.DROP, this.disableEvent, false);
1451
- document.removeEventListener(EVENT.GESTURE_START, this.onGestureStart, false);
1452
- document.removeEventListener(EVENT.GESTURE_CHANGE, this.onGestureChange, false);
1453
- document.removeEventListener(EVENT.GESTURE_END, this.onGestureEnd, false);
1454
- document.removeEventListener(EVENT.FULLSCREENCHANGE, this.onFullscreenChange);
1455
- window.removeEventListener(EVENT.MESSAGE, this.onWindowMessage, false);
1456
- }
1457
1440
  /** generally invoked only if fullscreen was invoked programmatically */
1458
1441
  onFullscreenChange = () => {
1459
1442
  if (
@@ -1465,41 +1448,39 @@ class App extends React.Component {
1465
1448
  });
1466
1449
  }
1467
1450
  };
1451
+ removeEventListeners() {
1452
+ this.onRemoveEventListenersEmitter.trigger();
1453
+ }
1468
1454
  addEventListeners() {
1455
+ // remove first as we can add event listeners multiple times
1469
1456
  this.removeEventListeners();
1470
- window.addEventListener(EVENT.MESSAGE, this.onWindowMessage, false);
1471
- document.addEventListener(EVENT.POINTER_UP, this.removePointer); // #3553
1472
- document.addEventListener(EVENT.COPY, this.onCopy);
1473
- this.excalidrawContainerRef.current?.addEventListener(EVENT.WHEEL, this.onWheel, { passive: false });
1457
+ // -------------------------------------------------------------------------
1458
+ // view+edit mode listeners
1459
+ // -------------------------------------------------------------------------
1474
1460
  if (this.props.handleKeyboardGlobally) {
1475
- document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
1461
+ this.onRemoveEventListenersEmitter.once(addEventListener(document, EVENT.KEYDOWN, this.onKeyDown, false));
1476
1462
  }
1477
- document.addEventListener(EVENT.KEYUP, this.onKeyUp, { passive: true });
1478
- document.addEventListener(EVENT.MOUSE_MOVE, this.updateCurrentCursorPosition);
1463
+ this.onRemoveEventListenersEmitter.once(addEventListener(this.excalidrawContainerRef.current, EVENT.WHEEL, this.onWheel, { passive: false }), addEventListener(window, EVENT.MESSAGE, this.onWindowMessage, false), addEventListener(document, EVENT.POINTER_UP, this.removePointer), // #3553
1464
+ addEventListener(document, EVENT.COPY, this.onCopy), addEventListener(document, EVENT.KEYUP, this.onKeyUp, { passive: true }), addEventListener(document, EVENT.MOUSE_MOVE, this.updateCurrentCursorPosition),
1479
1465
  // rerender text elements on font load to fix #637 && #1553
1480
- document.fonts?.addEventListener?.("loadingdone", (event) => {
1466
+ addEventListener(document.fonts, "loadingdone", (event) => {
1481
1467
  const loadedFontFaces = event.fontfaces;
1482
1468
  this.fonts.onFontsLoaded(loadedFontFaces);
1483
- });
1469
+ }),
1484
1470
  // Safari-only desktop pinch zoom
1485
- document.addEventListener(EVENT.GESTURE_START, this.onGestureStart, false);
1486
- document.addEventListener(EVENT.GESTURE_CHANGE, this.onGestureChange, false);
1487
- document.addEventListener(EVENT.GESTURE_END, this.onGestureEnd, false);
1471
+ addEventListener(document, EVENT.GESTURE_START, this.onGestureStart, false), addEventListener(document, EVENT.GESTURE_CHANGE, this.onGestureChange, false), addEventListener(document, EVENT.GESTURE_END, this.onGestureEnd, false), addEventListener(window, EVENT.FOCUS, () => {
1472
+ this.maybeCleanupAfterMissingPointerUp(null);
1473
+ }));
1488
1474
  if (this.state.viewModeEnabled) {
1489
1475
  return;
1490
1476
  }
1491
- document.addEventListener(EVENT.FULLSCREENCHANGE, this.onFullscreenChange);
1492
- document.addEventListener(EVENT.PASTE, this.pasteFromClipboard);
1493
- document.addEventListener(EVENT.CUT, this.onCut);
1477
+ // -------------------------------------------------------------------------
1478
+ // edit-mode listeners only
1479
+ // -------------------------------------------------------------------------
1480
+ this.onRemoveEventListenersEmitter.once(addEventListener(document, EVENT.FULLSCREENCHANGE, this.onFullscreenChange), addEventListener(document, EVENT.PASTE, this.pasteFromClipboard), addEventListener(document, EVENT.CUT, this.onCut), addEventListener(window, EVENT.RESIZE, this.onResize, false), addEventListener(window, EVENT.UNLOAD, this.onUnload, false), addEventListener(window, EVENT.BLUR, this.onBlur, false), addEventListener(this.excalidrawContainerRef.current, EVENT.DRAG_OVER, this.disableEvent, false), addEventListener(this.excalidrawContainerRef.current, EVENT.DROP, this.disableEvent, false));
1494
1481
  if (this.props.detectScroll) {
1495
- this.nearestScrollableContainer = getNearestScrollableContainer(this.excalidrawContainerRef.current);
1496
- this.nearestScrollableContainer.addEventListener(EVENT.SCROLL, this.onScroll);
1497
- }
1498
- window.addEventListener(EVENT.RESIZE, this.onResize, false);
1499
- window.addEventListener(EVENT.UNLOAD, this.onUnload, false);
1500
- window.addEventListener(EVENT.BLUR, this.onBlur, false);
1501
- this.excalidrawContainerRef.current?.addEventListener(EVENT.DRAG_OVER, this.disableEvent, false);
1502
- this.excalidrawContainerRef.current?.addEventListener(EVENT.DROP, this.disableEvent, false);
1482
+ this.onRemoveEventListenersEmitter.once(addEventListener(getNearestScrollableContainer(this.excalidrawContainerRef.current), EVENT.SCROLL, this.onScroll));
1483
+ }
1503
1484
  }
1504
1485
  componentDidUpdate(prevProps, prevState) {
1505
1486
  this.updateEmbeddables();
@@ -1664,9 +1645,8 @@ class App extends React.Component {
1664
1645
  didTapTwice = false;
1665
1646
  }
1666
1647
  onTouchStart = (event) => {
1667
- // fix for Apple Pencil Scribble
1668
- // On Android, preventing the event would disable contextMenu on tap-hold
1669
- if (!isAndroid) {
1648
+ // fix for Apple Pencil Scribble (do not prevent for other devices)
1649
+ if (isIOS) {
1670
1650
  event.preventDefault();
1671
1651
  }
1672
1652
  if (!didTapTwice) {
@@ -1687,9 +1667,6 @@ class App extends React.Component {
1687
1667
  didTapTwice = false;
1688
1668
  clearTimeout(tappedTwiceTimer);
1689
1669
  }
1690
- if (isAndroid) {
1691
- event.preventDefault();
1692
- }
1693
1670
  if (event.touches.length === 2) {
1694
1671
  this.setState({
1695
1672
  selectedElementIds: makeNextSelectedElementIds({}, this.state),
@@ -3021,6 +2998,7 @@ class App extends React.Component {
3021
2998
  };
3022
2999
  handleCanvasPointerMove = (event) => {
3023
3000
  this.savePointer(event.clientX, event.clientY, this.state.cursorButton);
3001
+ this.lastPointerMoveEvent = event.nativeEvent;
3024
3002
  if (gesture.pointers.has(event.pointerId)) {
3025
3003
  gesture.pointers.set(event.pointerId, {
3026
3004
  x: event.clientX,
@@ -3411,6 +3389,7 @@ class App extends React.Component {
3411
3389
  }
3412
3390
  }
3413
3391
  handleCanvasPointerDown = (event) => {
3392
+ this.maybeCleanupAfterMissingPointerUp(event.nativeEvent);
3414
3393
  this.maybeUnfollowRemoteUser();
3415
3394
  // since contextMenu options are potentially evaluated on each render,
3416
3395
  // and an contextMenu action may depend on selection state, we must
@@ -3463,7 +3442,6 @@ class App extends React.Component {
3463
3442
  selection.removeAllRanges();
3464
3443
  }
3465
3444
  this.maybeOpenContextMenuAfterPointerDownOnTouchDevices(event);
3466
- this.maybeCleanupAfterMissingPointerUp(event);
3467
3445
  //fires only once, if pen is detected, penMode is enabled
3468
3446
  //the user can disable this by toggling the penMode button
3469
3447
  if (!this.state.penDetected && event.pointerType === "pen") {
@@ -3493,9 +3471,47 @@ class App extends React.Component {
3493
3471
  cursorButton: "down",
3494
3472
  });
3495
3473
  this.savePointer(event.clientX, event.clientY, "down");
3474
+ if (event.button === POINTER_BUTTON.ERASER &&
3475
+ this.state.activeTool.type !== TOOL_TYPE.eraser) {
3476
+ this.setState({
3477
+ activeTool: updateActiveTool(this.state, {
3478
+ type: TOOL_TYPE.eraser,
3479
+ lastActiveToolBeforeEraser: this.state.activeTool,
3480
+ }),
3481
+ }, () => {
3482
+ this.handleCanvasPointerDown(event);
3483
+ const onPointerUp = () => {
3484
+ unsubPointerUp();
3485
+ unsubCleanup?.();
3486
+ if (isEraserActive(this.state)) {
3487
+ this.setState({
3488
+ activeTool: updateActiveTool(this.state, {
3489
+ ...(this.state.activeTool.lastActiveTool || {
3490
+ type: TOOL_TYPE.selection,
3491
+ }),
3492
+ lastActiveToolBeforeEraser: null,
3493
+ }),
3494
+ });
3495
+ }
3496
+ };
3497
+ const unsubPointerUp = addEventListener(window, EVENT.POINTER_UP, onPointerUp, {
3498
+ once: true,
3499
+ });
3500
+ let unsubCleanup;
3501
+ // subscribe inside rAF lest it'd be triggered on the same pointerdown
3502
+ // if we start erasing while coming from blurred document since
3503
+ // we cleanup pointer events on focus
3504
+ requestAnimationFrame(() => {
3505
+ unsubCleanup =
3506
+ this.missingPointerEventCleanupEmitter.once(onPointerUp);
3507
+ });
3508
+ });
3509
+ return;
3510
+ }
3496
3511
  // only handle left mouse button or touch
3497
3512
  if (event.button !== POINTER_BUTTON.MAIN &&
3498
- event.button !== POINTER_BUTTON.TOUCH) {
3513
+ event.button !== POINTER_BUTTON.TOUCH &&
3514
+ event.button !== POINTER_BUTTON.ERASER) {
3499
3515
  return;
3500
3516
  }
3501
3517
  // don't select while panning
@@ -3578,7 +3594,7 @@ class App extends React.Component {
3578
3594
  const onPointerUp = this.onPointerUpFromPointerDownHandler(pointerDownState);
3579
3595
  const onKeyDown = this.onKeyDownFromPointerDownHandler(pointerDownState);
3580
3596
  const onKeyUp = this.onKeyUpFromPointerDownHandler(pointerDownState);
3581
- lastPointerUp = onPointerUp;
3597
+ this.missingPointerEventCleanupEmitter.once((_event) => onPointerUp(_event || event.nativeEvent));
3582
3598
  if (!this.state.viewModeEnabled || this.state.activeTool.type === "laser") {
3583
3599
  window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
3584
3600
  window.addEventListener(EVENT.POINTER_UP, onPointerUp);
@@ -3655,14 +3671,15 @@ class App extends React.Component {
3655
3671
  touchTimeout = 0;
3656
3672
  invalidateContextMenu = false;
3657
3673
  };
3658
- maybeCleanupAfterMissingPointerUp(event) {
3659
- if (lastPointerUp !== null) {
3660
- // Unfortunately, sometimes we don't get a pointerup after a pointerdown,
3661
- // this can happen when a contextual menu or alert is triggered. In order to avoid
3662
- // being in a weird state, we clean up on the next pointerdown
3663
- lastPointerUp(event);
3664
- }
3665
- }
3674
+ /**
3675
+ * pointerup may not fire in certian cases (user tabs away...), so in order
3676
+ * to properly cleanup pointerdown state, we need to fire any hanging
3677
+ * pointerup handlers manually
3678
+ */
3679
+ maybeCleanupAfterMissingPointerUp = (event) => {
3680
+ lastPointerUp?.();
3681
+ this.missingPointerEventCleanupEmitter.trigger(event).clear();
3682
+ };
3666
3683
  // Returns whether the event is a panning
3667
3684
  handleCanvasPanUsingWheelOrSpaceDrag = (event) => {
3668
3685
  if (!(gesture.pointers.size <= 1 &&
@@ -3818,9 +3835,9 @@ class App extends React.Component {
3818
3835
  this.handlePointerMoveOverScrollbars(event, pointerDownState);
3819
3836
  });
3820
3837
  const onPointerUp = withBatchedUpdates(() => {
3838
+ lastPointerUp = null;
3821
3839
  isDraggingScrollBar = false;
3822
3840
  setCursorForShape(this.interactiveCanvas, this.state);
3823
- lastPointerUp = null;
3824
3841
  this.setState({
3825
3842
  cursorButton: "up",
3826
3843
  });
@@ -4789,6 +4806,7 @@ class App extends React.Component {
4789
4806
  }
4790
4807
  onPointerUpFromPointerDownHandler(pointerDownState) {
4791
4808
  return withBatchedUpdates((childEvent) => {
4809
+ this.removePointer(childEvent);
4792
4810
  if (pointerDownState.eventListeners.onMove) {
4793
4811
  pointerDownState.eventListeners.onMove.flush();
4794
4812
  }
@@ -4860,7 +4878,7 @@ class App extends React.Component {
4860
4878
  }
4861
4879
  }
4862
4880
  }
4863
- lastPointerUp = null;
4881
+ this.missingPointerEventCleanupEmitter.clear();
4864
4882
  window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.eventListeners.onMove);
4865
4883
  window.removeEventListener(EVENT.POINTER_UP, pointerDownState.eventListeners.onUp);
4866
4884
  window.removeEventListener(EVENT.KEYDOWN, pointerDownState.eventListeners.onKeyDown);
@@ -5079,12 +5097,14 @@ class App extends React.Component {
5079
5097
  });
5080
5098
  }
5081
5099
  }
5082
- if (isEraserActive(this.state)) {
5083
- const draggedDistance = distance2d(this.lastPointerDownEvent.clientX, this.lastPointerDownEvent.clientY, this.lastPointerUpEvent.clientX, this.lastPointerUpEvent.clientY);
5100
+ const pointerStart = this.lastPointerDownEvent;
5101
+ const pointerEnd = this.lastPointerUpEvent || this.lastPointerMoveEvent;
5102
+ if (isEraserActive(this.state) && pointerStart && pointerEnd) {
5103
+ const draggedDistance = distance2d(pointerStart.clientX, pointerStart.clientY, pointerEnd.clientX, pointerEnd.clientY);
5084
5104
  if (draggedDistance === 0) {
5085
5105
  const scenePointer = viewportCoordsToSceneCoords({
5086
- clientX: this.lastPointerUpEvent.clientX,
5087
- clientY: this.lastPointerUpEvent.clientY,
5106
+ clientX: pointerEnd.clientX,
5107
+ clientY: pointerEnd.clientY,
5088
5108
  }, this.state);
5089
5109
  const hitElements = this.getElementsAtPosition(scenePointer.x, scenePointer.y);
5090
5110
  hitElements.forEach((hitElement) => (pointerDownState.elementIdsToErase[hitElement.id] = {
@@ -7,7 +7,7 @@ import { canvasToBlob } from "../data/blob";
7
7
  import { nativeFileSystemSupported } from "../data/filesystem";
8
8
  import { t } from "../i18n";
9
9
  import { isSomeElementSelected } from "../scene";
10
- import { exportToCanvas } from "../../utils/export";
10
+ import { exportToCanvas } from "../../utils/index";
11
11
  import { copyIcon, downloadIcon, helpIcon } from "./icons";
12
12
  import { Dialog } from "./Dialog";
13
13
  import { RadioGroup } from "./RadioGroup";
@@ -4,7 +4,7 @@ import OpenColor from "open-color";
4
4
  import { Dialog } from "./Dialog";
5
5
  import { t } from "../i18n";
6
6
  import Trans from "./Trans";
7
- import { exportToCanvas, exportToSvg } from "../../utils/export";
7
+ import { exportToCanvas, exportToSvg } from "../../utils/index";
8
8
  import { EDITOR_LS_KEYS, EXPORT_DATA_TYPES, EXPORT_SOURCE, MIME_TYPES, VERSIONS, } from "../constants";
9
9
  import { canvasToBlob, resizeImageFile } from "../data/blob";
10
10
  import { chunk } from "../utils";
@@ -22,7 +22,7 @@ export declare const SidebarInner: React.ForwardRefExoticComponent<Pick<{
22
22
  docked?: boolean | undefined;
23
23
  className?: string | undefined;
24
24
  __fallback?: boolean | undefined;
25
- } & Omit<React.RefAttributes<HTMLDivElement>, "onSelect">, "name" | "children" | "key" | "className" | "onDock" | "docked" | "onStateChange" | "__fallback"> & React.RefAttributes<HTMLDivElement>>;
25
+ } & Omit<React.RefAttributes<HTMLDivElement>, "onSelect">, "name" | "children" | "key" | "className" | "__fallback" | "onDock" | "docked" | "onStateChange"> & React.RefAttributes<HTMLDivElement>>;
26
26
  export declare const Sidebar: React.ForwardRefExoticComponent<{
27
27
  name: string;
28
28
  children: React.ReactNode;
@@ -3,4 +3,4 @@ export declare const DropdownMenuContentPropsContext: React.Context<{
3
3
  onSelect?: ((event: Event) => void) | undefined;
4
4
  }>;
5
5
  export declare const getDropdownMenuItemClassName: (className?: string, selected?: boolean) => string;
6
- export declare const useHandleDropdownMenuItemClick: (origOnClick: React.MouseEventHandler<HTMLAnchorElement | HTMLButtonElement> | undefined, onSelect: ((event: Event) => void) | undefined) => (event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement, MouseEvent>) => void;
6
+ export declare const useHandleDropdownMenuItemClick: (origOnClick: React.MouseEventHandler<HTMLAnchorElement | HTMLButtonElement> | undefined, onSelect: ((event: Event) => void) | undefined) => (event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement, MouseEvent>) => void;
@@ -6,6 +6,7 @@ export declare const isAndroid: boolean;
6
6
  export declare const isFirefox: boolean;
7
7
  export declare const isChrome: boolean;
8
8
  export declare const isSafari: boolean;
9
+ export declare const isIOS: boolean;
9
10
  export declare const isBrave: () => boolean;
10
11
  export declare const APP_NAME = "Excalidraw";
11
12
  export declare const DRAGGING_THRESHOLD = 10;
@@ -28,6 +29,7 @@ export declare const POINTER_BUTTON: {
28
29
  readonly WHEEL: 1;
29
30
  readonly SECONDARY: 2;
30
31
  readonly TOUCH: -1;
32
+ readonly ERASER: 5;
31
33
  };
32
34
  export declare const POINTER_EVENTS: {
33
35
  readonly enabled: "all";
@@ -8,6 +8,9 @@ export const isFirefox = "netscape" in window &&
8
8
  navigator.userAgent.indexOf("Gecko") > 1;
9
9
  export const isChrome = navigator.userAgent.indexOf("Chrome") !== -1;
10
10
  export const isSafari = !isChrome && navigator.userAgent.indexOf("Safari") !== -1;
11
+ export const isIOS = /iPad|iPhone/.test(navigator.platform) ||
12
+ // iPadOS 13+
13
+ (navigator.userAgent.includes("Mac") && "ontouchend" in document);
11
14
  // keeping function so it can be mocked in test
12
15
  export const isBrave = () => navigator.brave?.isBrave?.name === "isBrave";
13
16
  export const APP_NAME = "Excalidraw";
@@ -31,6 +34,7 @@ export const POINTER_BUTTON = {
31
34
  WHEEL: 1,
32
35
  SECONDARY: 2,
33
36
  TOUCH: -1,
37
+ ERASER: 5,
34
38
  };
35
39
  export const POINTER_EVENTS = {
36
40
  enabled: "all",
@@ -3,7 +3,7 @@ import { DEFAULT_EXPORT_PADDING, isFirefox, MIME_TYPES } from "../constants";
3
3
  import { getNonDeletedElements } from "../element";
4
4
  import { isFrameLikeElement } from "../element/typeChecks";
5
5
  import { t } from "../i18n";
6
- import { elementsOverlappingBBox } from "../../utils/export";
6
+ import { elementsOverlappingBBox } from "../../utils/index";
7
7
  import { isSomeElementSelected, getSelectedElements } from "../scene";
8
8
  import { exportToCanvas, exportToSvg } from "../scene/export";
9
9
  import { cloneJSON } from "../utils";
@@ -91,7 +91,7 @@ export declare const actionLink: {
91
91
  tab?: string | undefined;
92
92
  } | null;
93
93
  openDialog: {
94
- name: "imageExport" | "help" | "jsonExport";
94
+ name: "help" | "imageExport" | "jsonExport";
95
95
  } | {
96
96
  name: "settings";
97
97
  source: "settings" | "tool" | "generation";
@@ -84,7 +84,7 @@ export declare const actionSetEmbeddableAsActiveTool: {
84
84
  tab?: string | undefined;
85
85
  } | null;
86
86
  openDialog: {
87
- name: "imageExport" | "help" | "jsonExport";
87
+ name: "help" | "imageExport" | "jsonExport";
88
88
  } | {
89
89
  name: "settings";
90
90
  source: "settings" | "tool" | "generation";
@@ -194,7 +194,7 @@ export declare class LinearElementEditor {
194
194
  tab?: string | undefined;
195
195
  } | null;
196
196
  openDialog: {
197
- name: "imageExport" | "help" | "jsonExport";
197
+ name: "help" | "imageExport" | "jsonExport";
198
198
  } | {
199
199
  name: "settings";
200
200
  source: "settings" | "tool" | "generation";
@@ -1,20 +1,16 @@
1
+ import { UnsubscribeCallback } from "./types";
1
2
  type Subscriber<T extends any[]> = (...payload: T) => void;
2
3
  export declare class Emitter<T extends any[] = []> {
3
4
  subscribers: Subscriber<T>[];
4
- value: T | undefined;
5
- private updateOnChangeOnly;
6
- constructor(opts?: {
7
- initialState?: T;
8
- updateOnChangeOnly?: boolean;
9
- });
10
5
  /**
11
6
  * Attaches subscriber
12
7
  *
13
8
  * @returns unsubscribe function
14
9
  */
15
- on(...handlers: Subscriber<T>[] | Subscriber<T>[][]): () => void;
10
+ on(...handlers: Subscriber<T>[] | Subscriber<T>[][]): UnsubscribeCallback;
11
+ once(...handlers: Subscriber<T>[] | Subscriber<T>[][]): UnsubscribeCallback;
16
12
  off(...handlers: Subscriber<T>[] | Subscriber<T>[][]): void;
17
- trigger(...payload: T): any[];
18
- destroy(): void;
13
+ trigger(...payload: T): this;
14
+ clear(): void;
19
15
  }
20
16
  export {};
@@ -1,11 +1,5 @@
1
1
  export class Emitter {
2
2
  subscribers = [];
3
- value;
4
- updateOnChangeOnly;
5
- constructor(opts) {
6
- this.updateOnChangeOnly = opts?.updateOnChangeOnly ?? false;
7
- this.value = opts?.initialState;
8
- }
9
3
  /**
10
4
  * Attaches subscriber
11
5
  *
@@ -18,19 +12,25 @@ export class Emitter {
18
12
  this.subscribers.push(..._handlers);
19
13
  return () => this.off(_handlers);
20
14
  }
15
+ once(...handlers) {
16
+ const _handlers = handlers
17
+ .flat()
18
+ .filter((item) => typeof item === "function");
19
+ _handlers.push(() => detach());
20
+ const detach = this.on(..._handlers);
21
+ return detach;
22
+ }
21
23
  off(...handlers) {
22
24
  const _handlers = handlers.flat();
23
25
  this.subscribers = this.subscribers.filter((handler) => !_handlers.includes(handler));
24
26
  }
25
27
  trigger(...payload) {
26
- if (this.updateOnChangeOnly && this.value === payload) {
27
- return [];
28
+ for (const handler of this.subscribers) {
29
+ handler(...payload);
28
30
  }
29
- this.value = payload;
30
- return this.subscribers.map((handler) => handler(...payload));
31
+ return this;
31
32
  }
32
- destroy() {
33
+ clear() {
33
34
  this.subscribers = [];
34
- this.value = undefined;
35
35
  }
36
36
  }
@@ -7,7 +7,7 @@ import { getElementsWithinSelection, getSelectedElements } from "./scene";
7
7
  import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
8
8
  import Scene from "./scene/Scene";
9
9
  import { getElementLineSegments } from "./element/bounds";
10
- import { doLineSegmentsIntersect } from "../utils/export";
10
+ import { doLineSegmentsIntersect } from "../utils/index";
11
11
  import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
12
12
  // --------------------------- Frame State ------------------------------------
13
13
  export const bindElementsToFramesAfterDuplication = (nextElements, oldElements, oldIdToDuplicatedId) => {
@@ -2,7 +2,7 @@ import { atom, useAtom } from "jotai";
2
2
  import { useEffect, useState } from "react";
3
3
  import { COLOR_PALETTE } from "../colors";
4
4
  import { jotaiScope } from "../jotai";
5
- import { exportToSvg } from "../../utils/export";
5
+ import { exportToSvg } from "../../utils/index";
6
6
  export const libraryItemSvgsCache = atom(new Map());
7
7
  const exportLibraryItemToSvg = async (elements) => {
8
8
  return await exportToSvg({
@@ -11,11 +11,13 @@ export declare const Excalidraw: React.MemoExoticComponent<(props: ExcalidrawPro
11
11
  export { getSceneVersion, isInvisiblySmallElement, getNonDeletedElements, } from "./element";
12
12
  export { defaultLang, useI18n, languages } from "./i18n";
13
13
  export { restore, restoreAppState, restoreElements, restoreLibraryItems, } from "./data/restore";
14
- export { exportToCanvas, exportToBlob, exportToSvg, serializeAsJSON, serializeLibraryAsJSON, loadLibraryFromBlob, loadFromBlob, loadSceneOrLibraryFromBlob, getFreeDrawSvgPath, exportToClipboard, mergeLibraryItems, } from "../utils/export";
14
+ export { exportToCanvas, exportToBlob, exportToSvg, exportToClipboard, } from "../utils/index";
15
+ export { serializeAsJSON, serializeLibraryAsJSON } from "./data/json";
16
+ export { loadLibraryFromBlob, loadFromBlob, loadSceneOrLibraryFromBlob, } from "./data/blob";
15
17
  export { isLinearElement } from "./element/typeChecks";
16
18
  export { FONT_FAMILY, THEME, MIME_TYPES } from "./constants";
17
19
  export { mutateElement, newElementWith, bumpVersion, } from "./element/mutateElement";
18
- export { parseLibraryTokensFromUrl, useHandleLibrary } from "./data/library";
20
+ export { parseLibraryTokensFromUrl, useHandleLibrary, mergeLibraryItems, } from "./data/library";
19
21
  export { sceneCoordsToViewportCoords, viewportCoordsToSceneCoords, } from "./utils";
20
22
  export { Sidebar } from "./components/Sidebar/Sidebar";
21
23
  export { Button } from "./components/Button";
@@ -30,5 +32,8 @@ export { TTDDialogTrigger } from "./components/TTDDialog/TTDDialogTrigger";
30
32
  export { normalizeLink } from "./data/url";
31
33
  export { zoomToFitBounds } from "./actions/actionCanvas";
32
34
  export { convertToExcalidrawElements } from "./data/transform";
33
- export { getCommonBounds, getVisibleSceneBounds } from "./element/bounds";
34
- export { elementsOverlappingBBox, isElementInsideBBox, elementPartiallyOverlapsWithOrContainsBBox, } from "../utils/export";
35
+ export { getCommonBounds, getVisibleSceneBounds, getElementBounds, } from "./element/bounds";
36
+ export { getDefaultAppState } from "./appState";
37
+ export { isValueInRange, rotatePoint } from "./math";
38
+ export { isArrowElement, isExcalidrawElement, isFreeDrawElement, isTextElement, } from "./element/typeChecks";
39
+ export { getFreeDrawSvgPath } from "./renderer/renderElement";
@@ -93,11 +93,13 @@ Excalidraw.displayName = "Excalidraw";
93
93
  export { getSceneVersion, isInvisiblySmallElement, getNonDeletedElements, } from "./element";
94
94
  export { defaultLang, useI18n, languages } from "./i18n";
95
95
  export { restore, restoreAppState, restoreElements, restoreLibraryItems, } from "./data/restore";
96
- export { exportToCanvas, exportToBlob, exportToSvg, serializeAsJSON, serializeLibraryAsJSON, loadLibraryFromBlob, loadFromBlob, loadSceneOrLibraryFromBlob, getFreeDrawSvgPath, exportToClipboard, mergeLibraryItems, } from "../utils/export";
96
+ export { exportToCanvas, exportToBlob, exportToSvg, exportToClipboard, } from "../utils/index";
97
+ export { serializeAsJSON, serializeLibraryAsJSON } from "./data/json";
98
+ export { loadLibraryFromBlob, loadFromBlob, loadSceneOrLibraryFromBlob, } from "./data/blob";
97
99
  export { isLinearElement } from "./element/typeChecks";
98
100
  export { FONT_FAMILY, THEME, MIME_TYPES } from "./constants";
99
101
  export { mutateElement, newElementWith, bumpVersion, } from "./element/mutateElement";
100
- export { parseLibraryTokensFromUrl, useHandleLibrary } from "./data/library";
102
+ export { parseLibraryTokensFromUrl, useHandleLibrary, mergeLibraryItems, } from "./data/library";
101
103
  export { sceneCoordsToViewportCoords, viewportCoordsToSceneCoords, } from "./utils";
102
104
  export { Sidebar } from "./components/Sidebar/Sidebar";
103
105
  export { Button } from "./components/Button";
@@ -112,5 +114,8 @@ export { TTDDialogTrigger } from "./components/TTDDialog/TTDDialogTrigger";
112
114
  export { normalizeLink } from "./data/url";
113
115
  export { zoomToFitBounds } from "./actions/actionCanvas";
114
116
  export { convertToExcalidrawElements } from "./data/transform";
115
- export { getCommonBounds, getVisibleSceneBounds } from "./element/bounds";
116
- export { elementsOverlappingBBox, isElementInsideBBox, elementPartiallyOverlapsWithOrContainsBBox, } from "../utils/export";
117
+ export { getCommonBounds, getVisibleSceneBounds, getElementBounds, } from "./element/bounds";
118
+ export { getDefaultAppState } from "./appState";
119
+ export { isValueInRange, rotatePoint } from "./math";
120
+ export { isArrowElement, isExcalidrawElement, isFreeDrawElement, isTextElement, } from "./element/typeChecks";
121
+ export { getFreeDrawSvgPath } from "./renderer/renderElement";
@@ -6,7 +6,7 @@ import { DEFAULT_EXPORT_PADDING, FONT_FAMILY, FRAME_STYLE, SVG_NS, THEME_FILTER,
6
6
  import { getDefaultAppState } from "../appState";
7
7
  import { serializeAsJSON } from "../data/json";
8
8
  import { getInitializedImageElements, updateImageCache, } from "../element/image";
9
- import { elementsOverlappingBBox } from "../../utils/export";
9
+ import { elementsOverlappingBBox } from "../../utils/index";
10
10
  import { getFrameLikeElements, getFrameLikeTitle, getRootElements, } from "../frame";
11
11
  import { newTextElement } from "../element";
12
12
  import { newElementWith } from "../element/mutateElement";