@excalidraw/excalidraw 0.17.1-7441-4e2c539 → 0.17.1-a38e82f

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 (249) hide show
  1. package/CHANGELOG.md +52 -2
  2. package/dist/browser/dev/excalidraw-assets-dev/chunk-5VWQDKDR.js +20279 -0
  3. package/dist/browser/dev/excalidraw-assets-dev/chunk-5VWQDKDR.js.map +7 -0
  4. package/dist/browser/dev/excalidraw-assets-dev/{chunk-2W5GQUR4.js → chunk-IM4WTX2M.js} +12 -6
  5. package/dist/browser/dev/excalidraw-assets-dev/chunk-IM4WTX2M.js.map +7 -0
  6. package/dist/browser/dev/excalidraw-assets-dev/{en-OC6JWP3X.js → en-IOBA4CS2.js} +4 -2
  7. package/dist/browser/dev/excalidraw-assets-dev/image-LK4UNFRZ.css +6 -0
  8. package/dist/browser/dev/excalidraw-assets-dev/image-LK4UNFRZ.css.map +7 -0
  9. package/dist/browser/dev/excalidraw-assets-dev/{image-HYNUJ3XL.js → image-VKDAL6BQ.js} +2 -4
  10. package/dist/browser/dev/excalidraw-assets-dev/roundRect-T5BX56ZF.js +161 -0
  11. package/dist/browser/dev/excalidraw-assets-dev/roundRect-T5BX56ZF.js.map +7 -0
  12. package/dist/browser/dev/index.css +189 -129
  13. package/dist/browser/dev/index.css.map +3 -3
  14. package/dist/browser/dev/index.js +34707 -26
  15. package/dist/browser/dev/index.js.map +4 -4
  16. package/dist/browser/prod/excalidraw-assets/chunk-LIG3S5TN.js +11 -0
  17. package/dist/browser/prod/excalidraw-assets/chunk-N2C5DK3B.js +55 -0
  18. package/dist/browser/prod/excalidraw-assets/en-WFZVQ7I6.js +1 -0
  19. package/dist/browser/prod/excalidraw-assets/image-4AT7LYMR.js +1 -0
  20. package/dist/browser/prod/excalidraw-assets/image-X66R2EM5.css +1 -0
  21. package/dist/browser/prod/excalidraw-assets/roundRect-2ACQK4DA.js +1 -0
  22. package/dist/browser/prod/index.css +1 -1
  23. package/dist/browser/prod/index.js +203 -1
  24. package/dist/{prod/en-RLIAOBCI.json → dev/en-TDNWCAOT.json} +9 -5
  25. package/dist/dev/index.css +189 -129
  26. package/dist/dev/index.css.map +3 -3
  27. package/dist/dev/index.js +38445 -39402
  28. package/dist/dev/index.js.map +4 -4
  29. package/dist/excalidraw/actions/actionAddToLibrary.d.ts +12 -12
  30. package/dist/excalidraw/actions/actionAlign.d.ts +6 -6
  31. package/dist/excalidraw/actions/actionAlign.js +2 -1
  32. package/dist/excalidraw/actions/actionBoundText.d.ts +8 -8
  33. package/dist/excalidraw/actions/actionBoundText.js +8 -8
  34. package/dist/excalidraw/actions/actionCanvas.d.ts +46 -46
  35. package/dist/excalidraw/actions/actionClipboard.d.ts +27 -27
  36. package/dist/excalidraw/actions/actionClipboard.js +9 -2
  37. package/dist/excalidraw/actions/actionDeleteSelected.d.ts +12 -12
  38. package/dist/excalidraw/actions/actionDeleteSelected.js +3 -2
  39. package/dist/excalidraw/actions/actionDistribute.d.ts +2 -2
  40. package/dist/excalidraw/actions/actionDistribute.js +1 -1
  41. package/dist/excalidraw/actions/actionDuplicateSelection.d.ts +1 -1
  42. package/dist/excalidraw/actions/actionDuplicateSelection.js +4 -3
  43. package/dist/excalidraw/actions/actionElementLock.d.ts +8 -8
  44. package/dist/excalidraw/actions/actionExport.d.ts +35 -35
  45. package/dist/excalidraw/actions/actionExport.js +4 -4
  46. package/dist/excalidraw/actions/actionFinalize.d.ts +7 -7
  47. package/dist/excalidraw/actions/actionFinalize.js +7 -6
  48. package/dist/excalidraw/actions/actionFlip.d.ts +2 -2
  49. package/dist/excalidraw/actions/actionFlip.js +11 -11
  50. package/dist/excalidraw/actions/actionFrame.d.ts +13 -13
  51. package/dist/excalidraw/actions/actionFrame.js +1 -1
  52. package/dist/excalidraw/actions/actionGroup.d.ts +8 -8
  53. package/dist/excalidraw/actions/actionGroup.js +3 -2
  54. package/dist/excalidraw/actions/actionLinearEditor.d.ts +4 -4
  55. package/dist/excalidraw/actions/actionLinearEditor.js +1 -1
  56. package/dist/excalidraw/{element/Hyperlink.d.ts → actions/actionLink.d.ts} +28 -50
  57. package/dist/excalidraw/actions/actionLink.js +40 -0
  58. package/dist/excalidraw/actions/actionMenu.d.ts +11 -11
  59. package/dist/excalidraw/actions/actionNavigate.d.ts +8 -8
  60. package/dist/excalidraw/actions/actionNavigate.js +1 -1
  61. package/dist/excalidraw/actions/actionProperties.d.ts +64 -64
  62. package/dist/excalidraw/actions/actionProperties.js +32 -27
  63. package/dist/excalidraw/actions/actionSelectAll.d.ts +4 -4
  64. package/dist/excalidraw/actions/actionSelectAll.js +1 -1
  65. package/dist/excalidraw/actions/actionStyles.d.ts +6 -6
  66. package/dist/excalidraw/actions/actionStyles.js +4 -4
  67. package/dist/excalidraw/actions/actionToggleGridMode.d.ts +4 -4
  68. package/dist/excalidraw/actions/actionToggleObjectsSnapMode.d.ts +4 -4
  69. package/dist/excalidraw/actions/actionToggleStats.d.ts +4 -4
  70. package/dist/excalidraw/actions/actionToggleViewMode.d.ts +4 -4
  71. package/dist/excalidraw/actions/actionToggleZenMode.d.ts +4 -4
  72. package/dist/excalidraw/actions/index.d.ts +1 -1
  73. package/dist/excalidraw/actions/index.js +1 -1
  74. package/dist/excalidraw/actions/manager.js +2 -1
  75. package/dist/excalidraw/align.d.ts +2 -2
  76. package/dist/excalidraw/align.js +2 -2
  77. package/dist/excalidraw/animated-trail.d.ts +33 -0
  78. package/dist/excalidraw/animated-trail.js +96 -0
  79. package/dist/excalidraw/animation-frame-handler.d.ts +16 -0
  80. package/dist/excalidraw/animation-frame-handler.js +55 -0
  81. package/dist/excalidraw/appState.d.ts +1 -1
  82. package/dist/excalidraw/appState.js +1 -3
  83. package/dist/excalidraw/clipboard.js +5 -5
  84. package/dist/excalidraw/components/Actions.d.ts +3 -3
  85. package/dist/excalidraw/components/Actions.js +18 -7
  86. package/dist/excalidraw/components/App.d.ts +32 -17
  87. package/dist/excalidraw/components/App.js +474 -339
  88. package/dist/excalidraw/components/Button.d.ts +1 -1
  89. package/dist/excalidraw/components/FilledButton.d.ts +2 -2
  90. package/dist/excalidraw/components/FilledButton.js +27 -3
  91. package/dist/excalidraw/components/FollowMode/FollowMode.js +1 -1
  92. package/dist/excalidraw/components/ImageExportDialog.d.ts +2 -1
  93. package/dist/excalidraw/components/ImageExportDialog.js +16 -12
  94. package/dist/excalidraw/components/JSONExportDialog.js +1 -1
  95. package/dist/excalidraw/components/{LaserTool/LaserPointerButton.d.ts → LaserPointerButton.d.ts} +1 -1
  96. package/dist/excalidraw/components/{LaserTool/LaserPointerButton.js → LaserPointerButton.js} +2 -2
  97. package/dist/excalidraw/components/LayerUI.js +3 -3
  98. package/dist/excalidraw/components/MobileMenu.js +1 -1
  99. package/dist/excalidraw/components/ProjectName.d.ts +0 -1
  100. package/dist/excalidraw/components/ProjectName.js +1 -1
  101. package/dist/excalidraw/components/SVGLayer.d.ts +8 -0
  102. package/dist/excalidraw/components/SVGLayer.js +20 -0
  103. package/dist/excalidraw/components/ShareableLinkDialog.js +10 -10
  104. package/dist/excalidraw/components/Stack.d.ts +2 -2
  105. package/dist/excalidraw/components/TTDDialog/common.js +10 -1
  106. package/dist/excalidraw/components/TextField.d.ts +5 -2
  107. package/dist/excalidraw/components/TextField.js +6 -3
  108. package/dist/excalidraw/components/Toast.d.ts +3 -2
  109. package/dist/excalidraw/components/Toast.js +2 -2
  110. package/dist/excalidraw/components/ToolButton.js +2 -1
  111. package/dist/excalidraw/components/canvases/InteractiveCanvas.d.ts +2 -2
  112. package/dist/excalidraw/components/canvases/InteractiveCanvas.js +6 -5
  113. package/dist/excalidraw/components/canvases/StaticCanvas.d.ts +4 -3
  114. package/dist/excalidraw/components/canvases/StaticCanvas.js +7 -5
  115. package/dist/excalidraw/components/dropdownMenu/DropdownMenuContent.js +22 -2
  116. package/dist/excalidraw/components/dropdownMenu/common.d.ts +1 -1
  117. package/dist/excalidraw/components/hyperlink/Hyperlink.d.ts +19 -0
  118. package/dist/excalidraw/{element → components/hyperlink}/Hyperlink.js +40 -115
  119. package/dist/excalidraw/components/hyperlink/helpers.d.ts +7 -0
  120. package/dist/excalidraw/components/hyperlink/helpers.js +49 -0
  121. package/dist/excalidraw/components/icons.d.ts +2 -1
  122. package/dist/excalidraw/components/icons.js +2 -1
  123. package/dist/excalidraw/components/live-collaboration/LiveCollaborationTrigger.js +3 -2
  124. package/dist/excalidraw/components/main-menu/DefaultItems.js +5 -2
  125. package/dist/excalidraw/constants.d.ts +8 -0
  126. package/dist/excalidraw/constants.js +10 -0
  127. package/dist/excalidraw/data/blob.js +13 -14
  128. package/dist/excalidraw/data/filesystem.d.ts +1 -1
  129. package/dist/excalidraw/data/index.d.ts +2 -1
  130. package/dist/excalidraw/data/index.js +20 -16
  131. package/dist/excalidraw/data/json.d.ts +1 -1
  132. package/dist/excalidraw/data/json.js +5 -3
  133. package/dist/excalidraw/data/resave.d.ts +1 -1
  134. package/dist/excalidraw/data/resave.js +2 -2
  135. package/dist/excalidraw/data/restore.js +8 -13
  136. package/dist/excalidraw/data/transform.js +13 -9
  137. package/dist/excalidraw/distribute.d.ts +2 -2
  138. package/dist/excalidraw/distribute.js +2 -2
  139. package/dist/excalidraw/element/ElementCanvasButtons.d.ts +3 -2
  140. package/dist/excalidraw/element/ElementCanvasButtons.js +4 -4
  141. package/dist/excalidraw/element/binding.d.ts +9 -9
  142. package/dist/excalidraw/element/binding.js +61 -59
  143. package/dist/excalidraw/element/bounds.d.ts +5 -5
  144. package/dist/excalidraw/element/bounds.js +29 -32
  145. package/dist/excalidraw/element/collision.d.ts +11 -11
  146. package/dist/excalidraw/element/collision.js +49 -46
  147. package/dist/excalidraw/element/containerCache.d.ts +11 -0
  148. package/dist/excalidraw/element/containerCache.js +14 -0
  149. package/dist/excalidraw/element/dragElements.js +10 -19
  150. package/dist/excalidraw/element/embeddable.d.ts +11 -12
  151. package/dist/excalidraw/element/embeddable.js +17 -27
  152. package/dist/excalidraw/element/image.js +1 -2
  153. package/dist/excalidraw/element/index.d.ts +0 -1
  154. package/dist/excalidraw/element/index.js +0 -1
  155. package/dist/excalidraw/element/linearElementEditor.d.ts +35 -35
  156. package/dist/excalidraw/element/linearElementEditor.js +79 -80
  157. package/dist/excalidraw/element/newElement.d.ts +4 -6
  158. package/dist/excalidraw/element/newElement.js +11 -16
  159. package/dist/excalidraw/element/resizeElements.d.ts +6 -6
  160. package/dist/excalidraw/element/resizeElements.js +40 -46
  161. package/dist/excalidraw/element/resizeTest.d.ts +3 -3
  162. package/dist/excalidraw/element/resizeTest.js +4 -4
  163. package/dist/excalidraw/element/sizeHelpers.d.ts +2 -2
  164. package/dist/excalidraw/element/sizeHelpers.js +2 -2
  165. package/dist/excalidraw/element/textElement.d.ts +18 -20
  166. package/dist/excalidraw/element/textElement.js +80 -111
  167. package/dist/excalidraw/element/textWysiwyg.d.ts +1 -6
  168. package/dist/excalidraw/element/textWysiwyg.js +15 -37
  169. package/dist/excalidraw/element/transformHandles.d.ts +4 -4
  170. package/dist/excalidraw/element/transformHandles.js +6 -6
  171. package/dist/excalidraw/element/typeChecks.js +4 -1
  172. package/dist/excalidraw/element/types.d.ts +24 -11
  173. package/dist/excalidraw/emitter.d.ts +5 -9
  174. package/dist/excalidraw/emitter.js +12 -12
  175. package/dist/excalidraw/frame.d.ts +26 -20
  176. package/dist/excalidraw/frame.js +157 -84
  177. package/dist/excalidraw/groups.d.ts +3 -3
  178. package/dist/excalidraw/groups.js +11 -3
  179. package/dist/excalidraw/history.d.ts +1 -1
  180. package/dist/excalidraw/index.d.ts +7 -3
  181. package/dist/excalidraw/index.js +14 -5
  182. package/dist/excalidraw/laser-trails.d.ts +19 -0
  183. package/dist/excalidraw/laser-trails.js +95 -0
  184. package/dist/excalidraw/locales/en.json +9 -5
  185. package/dist/excalidraw/reactUtils.d.ts +14 -0
  186. package/dist/excalidraw/reactUtils.js +45 -0
  187. package/dist/excalidraw/renderer/helpers.d.ts +13 -0
  188. package/dist/excalidraw/renderer/helpers.js +39 -0
  189. package/dist/excalidraw/renderer/interactiveScene.d.ts +20 -0
  190. package/dist/excalidraw/renderer/{renderScene.js → interactiveScene.js} +199 -474
  191. package/dist/excalidraw/renderer/renderElement.d.ts +6 -6
  192. package/dist/excalidraw/renderer/renderElement.js +54 -366
  193. package/dist/excalidraw/renderer/staticScene.d.ts +11 -0
  194. package/dist/excalidraw/renderer/staticScene.js +205 -0
  195. package/dist/excalidraw/renderer/staticSvgScene.d.ts +5 -0
  196. package/dist/excalidraw/renderer/staticSvgScene.js +385 -0
  197. package/dist/excalidraw/scene/Fonts.js +2 -1
  198. package/dist/excalidraw/scene/Renderer.d.ts +1 -1
  199. package/dist/excalidraw/scene/Renderer.js +32 -20
  200. package/dist/excalidraw/scene/Scene.d.ts +10 -9
  201. package/dist/excalidraw/scene/Scene.js +45 -21
  202. package/dist/excalidraw/scene/Shape.d.ts +3 -1
  203. package/dist/excalidraw/scene/Shape.js +7 -5
  204. package/dist/excalidraw/scene/ShapeCache.d.ts +2 -1
  205. package/dist/excalidraw/scene/ShapeCache.js +1 -0
  206. package/dist/excalidraw/scene/comparisons.js +2 -1
  207. package/dist/excalidraw/scene/export.d.ts +3 -0
  208. package/dist/excalidraw/scene/export.js +20 -40
  209. package/dist/excalidraw/scene/index.d.ts +0 -1
  210. package/dist/excalidraw/scene/index.js +0 -1
  211. package/dist/excalidraw/scene/scrollbars.d.ts +1 -1
  212. package/dist/excalidraw/scene/scrollbars.js +1 -1
  213. package/dist/excalidraw/scene/selection.d.ts +5 -5
  214. package/dist/excalidraw/scene/selection.js +16 -14
  215. package/dist/excalidraw/scene/types.d.ts +11 -5
  216. package/dist/excalidraw/snapping.d.ts +7 -7
  217. package/dist/excalidraw/snapping.js +21 -20
  218. package/dist/excalidraw/types.d.ts +11 -12
  219. package/dist/excalidraw/utility-types.d.ts +5 -0
  220. package/dist/excalidraw/utils.d.ts +25 -16
  221. package/dist/excalidraw/utils.js +52 -45
  222. package/dist/{dev/en-RLIAOBCI.json → prod/en-TDNWCAOT.json} +9 -5
  223. package/dist/prod/index.css +1 -1
  224. package/dist/prod/index.js +45 -45
  225. package/dist/utils/export.d.ts +0 -6
  226. package/dist/utils/export.js +0 -6
  227. package/dist/utils/index.d.ts +3 -0
  228. package/dist/utils/index.js +3 -0
  229. package/dist/utils/withinBounds.js +2 -1
  230. package/package.json +4 -4
  231. package/dist/browser/dev/excalidraw-assets-dev/chunk-2W5GQUR4.js.map +0 -7
  232. package/dist/browser/dev/excalidraw-assets-dev/chunk-SUHLFFEF.js +0 -53449
  233. package/dist/browser/dev/excalidraw-assets-dev/chunk-SUHLFFEF.js.map +0 -7
  234. package/dist/browser/dev/excalidraw-assets-dev/image-NOPDRTTM.css +0 -5797
  235. package/dist/browser/dev/excalidraw-assets-dev/image-NOPDRTTM.css.map +0 -7
  236. package/dist/browser/prod/excalidraw-assets/chunk-HE2P7BQ6.js +0 -257
  237. package/dist/browser/prod/excalidraw-assets/chunk-OWLL6VOG.js +0 -11
  238. package/dist/browser/prod/excalidraw-assets/en-ERQOR3OC.js +0 -1
  239. package/dist/browser/prod/excalidraw-assets/image-DZ6B4AID.js +0 -1
  240. package/dist/browser/prod/excalidraw-assets/image-J2QCCYAR.css +0 -1
  241. package/dist/excalidraw/components/LaserTool/LaserPathManager.d.ts +0 -28
  242. package/dist/excalidraw/components/LaserTool/LaserPathManager.js +0 -225
  243. package/dist/excalidraw/components/LaserTool/LaserTool.d.ts +0 -8
  244. package/dist/excalidraw/components/LaserTool/LaserTool.js +0 -15
  245. package/dist/excalidraw/renderer/renderScene.d.ts +0 -25
  246. package/dist/excalidraw/vite.config.d.mts +0 -2
  247. package/dist/excalidraw/vite.config.mjs +0 -13
  248. /package/dist/browser/dev/excalidraw-assets-dev/{en-OC6JWP3X.js.map → en-IOBA4CS2.js.map} +0 -0
  249. /package/dist/browser/dev/excalidraw-assets-dev/{image-HYNUJ3XL.js.map → image-VKDAL6BQ.js.map} +0 -0
@@ -11,11 +11,11 @@ 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_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";
18
- import { dragNewElement, dragSelectedElements, duplicateElement, getCommonBounds, getCursorForResizingElement, getDragOffsetXY, getElementWithTransformHandleType, getNormalizedDimensions, getResizeArrowDirection, getResizeOffsetXY, getLockedLinearCursorAlignSize, getTransformHandleTypeFromCoords, hitTest, isHittingElementBoundingBoxWithoutHittingElement, isInvisiblySmallElement, isNonDeletedElement, isTextElement, newElement, newLinearElement, newTextElement, newImageElement, textWysiwyg, transformElements, updateTextElement, redrawTextBoundingBox, } from "../element";
18
+ import { dragNewElement, dragSelectedElements, duplicateElement, getCommonBounds, getCursorForResizingElement, getDragOffsetXY, getElementWithTransformHandleType, getNormalizedDimensions, getResizeArrowDirection, getResizeOffsetXY, getLockedLinearCursorAlignSize, getTransformHandleTypeFromCoords, hitTest, isHittingElementBoundingBoxWithoutHittingElement, isInvisiblySmallElement, isNonDeletedElement, isTextElement, newElement, newLinearElement, newTextElement, newImageElement, transformElements, updateTextElement, redrawTextBoundingBox, } from "../element";
19
19
  import { bindOrUnbindLinearElement, bindOrUnbindSelectedElements, fixBindingsAfterDeletion, fixBindingsAfterDuplication, getEligibleElementsForBinding, getHoveredElementForBinding, isBindingEnabled, isLinearElementSimpleAndAlreadyBound, maybeBindLinearElement, shouldEnableBindingForPointerEvent, unbindLinearElements, updateBoundElements, } from "../element/binding";
20
20
  import { LinearElementEditor } from "../element/linearElementEditor";
21
21
  import { mutateElement, newElementWith } from "../element/mutateElement";
@@ -28,12 +28,12 @@ import { defaultLang, getLanguage, languages, setLanguage, t } from "../i18n";
28
28
  import { CODES, shouldResizeFromCenter, shouldMaintainAspectRatio, shouldRotateWithDiscreteAngle, isArrowKey, KEYS, } from "../keys";
29
29
  import { isElementInViewport } from "../element/sizeHelpers";
30
30
  import { distance2d, getCornerRadius, getGridPoint, isPathALoop, } from "../math";
31
- import { calculateScrollCenter, getElementsAtPosition, getElementsWithinSelection, getNormalizedZoom, getSelectedElements, hasBackground, isOverScrollBars, isSomeElementSelected, } from "../scene";
31
+ import { calculateScrollCenter, getElementsAtPosition, getElementsWithinSelection, getNormalizedZoom, getSelectedElements, hasBackground, isSomeElementSelected, } from "../scene";
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";
36
- import { createSrcDoc, embeddableURLValidator, extractSrc, getEmbedLink, } from "../element/embeddable";
35
+ import { debounce, distance, getFontString, getNearestScrollableContainer, isInputLike, isToolIcon, isWritableElement, sceneCoordsToViewportCoords, tupleToCoors, viewportCoordsToSceneCoords, wrapEvent, updateObject, updateActiveTool, getShortcutKey, isTransparent, easeToValuesRAF, muteFSAbortError, isTestEnv, easeOut, updateStable, addEventListener, normalizeEOL, getDateTime, } from "../utils";
36
+ import { createSrcDoc, embeddableURLValidator, maybeParseEmbedSrc, getEmbedLink, } from "../element/embeddable";
37
37
  import { ContextMenu, CONTEXT_MENU_SEPARATOR, } from "./ContextMenu";
38
38
  import LayerUI from "./LayerUI";
39
39
  import { Toast } from "./Toast";
@@ -44,12 +44,12 @@ import throttle from "lodash.throttle";
44
44
  import { fileOpen } from "../data/filesystem";
45
45
  import { bindTextToShapeAfterDuplication, getApproxMinLineHeight, getApproxMinLineWidth, getBoundTextElement, getContainerCenter, getContainerElement, getDefaultLineHeight, getLineHeightInPx, getTextBindableContainerAtPosition, isMeasureTextSupported, isValidTextContainer, } from "../element/textElement";
46
46
  import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
47
- import { showHyperlinkTooltip, hideHyperlinkToolip, Hyperlink, isPointHittingLink, isPointHittingLinkIcon, } from "../element/Hyperlink";
47
+ import { showHyperlinkTooltip, hideHyperlinkToolip, Hyperlink, } from "../components/hyperlink/Hyperlink";
48
48
  import { isLocalLink, normalizeLink, toValidURL } from "../data/url";
49
49
  import { shouldShowBoundingBox } from "../element/transformHandles";
50
50
  import { actionUnlockAllElements } from "../actions/actionElementLock";
51
51
  import { Fonts } from "../scene/Fonts";
52
- import { getFrameChildren, isCursorInFrame, bindElementsToFramesAfterDuplication, addElementsToFrame, replaceAllElementsInFrame, removeElementsFromFrame, getElementsInResizingFrame, getElementsInNewFrame, getContainingFrame, elementOverlapsWithFrame, updateFrameMembershipOfSelectedElements, isElementInFrame, getFrameLikeTitle, } from "../frame";
52
+ import { getFrameChildren, isCursorInFrame, bindElementsToFramesAfterDuplication, addElementsToFrame, replaceAllElementsInFrame, removeElementsFromFrame, getElementsInResizingFrame, getElementsInNewFrame, getContainingFrame, elementOverlapsWithFrame, updateFrameMembershipOfSelectedElements, isElementInFrame, getFrameLikeTitle, getElementsOverlappingFrame, filterElementsEligibleAsFrameChildren, } from "../frame";
53
53
  import { excludeElementsInFramesFromSelection, makeNextSelectedElementIds, } from "../scene/selection";
54
54
  import { actionPaste } from "../actions/actionClipboard";
55
55
  import { actionRemoveAllElementsFromFrame, actionSelectAllElementsInFrame, } from "../actions/actionFrame";
@@ -66,18 +66,25 @@ import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
66
66
  import { StaticCanvas, InteractiveCanvas } from "./canvases";
67
67
  import { Renderer } from "../scene/Renderer";
68
68
  import { ShapeCache } from "../scene/ShapeCache";
69
- import { LaserToolOverlay } from "./LaserTool/LaserTool";
70
- import { LaserPathManager } from "./LaserTool/LaserPathManager";
69
+ import { SVGLayer } from "./SVGLayer";
71
70
  import { setEraserCursor, setCursor, resetCursor, setCursorForShape, } from "../cursor";
72
71
  import { Emitter } from "../emitter";
73
72
  import { ElementCanvasButtons } from "../element/ElementCanvasButtons";
74
73
  import { diagramToHTML } from "../data/magic";
75
- import { elementsOverlappingBBox, exportToBlob } from "../../utils/export";
74
+ import { exportToBlob } from "../../utils/export";
76
75
  import { COLOR_PALETTE } from "../colors";
77
76
  import { ElementCanvasButton } from "./MagicButton";
78
77
  import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
79
78
  import { EditorLocalStorage } from "../data/EditorLocalStorage";
80
79
  import FollowMode from "./FollowMode/FollowMode";
80
+ import { AnimationFrameHandler } from "../animation-frame-handler";
81
+ import { AnimatedTrail } from "../animated-trail";
82
+ import { LaserTrails } from "../laser-trails";
83
+ import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
84
+ import { getRenderOpacity } from "../renderer/renderElement";
85
+ import { textWysiwyg } from "../element/textWysiwyg";
86
+ import { isOverScrollBars } from "../scene/scrollbars";
87
+ import { isPointHittingLink, isPointHittingLinkIcon, } from "./hyperlink/helpers";
81
88
  const AppContext = React.createContext(null);
82
89
  const AppPropsContext = React.createContext(null);
83
90
  const deviceContextInitialValue = {
@@ -163,20 +170,52 @@ class App extends React.Component {
163
170
  files = {};
164
171
  imageCache = new Map();
165
172
  iFrameRefs = new Map();
173
+ /**
174
+ * Indicates whether the embeddable's url has been validated for rendering.
175
+ * If value not set, indicates that the validation is pending.
176
+ * Initially or on url change the flag is not reset so that we can guarantee
177
+ * the validation came from a trusted source (the editor).
178
+ **/
179
+ embedsValidationStatus = new Map();
180
+ /** embeds that have been inserted to DOM (as a perf optim, we don't want to
181
+ * insert to DOM before user initially scrolls to them) */
182
+ initializedEmbeds = new Set();
183
+ elementsPendingErasure = new Set();
166
184
  hitLinkElement;
167
185
  lastPointerDownEvent = null;
168
186
  lastPointerUpEvent = null;
187
+ lastPointerMoveEvent = null;
169
188
  lastViewportPosition = { x: 0, y: 0 };
170
- laserPathManager = new LaserPathManager(this);
189
+ animationFrameHandler = new AnimationFrameHandler();
190
+ laserTrails = new LaserTrails(this.animationFrameHandler, this);
191
+ eraserTrail = new AnimatedTrail(this.animationFrameHandler, this, {
192
+ streamline: 0.2,
193
+ size: 5,
194
+ keepHead: true,
195
+ sizeMapping: (c) => {
196
+ const DECAY_TIME = 200;
197
+ const DECAY_LENGTH = 10;
198
+ const t = Math.max(0, 1 - (performance.now() - c.pressure) / DECAY_TIME);
199
+ const l = (DECAY_LENGTH -
200
+ Math.min(DECAY_LENGTH, c.totalLength - c.currentIndex)) /
201
+ DECAY_LENGTH;
202
+ return Math.min(easeOut(l), easeOut(t));
203
+ },
204
+ fill: () => this.state.theme === THEME.LIGHT
205
+ ? "rgba(0, 0, 0, 0.2)"
206
+ : "rgba(255, 255, 255, 0.2)",
207
+ });
171
208
  onChangeEmitter = new Emitter();
172
209
  onPointerDownEmitter = new Emitter();
173
210
  onPointerUpEmitter = new Emitter();
174
211
  onUserFollowEmitter = new Emitter();
175
212
  onScrollChangeEmitter = new Emitter();
213
+ missingPointerEventCleanupEmitter = new Emitter();
214
+ onRemoveEventListenersEmitter = new Emitter();
176
215
  constructor(props) {
177
216
  super(props);
178
217
  const defaultAppState = getDefaultAppState();
179
- const { excalidrawAPI, viewModeEnabled = false, zenModeEnabled = false, gridModeEnabled = false, objectsSnapModeEnabled = false, theme = defaultAppState.theme, name = defaultAppState.name, } = props;
218
+ const { excalidrawAPI, viewModeEnabled = false, zenModeEnabled = false, gridModeEnabled = false, objectsSnapModeEnabled = false, theme = defaultAppState.theme, name = `${t("labels.untitled")}-${getDateTime()}`, } = props;
180
219
  this.state = {
181
220
  ...defaultAppState,
182
221
  theme,
@@ -211,6 +250,7 @@ class App extends React.Component {
211
250
  getSceneElements: this.getSceneElements,
212
251
  getAppState: () => this.state,
213
252
  getFiles: () => this.files,
253
+ getName: this.getName,
214
254
  registerAction: (action) => {
215
255
  this.actionManager.registerAction(action);
216
256
  },
@@ -375,17 +415,20 @@ class App extends React.Component {
375
415
  sceneY >= el.y + el.height / 3 &&
376
416
  sceneY <= el.y + (2 * el.height) / 3);
377
417
  }
418
+ updateEmbedValidationStatus = (element, status) => {
419
+ this.embedsValidationStatus.set(element.id, status);
420
+ ShapeCache.delete(element);
421
+ };
378
422
  updateEmbeddables = () => {
379
423
  const iframeLikes = new Set();
380
424
  let updated = false;
381
425
  this.scene.getNonDeletedElements().filter((element) => {
382
426
  if (isEmbeddableElement(element)) {
383
427
  iframeLikes.add(element.id);
384
- if (element.validated == null) {
428
+ if (!this.embedsValidationStatus.has(element.id)) {
385
429
  updated = true;
386
430
  const validated = embeddableURLValidator(element.link, this.props.validateEmbeddable);
387
- mutateElement(element, { validated }, false);
388
- ShapeCache.delete(element);
431
+ this.updateEmbedValidationStatus(element, validated);
389
432
  }
390
433
  }
391
434
  else if (isIframeElement(element)) {
@@ -409,9 +452,20 @@ class App extends React.Component {
409
452
  const normalizedHeight = this.state.height;
410
453
  const embeddableElements = this.scene
411
454
  .getNonDeletedElements()
412
- .filter((el) => (isEmbeddableElement(el) && !!el.validated) || isIframeElement(el));
455
+ .filter((el) => (isEmbeddableElement(el) &&
456
+ this.embedsValidationStatus.get(el.id) === true) ||
457
+ isIframeElement(el));
413
458
  return (_jsx(_Fragment, { children: embeddableElements.map((el) => {
414
459
  const { x, y } = sceneCoordsToViewportCoords({ sceneX: el.x, sceneY: el.y }, this.state);
460
+ const isVisible = isElementInViewport(el, normalizedWidth, normalizedHeight, this.state, this.scene.getNonDeletedElementsMap());
461
+ const hasBeenInitialized = this.initializedEmbeds.has(el.id);
462
+ if (isVisible && !hasBeenInitialized) {
463
+ this.initializedEmbeds.add(el.id);
464
+ }
465
+ const shouldRender = isVisible || hasBeenInitialized;
466
+ if (!shouldRender) {
467
+ return null;
468
+ }
415
469
  let src;
416
470
  if (isIframeElement(el)) {
417
471
  src = null;
@@ -551,8 +605,6 @@ class App extends React.Component {
551
605
  else {
552
606
  src = getEmbedLink(toValidURL(el.link || ""));
553
607
  }
554
- // console.log({ src });
555
- const isVisible = isElementInViewport(el, normalizedWidth, normalizedHeight, this.state);
556
608
  const isActive = this.state.activeEmbeddable?.element === el &&
557
609
  this.state.activeEmbeddable?.state === "active";
558
610
  const isHovered = this.state.activeEmbeddable?.element === el &&
@@ -564,7 +616,7 @@ class App extends React.Component {
564
616
  ? `translate(${x - this.state.offsetLeft}px, ${y - this.state.offsetTop}px) scale(${scale})`
565
617
  : "none",
566
618
  display: isVisible ? "block" : "none",
567
- opacity: el.opacity / 100,
619
+ opacity: getRenderOpacity(el, getContainingFrame(el, this.scene.getNonDeletedElementsMap()), this.elementsPendingErasure),
568
620
  ["--embeddable-radius"]: `${getCornerRadius(Math.min(el.width, el.height), el)}px`,
569
621
  }, children: _jsxs("div", {
570
622
  //this is a hack that addresses isse with embedded excalidraw.com embeddable
@@ -656,16 +708,14 @@ class App extends React.Component {
656
708
  scrollX: this.state.scrollX,
657
709
  scrollY: this.state.scrollY,
658
710
  zoom: this.state.zoom,
659
- })) {
711
+ }, this.scene.getNonDeletedElementsMap())) {
660
712
  // if frame not visible, don't render its name
661
713
  return null;
662
714
  }
663
715
  const { x: x1, y: y1 } = sceneCoordsToViewportCoords({ sceneX: f.x, sceneY: f.y }, this.state);
664
716
  const FRAME_NAME_EDIT_PADDING = 6;
665
717
  const reset = () => {
666
- if (f.name?.trim() === "") {
667
- mutateElement(f, { name: null });
668
- }
718
+ mutateElement(f, { name: f.name?.trim() || null });
669
719
  this.setState({ editingFrame: null });
670
720
  };
671
721
  let frameNameJSX;
@@ -676,7 +726,7 @@ class App extends React.Component {
676
726
  mutateElement(f, {
677
727
  name: e.target.value,
678
728
  });
679
- }, onBlur: () => reset(), onKeyDown: (event) => {
729
+ }, onFocus: (e) => e.target.select(), onBlur: () => reset(), onKeyDown: (event) => {
680
730
  // for some inexplicable reason, `onBlur` triggered on ESC
681
731
  // does not reset `state.editingFrame` despite being called,
682
732
  // and we need to reset it here as well
@@ -737,11 +787,17 @@ class App extends React.Component {
737
787
  }, children: frameNameJSX }, f.id));
738
788
  });
739
789
  };
790
+ toggleOverscrollBehavior(event) {
791
+ // when pointer inside editor, disable overscroll behavior to prevent
792
+ // panning to trigger history back/forward on MacOS Chrome
793
+ document.documentElement.style.overscrollBehaviorX =
794
+ event.type === "pointerenter" ? "none" : "auto";
795
+ }
740
796
  render() {
741
797
  const selectedElements = this.scene.getSelectedElements(this.state);
742
798
  const { renderTopRightUI, renderCustomStats } = this.props;
743
799
  const versionNonce = this.scene.getVersionNonce();
744
- const { canvasElements, visibleElements } = this.renderer.getRenderableElements({
800
+ const { elementsMap, visibleElements } = this.renderer.getRenderableElements({
745
801
  versionNonce,
746
802
  zoom: this.state.zoom,
747
803
  offsetLeft: this.state.offsetLeft,
@@ -753,6 +809,7 @@ class App extends React.Component {
753
809
  editingElement: this.state.editingElement,
754
810
  pendingImageElementId: this.state.pendingImageElementId,
755
811
  });
812
+ const allElementsMap = this.scene.getNonDeletedElementsMap();
756
813
  const shouldBlockPointerEvents = !(this.state.editingElement && isLinearElement(this.state.editingElement)) &&
757
814
  (this.state.selectionElement ||
758
815
  this.state.draggingElement ||
@@ -770,18 +827,18 @@ class App extends React.Component {
770
827
  ["--ui-pointerEvents"]: shouldBlockPointerEvents
771
828
  ? POINTER_EVENTS.disabled
772
829
  : POINTER_EVENTS.enabled,
773
- }, ref: this.excalidrawContainerRef, onDrop: this.handleAppOnDrop, tabIndex: 0, onKeyDown: this.props.handleKeyboardGlobally ? undefined : this.onKeyDown, children: _jsx(AppContext.Provider, { value: this, children: _jsx(AppPropsContext.Provider, { value: this.props, children: _jsx(ExcalidrawContainerContext.Provider, { value: this.excalidrawContainerValue, children: _jsx(DeviceContext.Provider, { value: this.device, children: _jsx(ExcalidrawSetAppStateContext.Provider, { value: this.setAppState, children: _jsx(ExcalidrawAppStateContext.Provider, { value: this.state, children: _jsxs(ExcalidrawElementsContext.Provider, { value: this.scene.getNonDeletedElements(), children: [_jsxs(ExcalidrawActionManagerContext.Provider, { value: this.actionManager, children: [_jsx(LayerUI, { canvas: this.canvas, appState: this.state, files: this.files, setAppState: this.setAppState, actionManager: this.actionManager, elements: this.scene.getNonDeletedElements(), onLockToggle: this.toggleLock, onPenModeToggle: this.togglePenMode, onHandToolToggle: this.onHandToolToggle, langCode: getLanguage().code, renderTopRightUI: renderTopRightUI, renderCustomStats: renderCustomStats, showExitZenModeBtn: typeof this.props?.zenModeEnabled === "undefined" &&
830
+ }, ref: this.excalidrawContainerRef, onDrop: this.handleAppOnDrop, tabIndex: 0, onKeyDown: this.props.handleKeyboardGlobally ? undefined : this.onKeyDown, onPointerEnter: this.toggleOverscrollBehavior, onPointerLeave: this.toggleOverscrollBehavior, children: _jsx(AppContext.Provider, { value: this, children: _jsx(AppPropsContext.Provider, { value: this.props, children: _jsx(ExcalidrawContainerContext.Provider, { value: this.excalidrawContainerValue, children: _jsx(DeviceContext.Provider, { value: this.device, children: _jsx(ExcalidrawSetAppStateContext.Provider, { value: this.setAppState, children: _jsx(ExcalidrawAppStateContext.Provider, { value: this.state, children: _jsxs(ExcalidrawElementsContext.Provider, { value: this.scene.getNonDeletedElements(), children: [_jsxs(ExcalidrawActionManagerContext.Provider, { value: this.actionManager, children: [_jsx(LayerUI, { canvas: this.canvas, appState: this.state, files: this.files, setAppState: this.setAppState, actionManager: this.actionManager, elements: this.scene.getNonDeletedElements(), onLockToggle: this.toggleLock, onPenModeToggle: this.togglePenMode, onHandToolToggle: this.onHandToolToggle, langCode: getLanguage().code, renderTopRightUI: renderTopRightUI, renderCustomStats: renderCustomStats, showExitZenModeBtn: typeof this.props?.zenModeEnabled === "undefined" &&
774
831
  this.state.zenModeEnabled, UIOptions: this.props.UIOptions, onExportImage: this.onExportImage, renderWelcomeScreen: !this.state.isLoading &&
775
832
  this.state.showWelcomeScreen &&
776
833
  this.state.activeTool.type === "selection" &&
777
834
  !this.state.zenModeEnabled &&
778
- !this.scene.getElementsIncludingDeleted().length, app: this, isCollaborating: this.props.isCollaborating, openAIKey: this.OPENAI_KEY, isOpenAIKeyPersisted: this.OPENAI_KEY_IS_PERSISTED, onOpenAIAPIKeyChange: this.onOpenAIKeyChange, onMagicSettingsConfirm: this.onMagicSettingsConfirm, children: this.props.children }), _jsx("div", { className: "excalidraw-textEditorContainer" }), _jsx("div", { className: "excalidraw-contextMenuContainer" }), _jsx("div", { className: "excalidraw-eye-dropper-container" }), _jsx(LaserToolOverlay, { manager: this.laserPathManager }), selectedElements.length === 1 &&
779
- this.state.showHyperlinkPopup && (_jsx(Hyperlink, { element: firstSelectedElement, setAppState: this.setAppState, onLinkOpen: this.props.onLinkOpen, setToast: this.setToast }, firstSelectedElement.id)), this.props.aiEnabled !== false &&
835
+ !this.scene.getElementsIncludingDeleted().length, app: this, isCollaborating: this.props.isCollaborating, openAIKey: this.OPENAI_KEY, isOpenAIKeyPersisted: this.OPENAI_KEY_IS_PERSISTED, onOpenAIAPIKeyChange: this.onOpenAIKeyChange, onMagicSettingsConfirm: this.onMagicSettingsConfirm, children: this.props.children }), _jsx("div", { className: "excalidraw-textEditorContainer" }), _jsx("div", { className: "excalidraw-contextMenuContainer" }), _jsx("div", { className: "excalidraw-eye-dropper-container" }), _jsx(SVGLayer, { trails: [this.laserTrails, this.eraserTrail] }), selectedElements.length === 1 &&
836
+ this.state.showHyperlinkPopup && (_jsx(Hyperlink, { element: firstSelectedElement, elementsMap: allElementsMap, setAppState: this.setAppState, onLinkOpen: this.props.onLinkOpen, setToast: this.setToast, updateEmbedValidationStatus: this.updateEmbedValidationStatus }, firstSelectedElement.id)), this.props.aiEnabled !== false &&
780
837
  selectedElements.length === 1 &&
781
- isMagicFrameElement(firstSelectedElement) && (_jsx(ElementCanvasButtons, { element: firstSelectedElement, children: _jsx(ElementCanvasButton, { title: t("labels.convertToCode"), icon: MagicIcon, checked: false, onChange: () => this.onMagicFrameGenerate(firstSelectedElement, "button") }) })), selectedElements.length === 1 &&
838
+ isMagicFrameElement(firstSelectedElement) && (_jsx(ElementCanvasButtons, { element: firstSelectedElement, elementsMap: elementsMap, children: _jsx(ElementCanvasButton, { title: t("labels.convertToCode"), icon: MagicIcon, checked: false, onChange: () => this.onMagicFrameGenerate(firstSelectedElement, "button") }) })), selectedElements.length === 1 &&
782
839
  isIframeElement(firstSelectedElement) &&
783
840
  firstSelectedElement.customData?.generationData
784
- ?.status === "done" && (_jsxs(ElementCanvasButtons, { element: firstSelectedElement, children: [_jsx(ElementCanvasButton, { title: t("labels.copySource"), icon: copyIcon, checked: false, onChange: () => this.onIframeSrcCopy(firstSelectedElement) }), _jsx(ElementCanvasButton, { title: "Enter fullscreen", icon: fullscreenIcon, checked: false, onChange: () => {
841
+ ?.status === "done" && (_jsxs(ElementCanvasButtons, { element: firstSelectedElement, elementsMap: elementsMap, children: [_jsx(ElementCanvasButton, { title: t("labels.copySource"), icon: copyIcon, checked: false, onChange: () => this.onIframeSrcCopy(firstSelectedElement) }), _jsx(ElementCanvasButton, { title: "Enter fullscreen", icon: fullscreenIcon, checked: false, onChange: () => {
785
842
  const iframe = this.getHTMLIFrameElement(firstSelectedElement);
786
843
  if (iframe) {
787
844
  try {
@@ -810,12 +867,14 @@ class App extends React.Component {
810
867
  this.focusContainer();
811
868
  callback?.();
812
869
  });
813
- } })), _jsx(StaticCanvas, { canvas: this.canvas, rc: this.rc, elements: canvasElements, visibleElements: visibleElements, versionNonce: versionNonce, selectionNonce: this.state.selectionElement?.versionNonce, scale: window.devicePixelRatio, appState: this.state, renderConfig: {
870
+ } })), _jsx(StaticCanvas, { canvas: this.canvas, rc: this.rc, elementsMap: elementsMap, allElementsMap: allElementsMap, visibleElements: visibleElements, versionNonce: versionNonce, selectionNonce: this.state.selectionElement?.versionNonce, scale: window.devicePixelRatio, appState: this.state, renderConfig: {
814
871
  imageCache: this.imageCache,
815
872
  isExporting: false,
816
873
  renderGrid: true,
817
874
  canvasBackgroundColor: this.state.viewBackgroundColor,
818
- } }), _jsx(InteractiveCanvas, { containerRef: this.excalidrawContainerRef, canvas: this.interactiveCanvas, elements: canvasElements, visibleElements: visibleElements, selectedElements: selectedElements, versionNonce: versionNonce, selectionNonce: this.state.selectionElement?.versionNonce, scale: window.devicePixelRatio, appState: this.state, renderInteractiveSceneCallback: this.renderInteractiveSceneCallback, handleCanvasRef: this.handleInteractiveCanvasRef, onContextMenu: this.handleCanvasContextMenu, onPointerMove: this.handleCanvasPointerMove, onPointerUp: this.handleCanvasPointerUp, onPointerCancel: this.removePointer, onTouchMove: this.handleTouchMove, onPointerDown: this.handleCanvasPointerDown, onDoubleClick: this.handleCanvasDoubleClick }), this.state.userToFollow && (_jsx(FollowMode, { width: this.state.width, height: this.state.height, userToFollow: this.state.userToFollow, onDisconnect: this.maybeUnfollowRemoteUser })), this.renderFrameNames()] }), this.renderEmbeddables()] }) }) }) }) }) }) }) }));
875
+ embedsValidationStatus: this.embedsValidationStatus,
876
+ elementsPendingErasure: this.elementsPendingErasure,
877
+ } }), _jsx(InteractiveCanvas, { containerRef: this.excalidrawContainerRef, canvas: this.interactiveCanvas, elementsMap: elementsMap, visibleElements: visibleElements, selectedElements: selectedElements, versionNonce: versionNonce, selectionNonce: this.state.selectionElement?.versionNonce, scale: window.devicePixelRatio, appState: this.state, renderInteractiveSceneCallback: this.renderInteractiveSceneCallback, handleCanvasRef: this.handleInteractiveCanvasRef, onContextMenu: this.handleCanvasContextMenu, onPointerMove: this.handleCanvasPointerMove, onPointerUp: this.handleCanvasPointerUp, onPointerCancel: this.removePointer, onTouchMove: this.handleTouchMove, onPointerDown: this.handleCanvasPointerDown, onDoubleClick: this.handleCanvasDoubleClick }), this.state.userToFollow && (_jsx(FollowMode, { width: this.state.width, height: this.state.height, userToFollow: this.state.userToFollow, onDisconnect: this.maybeUnfollowRemoteUser })), this.renderFrameNames()] }), this.renderEmbeddables()] }) }) }) }) }) }) }) }));
819
878
  }
820
879
  focusContainer = () => {
821
880
  this.excalidrawContainerRef.current?.focus();
@@ -837,7 +896,7 @@ class App extends React.Component {
837
896
  trackEvent("export", type, "ui");
838
897
  const fileHandle = await exportCanvas(type, elements, this.state, this.files, {
839
898
  exportBackground: this.state.exportBackground,
840
- name: this.state.name,
899
+ name: this.getName(),
841
900
  viewBackgroundColor: this.state.viewBackgroundColor,
842
901
  exportingFrame: opts.exportingFrame,
843
902
  })
@@ -890,11 +949,7 @@ class App extends React.Component {
890
949
  trackEvent("ai", "generate (missing key)", "d2c");
891
950
  return;
892
951
  }
893
- const magicFrameChildren = elementsOverlappingBBox({
894
- elements: this.scene.getNonDeletedElements(),
895
- bounds: magicFrame,
896
- type: "overlap",
897
- }).filter((el) => !isMagicFrameElement(el));
952
+ const magicFrameChildren = getElementsOverlappingFrame(this.scene.getNonDeletedElements(), magicFrame).filter((el) => !isMagicFrameElement(el));
898
953
  if (!magicFrameChildren.length) {
899
954
  if (source === "button") {
900
955
  this.setState({ errorMessage: "Cannot generate from an empty frame" });
@@ -1149,7 +1204,7 @@ class App extends React.Component {
1149
1204
  let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
1150
1205
  let gridSize = actionResult?.appState?.gridSize || null;
1151
1206
  const theme = actionResult?.appState?.theme || this.props.theme || THEME.LIGHT;
1152
- let name = actionResult?.appState?.name ?? this.state.name;
1207
+ const name = actionResult?.appState?.name ?? this.state.name;
1153
1208
  const errorMessage = actionResult?.appState?.errorMessage ?? this.state.errorMessage;
1154
1209
  if (typeof this.props.viewModeEnabled !== "undefined") {
1155
1210
  viewModeEnabled = this.props.viewModeEnabled;
@@ -1160,9 +1215,6 @@ class App extends React.Component {
1160
1215
  if (typeof this.props.gridModeEnabled !== "undefined") {
1161
1216
  gridSize = this.props.gridModeEnabled ? GRID_SIZE : null;
1162
1217
  }
1163
- if (typeof this.props.name !== "undefined") {
1164
- name = this.props.name;
1165
- }
1166
1218
  editingElement =
1167
1219
  editingElement || actionResult.appState?.editingElement || null;
1168
1220
  if (editingElement?.isDeleted) {
@@ -1413,14 +1465,16 @@ class App extends React.Component {
1413
1465
  this.removeEventListeners();
1414
1466
  this.scene.destroy();
1415
1467
  this.library.destroy();
1416
- this.laserPathManager.destroy();
1417
- this.onChangeEmitter.destroy();
1468
+ this.laserTrails.stop();
1469
+ this.eraserTrail.stop();
1470
+ this.onChangeEmitter.clear();
1418
1471
  ShapeCache.destroy();
1419
1472
  SnapCache.destroy();
1420
1473
  clearTimeout(touchTimeout);
1421
1474
  isSomeElementSelected.clearCache();
1422
1475
  selectGroupsForSelectedElements.clearCache();
1423
1476
  touchTimeout = 0;
1477
+ document.documentElement.style.overscrollBehaviorX = "";
1424
1478
  }
1425
1479
  onResize = withBatchedUpdates(() => {
1426
1480
  this.scene
@@ -1433,27 +1487,6 @@ class App extends React.Component {
1433
1487
  }
1434
1488
  this.setState({});
1435
1489
  });
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
1490
  /** generally invoked only if fullscreen was invoked programmatically */
1458
1491
  onFullscreenChange = () => {
1459
1492
  if (
@@ -1465,46 +1498,45 @@ class App extends React.Component {
1465
1498
  });
1466
1499
  }
1467
1500
  };
1501
+ removeEventListeners() {
1502
+ this.onRemoveEventListenersEmitter.trigger();
1503
+ }
1468
1504
  addEventListeners() {
1505
+ // remove first as we can add event listeners multiple times
1469
1506
  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 });
1507
+ // -------------------------------------------------------------------------
1508
+ // view+edit mode listeners
1509
+ // -------------------------------------------------------------------------
1474
1510
  if (this.props.handleKeyboardGlobally) {
1475
- document.addEventListener(EVENT.KEYDOWN, this.onKeyDown, false);
1511
+ this.onRemoveEventListenersEmitter.once(addEventListener(document, EVENT.KEYDOWN, this.onKeyDown, false));
1476
1512
  }
1477
- document.addEventListener(EVENT.KEYUP, this.onKeyUp, { passive: true });
1478
- document.addEventListener(EVENT.MOUSE_MOVE, this.updateCurrentCursorPosition);
1513
+ 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
1514
+ addEventListener(document, EVENT.COPY, this.onCopy), addEventListener(document, EVENT.KEYUP, this.onKeyUp, { passive: true }), addEventListener(document, EVENT.MOUSE_MOVE, this.updateCurrentCursorPosition),
1479
1515
  // rerender text elements on font load to fix #637 && #1553
1480
- document.fonts?.addEventListener?.("loadingdone", (event) => {
1516
+ addEventListener(document.fonts, "loadingdone", (event) => {
1481
1517
  const loadedFontFaces = event.fontfaces;
1482
1518
  this.fonts.onFontsLoaded(loadedFontFaces);
1483
- });
1519
+ }),
1484
1520
  // 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);
1521
+ 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, () => {
1522
+ this.maybeCleanupAfterMissingPointerUp(null);
1523
+ }));
1488
1524
  if (this.state.viewModeEnabled) {
1489
1525
  return;
1490
1526
  }
1491
- document.addEventListener(EVENT.FULLSCREENCHANGE, this.onFullscreenChange);
1492
- document.addEventListener(EVENT.PASTE, this.pasteFromClipboard);
1493
- document.addEventListener(EVENT.CUT, this.onCut);
1527
+ // -------------------------------------------------------------------------
1528
+ // edit-mode listeners only
1529
+ // -------------------------------------------------------------------------
1530
+ 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
1531
  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);
1532
+ this.onRemoveEventListenersEmitter.once(addEventListener(getNearestScrollableContainer(this.excalidrawContainerRef.current), EVENT.SCROLL, this.onScroll));
1533
+ }
1503
1534
  }
1504
1535
  componentDidUpdate(prevProps, prevState) {
1505
1536
  this.updateEmbeddables();
1506
- if (!this.state.showWelcomeScreen &&
1507
- !this.scene.getElementsIncludingDeleted().length) {
1537
+ const elements = this.scene.getElementsIncludingDeleted();
1538
+ const elementsMap = this.scene.getNonDeletedElementsMap();
1539
+ if (!this.state.showWelcomeScreen && !elements.length) {
1508
1540
  this.setState({ showWelcomeScreen: true });
1509
1541
  }
1510
1542
  if (prevProps.UIOptions.dockedSidebarBreakpoint !==
@@ -1555,6 +1587,9 @@ class App extends React.Component {
1555
1587
  if (prevProps.langCode !== this.props.langCode) {
1556
1588
  this.updateLanguage();
1557
1589
  }
1590
+ if (isEraserActive(prevState) && !isEraserActive(this.state)) {
1591
+ this.eraserTrail.endPath();
1592
+ }
1558
1593
  if (prevProps.viewModeEnabled !== this.props.viewModeEnabled) {
1559
1594
  this.setState({ viewModeEnabled: !!this.props.viewModeEnabled });
1560
1595
  }
@@ -1573,11 +1608,6 @@ class App extends React.Component {
1573
1608
  gridSize: this.props.gridModeEnabled ? GRID_SIZE : null,
1574
1609
  });
1575
1610
  }
1576
- if (this.props.name && prevProps.name !== this.props.name) {
1577
- this.setState({
1578
- name: this.props.name,
1579
- });
1580
- }
1581
1611
  this.excalidrawContainerRef.current?.classList.toggle("theme--dark", this.state.theme === "dark");
1582
1612
  if (this.state.editingLinearElement &&
1583
1613
  !this.state.selectedElementIds[this.state.editingLinearElement.elementId]) {
@@ -1606,19 +1636,19 @@ class App extends React.Component {
1606
1636
  multiElement != null &&
1607
1637
  isBindingEnabled(this.state) &&
1608
1638
  isBindingElement(multiElement, false)) {
1609
- maybeBindLinearElement(multiElement, this.state, this.scene, tupleToCoors(LinearElementEditor.getPointAtIndexGlobalCoordinates(multiElement, -1)));
1639
+ maybeBindLinearElement(multiElement, this.state, this.scene, tupleToCoors(LinearElementEditor.getPointAtIndexGlobalCoordinates(multiElement, -1, elementsMap)), elementsMap);
1610
1640
  }
1611
- this.history.record(this.state, this.scene.getElementsIncludingDeleted());
1641
+ this.history.record(this.state, elements);
1612
1642
  // Do not notify consumers if we're still loading the scene. Among other
1613
1643
  // potential issues, this fixes a case where the tab isn't focused during
1614
1644
  // init, which would trigger onChange with empty elements, which would then
1615
1645
  // override whatever is in localStorage currently.
1616
1646
  if (!this.state.isLoading) {
1617
- this.props.onChange?.(this.scene.getElementsIncludingDeleted(), this.state, this.files);
1618
- this.onChangeEmitter.trigger(this.scene.getElementsIncludingDeleted(), this.state, this.files);
1647
+ this.props.onChange?.(elements, this.state, this.files);
1648
+ this.onChangeEmitter.trigger(elements, this.state, this.files);
1619
1649
  }
1620
1650
  }
1621
- renderInteractiveSceneCallback = ({ atLeastOneVisibleElement, scrollBars, elements, }) => {
1651
+ renderInteractiveSceneCallback = ({ atLeastOneVisibleElement, scrollBars, elementsMap, }) => {
1622
1652
  if (scrollBars) {
1623
1653
  currentScrollBars = scrollBars;
1624
1654
  }
@@ -1626,7 +1656,7 @@ class App extends React.Component {
1626
1656
  // hide when editing text
1627
1657
  isTextElement(this.state.editingElement)
1628
1658
  ? false
1629
- : !atLeastOneVisibleElement && elements.length > 0;
1659
+ : !atLeastOneVisibleElement && elementsMap.size > 0;
1630
1660
  if (this.state.scrolledOutside !== scrolledOutside) {
1631
1661
  this.setState({ scrolledOutside });
1632
1662
  }
@@ -1664,9 +1694,8 @@ class App extends React.Component {
1664
1694
  didTapTwice = false;
1665
1695
  }
1666
1696
  onTouchStart = (event) => {
1667
- // fix for Apple Pencil Scribble
1668
- // On Android, preventing the event would disable contextMenu on tap-hold
1669
- if (!isAndroid) {
1697
+ // fix for Apple Pencil Scribble (do not prevent for other devices)
1698
+ if (isIOS) {
1670
1699
  event.preventDefault();
1671
1700
  }
1672
1701
  if (!didTapTwice) {
@@ -1687,9 +1716,6 @@ class App extends React.Component {
1687
1716
  didTapTwice = false;
1688
1717
  clearTimeout(tappedTwiceTimer);
1689
1718
  }
1690
- if (isAndroid) {
1691
- event.preventDefault();
1692
- }
1693
1719
  if (event.touches.length === 2) {
1694
1720
  this.setState({
1695
1721
  selectedElementIds: makeNextSelectedElementIds({}, this.state),
@@ -1799,18 +1825,40 @@ class App extends React.Component {
1799
1825
  });
1800
1826
  }
1801
1827
  else if (data.text) {
1802
- const maybeUrl = extractSrc(data.text);
1803
- if (!isPlainPaste &&
1804
- embeddableURLValidator(maybeUrl, this.props.validateEmbeddable) &&
1805
- (/^(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(maybeUrl) ||
1806
- getEmbedLink(maybeUrl)?.type === "video")) {
1807
- const embeddable = this.insertEmbeddableElement({
1808
- sceneX,
1809
- sceneY,
1810
- link: normalizeLink(maybeUrl),
1811
- });
1812
- if (embeddable) {
1813
- this.setState({ selectedElementIds: { [embeddable.id]: true } });
1828
+ const nonEmptyLines = normalizeEOL(data.text)
1829
+ .split(/\n+/)
1830
+ .map((s) => s.trim())
1831
+ .filter(Boolean);
1832
+ const embbeddableUrls = nonEmptyLines
1833
+ .map((str) => maybeParseEmbedSrc(str))
1834
+ .filter((string) => {
1835
+ return (embeddableURLValidator(string, this.props.validateEmbeddable) &&
1836
+ (/^(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(string) ||
1837
+ getEmbedLink(string)?.type === "video"));
1838
+ });
1839
+ if (!IS_PLAIN_PASTE &&
1840
+ embbeddableUrls.length > 0 &&
1841
+ // if there were non-embeddable text (lines) mixed in with embeddable
1842
+ // urls, ignore and paste as text
1843
+ embbeddableUrls.length === nonEmptyLines.length) {
1844
+ const embeddables = [];
1845
+ for (const url of embbeddableUrls) {
1846
+ const prevEmbeddable = embeddables[embeddables.length - 1];
1847
+ const embeddable = this.insertEmbeddableElement({
1848
+ sceneX: prevEmbeddable
1849
+ ? prevEmbeddable.x + prevEmbeddable.width + 20
1850
+ : sceneX,
1851
+ sceneY,
1852
+ link: normalizeLink(url),
1853
+ });
1854
+ if (embeddable) {
1855
+ embeddables.push(embeddable);
1856
+ }
1857
+ }
1858
+ if (embeddables.length) {
1859
+ this.setState({
1860
+ selectedElementIds: Object.fromEntries(embeddables.map((embeddable) => [embeddable.id, true])),
1861
+ });
1814
1862
  }
1815
1863
  return;
1816
1864
  }
@@ -1846,15 +1894,20 @@ class App extends React.Component {
1846
1894
  }), {
1847
1895
  randomizeSeed: !opts.retainSeed,
1848
1896
  });
1849
- const nextElements = [
1897
+ const allElements = [
1850
1898
  ...this.scene.getElementsIncludingDeleted(),
1851
1899
  ...newElements,
1852
1900
  ];
1853
- this.scene.replaceAllElements(nextElements);
1901
+ const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y });
1902
+ if (topLayerFrame) {
1903
+ const eligibleElements = filterElementsEligibleAsFrameChildren(newElements, topLayerFrame);
1904
+ addElementsToFrame(allElements, eligibleElements, topLayerFrame);
1905
+ }
1906
+ this.scene.replaceAllElements(allElements);
1854
1907
  newElements.forEach((newElement) => {
1855
1908
  if (isTextElement(newElement) && isBoundToContainer(newElement)) {
1856
- const container = getContainerElement(newElement);
1857
- redrawTextBoundingBox(newElement, container);
1909
+ const container = getContainerElement(newElement, this.scene.getElementsMapIncludingDeleted());
1910
+ redrawTextBoundingBox(newElement, container, this.scene.getElementsMapIncludingDeleted());
1858
1911
  }
1859
1912
  });
1860
1913
  if (opts.files) {
@@ -1909,7 +1962,14 @@ class App extends React.Component {
1909
1962
  return { file: await ImageURLToFile(url) };
1910
1963
  }
1911
1964
  catch (error) {
1912
- return { errorMessage: error.message };
1965
+ let errorMessage = error.message;
1966
+ if (error.cause === "FETCH_ERROR") {
1967
+ errorMessage = t("errors.failedToFetchImage");
1968
+ }
1969
+ else if (error.cause === "UNSUPPORTED") {
1970
+ errorMessage = t("errors.unsupportedFileType");
1971
+ }
1972
+ return { errorMessage };
1913
1973
  }
1914
1974
  }));
1915
1975
  let y = sceneY;
@@ -2377,7 +2437,7 @@ class App extends React.Component {
2377
2437
  x: element.x + offsetX,
2378
2438
  y: element.y + offsetY,
2379
2439
  });
2380
- updateBoundElements(element, {
2440
+ updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
2381
2441
  simultaneouslyUpdated: selectedElements,
2382
2442
  });
2383
2443
  });
@@ -2395,7 +2455,7 @@ class App extends React.Component {
2395
2455
  selectedElements[0].id) {
2396
2456
  this.history.resumeRecording();
2397
2457
  this.setState({
2398
- editingLinearElement: new LinearElementEditor(selectedElement, this.scene),
2458
+ editingLinearElement: new LinearElementEditor(selectedElement),
2399
2459
  });
2400
2460
  }
2401
2461
  }
@@ -2406,7 +2466,7 @@ class App extends React.Component {
2406
2466
  if (!isTextElement(selectedElement)) {
2407
2467
  container = selectedElement;
2408
2468
  }
2409
- const midPoint = getContainerCenter(selectedElement, this.state);
2469
+ const midPoint = getContainerCenter(selectedElement, this.state, this.scene.getNonDeletedElementsMap());
2410
2470
  const sceneX = midPoint.x;
2411
2471
  const sceneY = midPoint.y;
2412
2472
  this.startTextEditing({
@@ -2520,9 +2580,10 @@ class App extends React.Component {
2520
2580
  }
2521
2581
  if (isArrowKey(event.key)) {
2522
2582
  const selectedElements = this.scene.getSelectedElements(this.state);
2583
+ const elementsMap = this.scene.getNonDeletedElementsMap();
2523
2584
  isBindingEnabled(this.state)
2524
- ? bindOrUnbindSelectedElements(selectedElements)
2525
- : unbindLinearElements(selectedElements);
2585
+ ? bindOrUnbindSelectedElements(selectedElements, this.scene.getNonDeletedElements(), elementsMap)
2586
+ : unbindLinearElements(selectedElements, elementsMap);
2526
2587
  this.setState({ suggestedBindings: [] });
2527
2588
  }
2528
2589
  });
@@ -2599,6 +2660,11 @@ class App extends React.Component {
2599
2660
  // touchscreen
2600
2661
  return gesture.pointers.size >= 2;
2601
2662
  };
2663
+ getName = () => {
2664
+ return (this.state.name ||
2665
+ this.props.name ||
2666
+ `${t("labels.untitled")}-${getDateTime()}`);
2667
+ };
2602
2668
  // fires only on Safari
2603
2669
  onGestureStart = withBatchedUpdates((event) => {
2604
2670
  event.preventDefault();
@@ -2651,11 +2717,13 @@ class App extends React.Component {
2651
2717
  gesture.initialScale = null;
2652
2718
  });
2653
2719
  handleTextWysiwyg(element, { isExistingElement = false, }) {
2720
+ const elementsMap = this.scene.getElementsMapIncludingDeleted();
2654
2721
  const updateElement = (text, originalText, isDeleted) => {
2655
2722
  this.scene.replaceAllElements([
2723
+ // Not sure why we include deleted elements as well hence using deleted elements map
2656
2724
  ...this.scene.getElementsIncludingDeleted().map((_element) => {
2657
2725
  if (_element.id === element.id && isTextElement(_element)) {
2658
- return updateTextElement(_element, {
2726
+ return updateTextElement(_element, getContainerElement(_element, elementsMap), elementsMap, {
2659
2727
  text,
2660
2728
  isDeleted,
2661
2729
  originalText,
@@ -2681,7 +2749,7 @@ class App extends React.Component {
2681
2749
  onChange: withBatchedUpdates((text) => {
2682
2750
  updateElement(text, text, false);
2683
2751
  if (isNonDeletedElement(element)) {
2684
- updateBoundElements(element);
2752
+ updateBoundElements(element, elementsMap);
2685
2753
  }
2686
2754
  }),
2687
2755
  onSubmit: withBatchedUpdates(({ text, viaKeyboard, originalText }) => {
@@ -2757,7 +2825,7 @@ class App extends React.Component {
2757
2825
  const elementWithHighestZIndex = allHitElements[allHitElements.length - 1];
2758
2826
  // If we're hitting element with highest z-index only on its bounding box
2759
2827
  // while also hitting other element figure, the latter should be considered.
2760
- return isHittingElementBoundingBoxWithoutHittingElement(elementWithHighestZIndex, this.state, this.frameNameBoundsCache, x, y)
2828
+ return isHittingElementBoundingBoxWithoutHittingElement(elementWithHighestZIndex, this.state, this.frameNameBoundsCache, x, y, this.scene.getNonDeletedElementsMap())
2761
2829
  ? allHitElements[allHitElements.length - 2]
2762
2830
  : elementWithHighestZIndex;
2763
2831
  }
@@ -2774,13 +2842,14 @@ class App extends React.Component {
2774
2842
  .filter((element) => (includeLockedElements || !element.locked) &&
2775
2843
  (includeBoundTextElement ||
2776
2844
  !(isTextElement(element) && element.containerId)));
2777
- return getElementsAtPosition(elements, (element) => hitTest(element, this.state, this.frameNameBoundsCache, x, y)).filter((element) => {
2845
+ const elementsMap = this.scene.getNonDeletedElementsMap();
2846
+ return getElementsAtPosition(elements, (element) => hitTest(element, this.state, this.frameNameBoundsCache, x, y, elementsMap)).filter((element) => {
2778
2847
  // hitting a frame's element from outside the frame is not considered a hit
2779
- const containingFrame = getContainingFrame(element);
2848
+ const containingFrame = getContainingFrame(element, elementsMap);
2780
2849
  return containingFrame &&
2781
2850
  this.state.frameRendering.enabled &&
2782
2851
  this.state.frameRendering.clip
2783
- ? isCursorInFrame({ x, y }, containingFrame)
2852
+ ? isCursorInFrame({ x, y }, containingFrame, elementsMap)
2784
2853
  : true;
2785
2854
  });
2786
2855
  }
@@ -2789,7 +2858,7 @@ class App extends React.Component {
2789
2858
  let parentCenterPosition = insertAtParentCenter &&
2790
2859
  this.getTextWysiwygSnappedToCenterPosition(sceneX, sceneY, this.state, container);
2791
2860
  if (container && parentCenterPosition) {
2792
- const boundTextElementToContainer = getBoundTextElement(container);
2861
+ const boundTextElementToContainer = getBoundTextElement(container, this.scene.getNonDeletedElementsMap());
2793
2862
  if (!boundTextElementToContainer) {
2794
2863
  shouldBindToContainer = true;
2795
2864
  }
@@ -2801,7 +2870,7 @@ class App extends React.Component {
2801
2870
  existingTextElement = selectedElements[0];
2802
2871
  }
2803
2872
  else if (container) {
2804
- existingTextElement = getBoundTextElement(selectedElements[0]);
2873
+ existingTextElement = getBoundTextElement(selectedElements[0], this.scene.getNonDeletedElementsMap());
2805
2874
  }
2806
2875
  else {
2807
2876
  existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
@@ -2909,7 +2978,7 @@ class App extends React.Component {
2909
2978
  this.state.editingLinearElement.elementId !== selectedElements[0].id)) {
2910
2979
  this.history.resumeRecording();
2911
2980
  this.setState({
2912
- editingLinearElement: new LinearElementEditor(selectedElements[0], this.scene),
2981
+ editingLinearElement: new LinearElementEditor(selectedElements[0]),
2913
2982
  });
2914
2983
  return;
2915
2984
  }
@@ -2945,12 +3014,12 @@ class App extends React.Component {
2945
3014
  });
2946
3015
  return;
2947
3016
  }
2948
- const container = getTextBindableContainerAtPosition(this.scene.getNonDeletedElements(), this.state, sceneX, sceneY);
3017
+ const container = getTextBindableContainerAtPosition(this.scene.getNonDeletedElements(), this.state, sceneX, sceneY, this.scene.getNonDeletedElementsMap());
2949
3018
  if (container) {
2950
3019
  if (hasBoundTextElement(container) ||
2951
3020
  !isTransparent(container.backgroundColor) ||
2952
- isHittingElementNotConsideringBoundingBox(container, this.state, this.frameNameBoundsCache, [sceneX, sceneY])) {
2953
- const midPoint = getContainerCenter(container, this.state);
3021
+ isHittingElementNotConsideringBoundingBox(container, this.state, this.frameNameBoundsCache, [sceneX, sceneY], this.scene.getNonDeletedElementsMap())) {
3022
+ const midPoint = getContainerCenter(container, this.state, this.scene.getNonDeletedElementsMap());
2954
3023
  sceneX = midPoint.x;
2955
3024
  sceneY = midPoint.y;
2956
3025
  }
@@ -2974,7 +3043,7 @@ class App extends React.Component {
2974
3043
  }
2975
3044
  return (element.link &&
2976
3045
  index <= hitElementIndex &&
2977
- isPointHittingLink(element, this.state, [scenePointer.x, scenePointer.y], this.device.editor.isMobile));
3046
+ isPointHittingLink(element, this.scene.getNonDeletedElementsMap(), this.state, [scenePointer.x, scenePointer.y], this.device.editor.isMobile));
2978
3047
  });
2979
3048
  };
2980
3049
  redirectToLink = (event, isTouchScreen) => {
@@ -2986,9 +3055,10 @@ class App extends React.Component {
2986
3055
  return;
2987
3056
  }
2988
3057
  const lastPointerDownCoords = viewportCoordsToSceneCoords(this.lastPointerDownEvent, this.state);
2989
- const lastPointerDownHittingLinkIcon = isPointHittingLink(this.hitLinkElement, this.state, [lastPointerDownCoords.x, lastPointerDownCoords.y], this.device.editor.isMobile);
3058
+ const elementsMap = this.scene.getNonDeletedElementsMap();
3059
+ const lastPointerDownHittingLinkIcon = isPointHittingLink(this.hitLinkElement, elementsMap, this.state, [lastPointerDownCoords.x, lastPointerDownCoords.y], this.device.editor.isMobile);
2990
3060
  const lastPointerUpCoords = viewportCoordsToSceneCoords(this.lastPointerUpEvent, this.state);
2991
- const lastPointerUpHittingLinkIcon = isPointHittingLink(this.hitLinkElement, this.state, [lastPointerUpCoords.x, lastPointerUpCoords.y], this.device.editor.isMobile);
3061
+ const lastPointerUpHittingLinkIcon = isPointHittingLink(this.hitLinkElement, elementsMap, this.state, [lastPointerUpCoords.x, lastPointerUpCoords.y], this.device.editor.isMobile);
2992
3062
  if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
2993
3063
  let url = this.hitLinkElement.link;
2994
3064
  if (url) {
@@ -3014,13 +3084,15 @@ class App extends React.Component {
3014
3084
  }
3015
3085
  };
3016
3086
  getTopLayerFrameAtSceneCoords = (sceneCoords) => {
3087
+ const elementsMap = this.scene.getNonDeletedElementsMap();
3017
3088
  const frames = this.scene
3018
3089
  .getNonDeletedFramesLikes()
3019
- .filter((frame) => isCursorInFrame(sceneCoords, frame));
3090
+ .filter((frame) => isCursorInFrame(sceneCoords, frame, elementsMap));
3020
3091
  return frames.length ? frames[frames.length - 1] : null;
3021
3092
  };
3022
3093
  handleCanvasPointerMove = (event) => {
3023
3094
  this.savePointer(event.clientX, event.clientY, this.state.cursorButton);
3095
+ this.lastPointerMoveEvent = event.nativeEvent;
3024
3096
  if (gesture.pointers.has(event.pointerId)) {
3025
3097
  gesture.pointers.set(event.pointerId, {
3026
3098
  x: event.clientX,
@@ -3087,7 +3159,7 @@ class App extends React.Component {
3087
3159
  const { originOffset, snapLines } = getSnapLinesAtPointer(this.scene.getNonDeletedElements(), this.state, {
3088
3160
  x: scenePointerX,
3089
3161
  y: scenePointerY,
3090
- }, event);
3162
+ }, event, this.scene.getNonDeletedElementsMap());
3091
3163
  this.setState((prevState) => {
3092
3164
  const nextSnapLines = updateStable(prevState.snapLines, snapLines);
3093
3165
  const nextOriginOffset = prevState.originSnapOffset
@@ -3115,7 +3187,7 @@ class App extends React.Component {
3115
3187
  }
3116
3188
  if (this.state.editingLinearElement &&
3117
3189
  !this.state.editingLinearElement.isDragging) {
3118
- const editingLinearElement = LinearElementEditor.handlePointerMove(event, scenePointerX, scenePointerY, this.state);
3190
+ const editingLinearElement = LinearElementEditor.handlePointerMove(event, scenePointerX, scenePointerY, this.state, this.scene.getNonDeletedElementsMap());
3119
3191
  if (editingLinearElement &&
3120
3192
  editingLinearElement !== this.state.editingLinearElement) {
3121
3193
  // Since we are reading from previous state which is not possible with
@@ -3217,7 +3289,7 @@ class App extends React.Component {
3217
3289
  if (selectedElements.length === 1 &&
3218
3290
  !isOverScrollBar &&
3219
3291
  !this.state.editingLinearElement) {
3220
- const elementWithTransformHandleType = getElementWithTransformHandleType(elements, this.state, scenePointerX, scenePointerY, this.state.zoom, event.pointerType);
3292
+ const elementWithTransformHandleType = getElementWithTransformHandleType(elements, this.state, scenePointerX, scenePointerY, this.state.zoom, event.pointerType, this.scene.getNonDeletedElementsMap());
3221
3293
  if (elementWithTransformHandleType &&
3222
3294
  elementWithTransformHandleType.transformHandleType) {
3223
3295
  setCursor(this.interactiveCanvas, getCursorForResizingElement(elementWithTransformHandleType));
@@ -3241,7 +3313,7 @@ class App extends React.Component {
3241
3313
  if (this.hitLinkElement &&
3242
3314
  !this.state.selectedElementIds[this.hitLinkElement.id]) {
3243
3315
  setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
3244
- showHyperlinkTooltip(this.hitLinkElement, this.state);
3316
+ showHyperlinkTooltip(this.hitLinkElement, this.state, this.scene.getNonDeletedElementsMap());
3245
3317
  }
3246
3318
  else {
3247
3319
  hideHyperlinkToolip();
@@ -3292,34 +3364,49 @@ class App extends React.Component {
3292
3364
  }
3293
3365
  };
3294
3366
  handleEraser = (event, pointerDownState, scenePointer) => {
3295
- const updateElementIds = (elements) => {
3296
- elements.forEach((element) => {
3367
+ this.eraserTrail.addPointToPath(scenePointer.x, scenePointer.y);
3368
+ let didChange = false;
3369
+ const processedGroups = new Set();
3370
+ const nonDeletedElements = this.scene.getNonDeletedElements();
3371
+ const processElements = (elements) => {
3372
+ for (const element of elements) {
3297
3373
  if (element.locked) {
3298
3374
  return;
3299
3375
  }
3300
- idsToUpdate.push(element.id);
3301
3376
  if (event.altKey) {
3302
- if (pointerDownState.elementIdsToErase[element.id] &&
3303
- pointerDownState.elementIdsToErase[element.id].erase) {
3304
- pointerDownState.elementIdsToErase[element.id].erase = false;
3377
+ if (this.elementsPendingErasure.delete(element.id)) {
3378
+ didChange = true;
3305
3379
  }
3306
3380
  }
3307
- else if (!pointerDownState.elementIdsToErase[element.id]) {
3308
- pointerDownState.elementIdsToErase[element.id] = {
3309
- erase: true,
3310
- opacity: element.opacity,
3311
- };
3381
+ else if (!this.elementsPendingErasure.has(element.id)) {
3382
+ didChange = true;
3383
+ this.elementsPendingErasure.add(element.id);
3312
3384
  }
3313
- });
3385
+ // (un)erase groups atomically
3386
+ if (didChange && element.groupIds?.length) {
3387
+ const shallowestGroupId = element.groupIds.at(-1);
3388
+ if (!processedGroups.has(shallowestGroupId)) {
3389
+ processedGroups.add(shallowestGroupId);
3390
+ const elems = getElementsInGroup(nonDeletedElements, shallowestGroupId);
3391
+ for (const elem of elems) {
3392
+ if (event.altKey) {
3393
+ this.elementsPendingErasure.delete(elem.id);
3394
+ }
3395
+ else {
3396
+ this.elementsPendingErasure.add(elem.id);
3397
+ }
3398
+ }
3399
+ }
3400
+ }
3401
+ }
3314
3402
  };
3315
- const idsToUpdate = [];
3316
3403
  const distance = distance2d(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y, scenePointer.x, scenePointer.y);
3317
3404
  const threshold = 10 / this.state.zoom.value;
3318
3405
  const point = { ...pointerDownState.lastCoords };
3319
3406
  let samplingInterval = 0;
3320
3407
  while (samplingInterval <= distance) {
3321
3408
  const hitElements = this.getElementsAtPosition(point.x, point.y);
3322
- updateElementIds(hitElements);
3409
+ processElements(hitElements);
3323
3410
  // Exit since we reached current point
3324
3411
  if (samplingInterval === distance) {
3325
3412
  break;
@@ -3332,48 +3419,45 @@ class App extends React.Component {
3332
3419
  point.x = nextX;
3333
3420
  point.y = nextY;
3334
3421
  }
3335
- const elements = this.scene.getElementsIncludingDeleted().map((ele) => {
3336
- const id = isBoundToContainer(ele) && idsToUpdate.includes(ele.containerId)
3337
- ? ele.containerId
3338
- : ele.id;
3339
- if (idsToUpdate.includes(id)) {
3340
- if (event.altKey) {
3341
- if (pointerDownState.elementIdsToErase[id] &&
3342
- pointerDownState.elementIdsToErase[id].erase === false) {
3343
- return newElementWith(ele, {
3344
- opacity: pointerDownState.elementIdsToErase[id].opacity,
3345
- });
3422
+ pointerDownState.lastCoords.x = scenePointer.x;
3423
+ pointerDownState.lastCoords.y = scenePointer.y;
3424
+ if (didChange) {
3425
+ for (const element of this.scene.getNonDeletedElements()) {
3426
+ if (isBoundToContainer(element) &&
3427
+ (this.elementsPendingErasure.has(element.id) ||
3428
+ this.elementsPendingErasure.has(element.containerId))) {
3429
+ if (event.altKey) {
3430
+ this.elementsPendingErasure.delete(element.id);
3431
+ this.elementsPendingErasure.delete(element.containerId);
3432
+ }
3433
+ else {
3434
+ this.elementsPendingErasure.add(element.id);
3435
+ this.elementsPendingErasure.add(element.containerId);
3346
3436
  }
3347
- }
3348
- else {
3349
- return newElementWith(ele, {
3350
- opacity: ELEMENT_READY_TO_ERASE_OPACITY,
3351
- });
3352
3437
  }
3353
3438
  }
3354
- return ele;
3355
- });
3356
- this.scene.replaceAllElements(elements);
3357
- pointerDownState.lastCoords.x = scenePointer.x;
3358
- pointerDownState.lastCoords.y = scenePointer.y;
3439
+ this.elementsPendingErasure = new Set(this.elementsPendingErasure);
3440
+ this.onSceneUpdated();
3441
+ }
3359
3442
  };
3360
3443
  // set touch moving for mobile context menu
3361
3444
  handleTouchMove = (event) => {
3362
3445
  invalidateContextMenu = true;
3363
3446
  };
3364
3447
  handleHoverSelectedLinearElement(linearElementEditor, scenePointerX, scenePointerY) {
3365
- const element = LinearElementEditor.getElement(linearElementEditor.elementId);
3366
- const boundTextElement = getBoundTextElement(element);
3448
+ const elementsMap = this.scene.getNonDeletedElementsMap();
3449
+ const element = LinearElementEditor.getElement(linearElementEditor.elementId, elementsMap);
3450
+ const boundTextElement = getBoundTextElement(element, elementsMap);
3367
3451
  if (!element) {
3368
3452
  return;
3369
3453
  }
3370
3454
  if (this.state.selectedLinearElement) {
3371
3455
  let hoverPointIndex = -1;
3372
3456
  let segmentMidPointHoveredCoords = null;
3373
- if (isHittingElementNotConsideringBoundingBox(element, this.state, this.frameNameBoundsCache, [scenePointerX, scenePointerY])) {
3374
- hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(element, this.state.zoom, scenePointerX, scenePointerY);
3457
+ if (isHittingElementNotConsideringBoundingBox(element, this.state, this.frameNameBoundsCache, [scenePointerX, scenePointerY], elementsMap)) {
3458
+ hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(element, elementsMap, this.state.zoom, scenePointerX, scenePointerY);
3375
3459
  segmentMidPointHoveredCoords =
3376
- LinearElementEditor.getSegmentMidpointHitCoords(linearElementEditor, { x: scenePointerX, y: scenePointerY }, this.state);
3460
+ LinearElementEditor.getSegmentMidpointHitCoords(linearElementEditor, { x: scenePointerX, y: scenePointerY }, this.state, this.scene.getNonDeletedElementsMap());
3377
3461
  if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
3378
3462
  setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
3379
3463
  }
@@ -3382,11 +3466,11 @@ class App extends React.Component {
3382
3466
  }
3383
3467
  }
3384
3468
  else if (shouldShowBoundingBox([element], this.state) &&
3385
- isHittingElementBoundingBoxWithoutHittingElement(element, this.state, this.frameNameBoundsCache, scenePointerX, scenePointerY)) {
3469
+ isHittingElementBoundingBoxWithoutHittingElement(element, this.state, this.frameNameBoundsCache, scenePointerX, scenePointerY, elementsMap)) {
3386
3470
  setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
3387
3471
  }
3388
3472
  else if (boundTextElement &&
3389
- hitTest(boundTextElement, this.state, this.frameNameBoundsCache, scenePointerX, scenePointerY)) {
3473
+ hitTest(boundTextElement, this.state, this.frameNameBoundsCache, scenePointerX, scenePointerY, this.scene.getNonDeletedElementsMap())) {
3390
3474
  setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
3391
3475
  }
3392
3476
  if (this.state.selectedLinearElement.hoverPointIndex !== hoverPointIndex) {
@@ -3411,6 +3495,7 @@ class App extends React.Component {
3411
3495
  }
3412
3496
  }
3413
3497
  handleCanvasPointerDown = (event) => {
3498
+ this.maybeCleanupAfterMissingPointerUp(event.nativeEvent);
3414
3499
  this.maybeUnfollowRemoteUser();
3415
3500
  // since contextMenu options are potentially evaluated on each render,
3416
3501
  // and an contextMenu action may depend on selection state, we must
@@ -3463,7 +3548,6 @@ class App extends React.Component {
3463
3548
  selection.removeAllRanges();
3464
3549
  }
3465
3550
  this.maybeOpenContextMenuAfterPointerDownOnTouchDevices(event);
3466
- this.maybeCleanupAfterMissingPointerUp(event);
3467
3551
  //fires only once, if pen is detected, penMode is enabled
3468
3552
  //the user can disable this by toggling the penMode button
3469
3553
  if (!this.state.penDetected && event.pointerType === "pen") {
@@ -3493,9 +3577,47 @@ class App extends React.Component {
3493
3577
  cursorButton: "down",
3494
3578
  });
3495
3579
  this.savePointer(event.clientX, event.clientY, "down");
3580
+ if (event.button === POINTER_BUTTON.ERASER &&
3581
+ this.state.activeTool.type !== TOOL_TYPE.eraser) {
3582
+ this.setState({
3583
+ activeTool: updateActiveTool(this.state, {
3584
+ type: TOOL_TYPE.eraser,
3585
+ lastActiveToolBeforeEraser: this.state.activeTool,
3586
+ }),
3587
+ }, () => {
3588
+ this.handleCanvasPointerDown(event);
3589
+ const onPointerUp = () => {
3590
+ unsubPointerUp();
3591
+ unsubCleanup?.();
3592
+ if (isEraserActive(this.state)) {
3593
+ this.setState({
3594
+ activeTool: updateActiveTool(this.state, {
3595
+ ...(this.state.activeTool.lastActiveTool || {
3596
+ type: TOOL_TYPE.selection,
3597
+ }),
3598
+ lastActiveToolBeforeEraser: null,
3599
+ }),
3600
+ });
3601
+ }
3602
+ };
3603
+ const unsubPointerUp = addEventListener(window, EVENT.POINTER_UP, onPointerUp, {
3604
+ once: true,
3605
+ });
3606
+ let unsubCleanup;
3607
+ // subscribe inside rAF lest it'd be triggered on the same pointerdown
3608
+ // if we start erasing while coming from blurred document since
3609
+ // we cleanup pointer events on focus
3610
+ requestAnimationFrame(() => {
3611
+ unsubCleanup =
3612
+ this.missingPointerEventCleanupEmitter.once(onPointerUp);
3613
+ });
3614
+ });
3615
+ return;
3616
+ }
3496
3617
  // only handle left mouse button or touch
3497
3618
  if (event.button !== POINTER_BUTTON.MAIN &&
3498
- event.button !== POINTER_BUTTON.TOUCH) {
3619
+ event.button !== POINTER_BUTTON.TOUCH &&
3620
+ event.button !== POINTER_BUTTON.ERASER) {
3499
3621
  return;
3500
3622
  }
3501
3623
  // don't select while panning
@@ -3566,7 +3688,7 @@ class App extends React.Component {
3566
3688
  this.createFrameElementOnPointerDown(pointerDownState, this.state.activeTool.type);
3567
3689
  }
3568
3690
  else if (this.state.activeTool.type === "laser") {
3569
- this.laserPathManager.startPath(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y);
3691
+ this.laserTrails.startPath(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y);
3570
3692
  }
3571
3693
  else if (this.state.activeTool.type !== "eraser" &&
3572
3694
  this.state.activeTool.type !== "hand") {
@@ -3574,11 +3696,14 @@ class App extends React.Component {
3574
3696
  }
3575
3697
  this.props?.onPointerDown?.(this.state.activeTool, pointerDownState);
3576
3698
  this.onPointerDownEmitter.trigger(this.state.activeTool, pointerDownState, event);
3699
+ if (this.state.activeTool.type === "eraser") {
3700
+ this.eraserTrail.startPath(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y);
3701
+ }
3577
3702
  const onPointerMove = this.onPointerMoveFromPointerDownHandler(pointerDownState);
3578
3703
  const onPointerUp = this.onPointerUpFromPointerDownHandler(pointerDownState);
3579
3704
  const onKeyDown = this.onKeyDownFromPointerDownHandler(pointerDownState);
3580
3705
  const onKeyUp = this.onKeyUpFromPointerDownHandler(pointerDownState);
3581
- lastPointerUp = onPointerUp;
3706
+ this.missingPointerEventCleanupEmitter.once((_event) => onPointerUp(_event || event.nativeEvent));
3582
3707
  if (!this.state.viewModeEnabled || this.state.activeTool.type === "laser") {
3583
3708
  window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
3584
3709
  window.addEventListener(EVENT.POINTER_UP, onPointerUp);
@@ -3611,10 +3736,7 @@ class App extends React.Component {
3611
3736
  !this.state.selectedElementIds[this.hitLinkElement.id]) {
3612
3737
  if (clicklength < 300 &&
3613
3738
  isIframeLikeElement(this.hitLinkElement) &&
3614
- !isPointHittingLinkIcon(this.hitLinkElement, this.state, [
3615
- scenePointer.x,
3616
- scenePointer.y,
3617
- ])) {
3739
+ !isPointHittingLinkIcon(this.hitLinkElement, this.scene.getNonDeletedElementsMap(), this.state, [scenePointer.x, scenePointer.y])) {
3618
3740
  this.handleEmbeddableCenterClick(this.hitLinkElement);
3619
3741
  }
3620
3742
  else {
@@ -3655,14 +3777,15 @@ class App extends React.Component {
3655
3777
  touchTimeout = 0;
3656
3778
  invalidateContextMenu = false;
3657
3779
  };
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
- }
3780
+ /**
3781
+ * pointerup may not fire in certian cases (user tabs away...), so in order
3782
+ * to properly cleanup pointerdown state, we need to fire any hanging
3783
+ * pointerup handlers manually
3784
+ */
3785
+ maybeCleanupAfterMissingPointerUp = (event) => {
3786
+ lastPointerUp?.();
3787
+ this.missingPointerEventCleanupEmitter.trigger(event).clear();
3788
+ };
3666
3789
  // Returns whether the event is a panning
3667
3790
  handleCanvasPanUsingWheelOrSpaceDrag = (event) => {
3668
3791
  if (!(gesture.pointers.size <= 1 &&
@@ -3676,7 +3799,9 @@ class App extends React.Component {
3676
3799
  isPanning = true;
3677
3800
  event.preventDefault();
3678
3801
  let nextPastePrevented = false;
3679
- const isLinux = /Linux/.test(window.navigator.platform);
3802
+ const isLinux = typeof window === undefined
3803
+ ? false
3804
+ : /Linux/.test(window.navigator.platform);
3680
3805
  setCursor(this.interactiveCanvas, CURSOR_TYPE.GRABBING);
3681
3806
  let { clientX: lastX, clientY: lastY } = event;
3682
3807
  const onPointerMove = withBatchedUpdatesThrottled((event) => {
@@ -3799,7 +3924,6 @@ class App extends React.Component {
3799
3924
  boxSelection: {
3800
3925
  hasOccurred: false,
3801
3926
  },
3802
- elementIdsToErase: {},
3803
3927
  };
3804
3928
  }
3805
3929
  // Returns whether the event is a dragging a scrollbar
@@ -3818,9 +3942,9 @@ class App extends React.Component {
3818
3942
  this.handlePointerMoveOverScrollbars(event, pointerDownState);
3819
3943
  });
3820
3944
  const onPointerUp = withBatchedUpdates(() => {
3945
+ lastPointerUp = null;
3821
3946
  isDraggingScrollBar = false;
3822
3947
  setCursorForShape(this.interactiveCanvas, this.state);
3823
- lastPointerUp = null;
3824
3948
  this.setState({
3825
3949
  cursorButton: "up",
3826
3950
  });
@@ -3850,9 +3974,10 @@ class App extends React.Component {
3850
3974
  handleSelectionOnPointerDown = (event, pointerDownState) => {
3851
3975
  if (this.state.activeTool.type === "selection") {
3852
3976
  const elements = this.scene.getNonDeletedElements();
3977
+ const elementsMap = this.scene.getNonDeletedElementsMap();
3853
3978
  const selectedElements = this.scene.getSelectedElements(this.state);
3854
3979
  if (selectedElements.length === 1 && !this.state.editingLinearElement) {
3855
- const elementWithTransformHandleType = getElementWithTransformHandleType(elements, this.state, pointerDownState.origin.x, pointerDownState.origin.y, this.state.zoom, event.pointerType);
3980
+ const elementWithTransformHandleType = getElementWithTransformHandleType(elements, this.state, pointerDownState.origin.x, pointerDownState.origin.y, this.state.zoom, event.pointerType, this.scene.getNonDeletedElementsMap());
3856
3981
  if (elementWithTransformHandleType != null) {
3857
3982
  this.setState({
3858
3983
  resizingElement: elementWithTransformHandleType.element,
@@ -3866,7 +3991,7 @@ class App extends React.Component {
3866
3991
  }
3867
3992
  if (pointerDownState.resize.handleType) {
3868
3993
  pointerDownState.resize.isResizing = true;
3869
- pointerDownState.resize.offset = tupleToCoors(getResizeOffsetXY(pointerDownState.resize.handleType, selectedElements, pointerDownState.origin.x, pointerDownState.origin.y));
3994
+ pointerDownState.resize.offset = tupleToCoors(getResizeOffsetXY(pointerDownState.resize.handleType, selectedElements, elementsMap, pointerDownState.origin.x, pointerDownState.origin.y));
3870
3995
  if (selectedElements.length === 1 &&
3871
3996
  isLinearElement(selectedElements[0]) &&
3872
3997
  selectedElements[0].points.length === 2) {
@@ -3876,7 +4001,7 @@ class App extends React.Component {
3876
4001
  else {
3877
4002
  if (this.state.selectedLinearElement) {
3878
4003
  const linearElementEditor = this.state.editingLinearElement || this.state.selectedLinearElement;
3879
- const ret = LinearElementEditor.handlePointerDown(event, this.state, this.history, pointerDownState.origin, linearElementEditor);
4004
+ const ret = LinearElementEditor.handlePointerDown(event, this.state, this.history, pointerDownState.origin, linearElementEditor, this.scene.getNonDeletedElements(), elementsMap);
3880
4005
  if (ret.hitElement) {
3881
4006
  pointerDownState.hit.element = ret.hitElement;
3882
4007
  }
@@ -4057,7 +4182,7 @@ class App extends React.Component {
4057
4182
  includeBoundTextElement: true,
4058
4183
  });
4059
4184
  // FIXME
4060
- let container = getTextBindableContainerAtPosition(this.scene.getNonDeletedElements(), this.state, sceneX, sceneY);
4185
+ let container = getTextBindableContainerAtPosition(this.scene.getNonDeletedElements(), this.state, sceneX, sceneY, this.scene.getNonDeletedElementsMap());
4061
4186
  if (hasBoundTextElement(element)) {
4062
4187
  container = element;
4063
4188
  sceneX = element.x + element.width / 2;
@@ -4115,7 +4240,7 @@ class App extends React.Component {
4115
4240
  points: [[0, 0]],
4116
4241
  pressures,
4117
4242
  });
4118
- const boundElement = getHoveredElementForBinding(pointerDownState.origin, this.scene);
4243
+ const boundElement = getHoveredElementForBinding(pointerDownState.origin, this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap());
4119
4244
  this.scene.addNewElement(element);
4120
4245
  this.setState({
4121
4246
  draggingElement: element,
@@ -4159,8 +4284,11 @@ class App extends React.Component {
4159
4284
  if (!embedLink) {
4160
4285
  return;
4161
4286
  }
4162
- if (embedLink.warning) {
4163
- this.setToast({ message: embedLink.warning, closable: true });
4287
+ if (embedLink.error instanceof URIError) {
4288
+ this.setToast({
4289
+ message: t("toast.unrecognizedLinkFormat"),
4290
+ closable: true,
4291
+ });
4164
4292
  }
4165
4293
  const element = newEmbeddableElement({
4166
4294
  type: "embeddable",
@@ -4178,7 +4306,6 @@ class App extends React.Component {
4178
4306
  width: embedLink.intrinsicSize.w,
4179
4307
  height: embedLink.intrinsicSize.h,
4180
4308
  link,
4181
- validated: null,
4182
4309
  });
4183
4310
  this.scene.replaceAllElements([
4184
4311
  ...this.scene.getElementsIncludingDeleted(),
@@ -4291,7 +4418,7 @@ class App extends React.Component {
4291
4418
  mutateElement(element, {
4292
4419
  points: [...element.points, [0, 0]],
4293
4420
  });
4294
- const boundElement = getHoveredElementForBinding(pointerDownState.origin, this.scene);
4421
+ const boundElement = getHoveredElementForBinding(pointerDownState.origin, this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap());
4295
4422
  this.scene.addNewElement(element);
4296
4423
  this.setState({
4297
4424
  draggingElement: element,
@@ -4336,7 +4463,6 @@ class App extends React.Component {
4336
4463
  if (elementType === "embeddable") {
4337
4464
  element = newEmbeddableElement({
4338
4465
  type: "embeddable",
4339
- validated: null,
4340
4466
  ...baseElementAttributes,
4341
4467
  });
4342
4468
  }
@@ -4392,7 +4518,7 @@ class App extends React.Component {
4392
4518
  selectedElements,
4393
4519
  }) &&
4394
4520
  (recomputeAnyways || !SnapCache.getReferenceSnapPoints())) {
4395
- SnapCache.setReferenceSnapPoints(getReferenceSnapPoints(this.scene.getNonDeletedElements(), selectedElements, this.state));
4521
+ SnapCache.setReferenceSnapPoints(getReferenceSnapPoints(this.scene.getNonDeletedElements(), selectedElements, this.state, this.scene.getNonDeletedElementsMap()));
4396
4522
  }
4397
4523
  }
4398
4524
  maybeCacheVisibleGaps(event, selectedElements, recomputeAnyways = false) {
@@ -4402,7 +4528,7 @@ class App extends React.Component {
4402
4528
  selectedElements,
4403
4529
  }) &&
4404
4530
  (recomputeAnyways || !SnapCache.getVisibleGaps())) {
4405
- SnapCache.setVisibleGaps(getVisibleGaps(this.scene.getNonDeletedElements(), selectedElements, this.state));
4531
+ SnapCache.setVisibleGaps(getVisibleGaps(this.scene.getNonDeletedElements(), selectedElements, this.state, this.scene.getNonDeletedElementsMap()));
4406
4532
  }
4407
4533
  }
4408
4534
  onKeyDownFromPointerDownHandler(pointerDownState) {
@@ -4445,7 +4571,7 @@ class App extends React.Component {
4445
4571
  return;
4446
4572
  }
4447
4573
  if (this.state.activeTool.type === "laser") {
4448
- this.laserPathManager.addPointToPath(pointerCoords.x, pointerCoords.y);
4574
+ this.laserTrails.addPointToPath(pointerCoords.x, pointerCoords.y);
4449
4575
  }
4450
4576
  const [gridX, gridY] = getGridPoint(pointerCoords.x, pointerCoords.y, event[KEYS.CTRL_OR_CMD] ? null : this.state.gridSize);
4451
4577
  // for arrows/lines, don't start dragging until a given threshold
@@ -4466,10 +4592,11 @@ class App extends React.Component {
4466
4592
  return true;
4467
4593
  }
4468
4594
  }
4595
+ const elementsMap = this.scene.getNonDeletedElementsMap();
4469
4596
  if (this.state.selectedLinearElement) {
4470
4597
  const linearElementEditor = this.state.editingLinearElement || this.state.selectedLinearElement;
4471
- if (LinearElementEditor.shouldAddMidpoint(this.state.selectedLinearElement, pointerCoords, this.state)) {
4472
- const ret = LinearElementEditor.addMidpoint(this.state.selectedLinearElement, pointerCoords, this.state, !event[KEYS.CTRL_OR_CMD]);
4598
+ if (LinearElementEditor.shouldAddMidpoint(this.state.selectedLinearElement, pointerCoords, this.state, elementsMap)) {
4599
+ const ret = LinearElementEditor.addMidpoint(this.state.selectedLinearElement, pointerCoords, this.state, !event[KEYS.CTRL_OR_CMD], elementsMap);
4473
4600
  if (!ret) {
4474
4601
  return;
4475
4602
  }
@@ -4504,7 +4631,7 @@ class App extends React.Component {
4504
4631
  }
4505
4632
  const didDrag = LinearElementEditor.handlePointDragging(event, this.state, pointerCoords.x, pointerCoords.y, (element, pointsSceneCoords) => {
4506
4633
  this.maybeSuggestBindingsForLinearElementAtCoords(element, pointsSceneCoords);
4507
- }, linearElementEditor);
4634
+ }, linearElementEditor, this.scene.getNonDeletedElementsMap());
4508
4635
  if (didDrag) {
4509
4636
  pointerDownState.lastCoords.x = pointerCoords.x;
4510
4637
  pointerDownState.lastCoords.y = pointerCoords.y;
@@ -4585,7 +4712,7 @@ class App extends React.Component {
4585
4712
  // it snaps to its position if previously snapped already.
4586
4713
  this.maybeCacheVisibleGaps(event, selectedElements);
4587
4714
  this.maybeCacheReferenceSnapPoints(event, selectedElements);
4588
- const { snapOffset, snapLines } = snapDraggedElements(getSelectedElements(originalElements, this.state), dragOffset, this.state, event);
4715
+ const { snapOffset, snapLines } = snapDraggedElements(originalElements, dragOffset, this.state, event, this.scene.getNonDeletedElementsMap());
4589
4716
  this.setState({ snapLines });
4590
4717
  // when we're editing the name of a frame, we want the user to be
4591
4718
  // able to select and interact with the text input
@@ -4703,7 +4830,7 @@ class App extends React.Component {
4703
4830
  const elements = this.scene.getNonDeletedElements();
4704
4831
  // box-select line editor points
4705
4832
  if (this.state.editingLinearElement) {
4706
- LinearElementEditor.handleBoxSelection(event, this.state, this.setState.bind(this));
4833
+ LinearElementEditor.handleBoxSelection(event, this.state, this.setState.bind(this), this.scene.getNonDeletedElementsMap());
4707
4834
  // regular box-select
4708
4835
  }
4709
4836
  else {
@@ -4722,7 +4849,7 @@ class App extends React.Component {
4722
4849
  shouldReuseSelection = false;
4723
4850
  }
4724
4851
  }
4725
- const elementsWithinSelection = getElementsWithinSelection(elements, draggingElement);
4852
+ const elementsWithinSelection = getElementsWithinSelection(elements, draggingElement, this.scene.getNonDeletedElementsMap());
4726
4853
  this.setState((prevState) => {
4727
4854
  const nextSelectedElementIds = {
4728
4855
  ...(shouldReuseSelection && prevState.selectedElementIds),
@@ -4752,7 +4879,7 @@ class App extends React.Component {
4752
4879
  // select linear element only when we haven't box-selected anything else
4753
4880
  selectedLinearElement: elementsWithinSelection.length === 1 &&
4754
4881
  isLinearElement(elementsWithinSelection[0])
4755
- ? new LinearElementEditor(elementsWithinSelection[0], this.scene)
4882
+ ? new LinearElementEditor(elementsWithinSelection[0])
4756
4883
  : null,
4757
4884
  showHyperlinkPopup: elementsWithinSelection.length === 1 &&
4758
4885
  (elementsWithinSelection[0].link ||
@@ -4789,6 +4916,7 @@ class App extends React.Component {
4789
4916
  }
4790
4917
  onPointerUpFromPointerDownHandler(pointerDownState) {
4791
4918
  return withBatchedUpdates((childEvent) => {
4919
+ this.removePointer(childEvent);
4792
4920
  if (pointerDownState.eventListeners.onMove) {
4793
4921
  pointerDownState.eventListeners.onMove.flush();
4794
4922
  }
@@ -4815,6 +4943,7 @@ class App extends React.Component {
4815
4943
  this.setState({
4816
4944
  selectedElementsAreBeingDragged: false,
4817
4945
  });
4946
+ const elementsMap = this.scene.getNonDeletedElementsMap();
4818
4947
  // Handle end of dragging a point of a linear element, might close a loop
4819
4948
  // and sets binding element
4820
4949
  if (this.state.editingLinearElement) {
@@ -4824,7 +4953,7 @@ class App extends React.Component {
4824
4953
  this.actionManager.executeAction(actionFinalize);
4825
4954
  }
4826
4955
  else {
4827
- const editingLinearElement = LinearElementEditor.handlePointerUp(childEvent, this.state.editingLinearElement, this.state);
4956
+ const editingLinearElement = LinearElementEditor.handlePointerUp(childEvent, this.state.editingLinearElement, this.state, this.scene.getNonDeletedElements(), elementsMap);
4828
4957
  if (editingLinearElement !== this.state.editingLinearElement) {
4829
4958
  this.setState({
4830
4959
  editingLinearElement,
@@ -4843,11 +4972,11 @@ class App extends React.Component {
4843
4972
  }
4844
4973
  }
4845
4974
  else {
4846
- const linearElementEditor = LinearElementEditor.handlePointerUp(childEvent, this.state.selectedLinearElement, this.state);
4975
+ const linearElementEditor = LinearElementEditor.handlePointerUp(childEvent, this.state.selectedLinearElement, this.state, this.scene.getNonDeletedElements(), elementsMap);
4847
4976
  const { startBindingElement, endBindingElement } = linearElementEditor;
4848
4977
  const element = this.scene.getElement(linearElementEditor.elementId);
4849
4978
  if (isBindingElement(element)) {
4850
- bindOrUnbindLinearElement(element, startBindingElement, endBindingElement);
4979
+ bindOrUnbindLinearElement(element, startBindingElement, endBindingElement, elementsMap);
4851
4980
  }
4852
4981
  if (linearElementEditor !== this.state.selectedLinearElement) {
4853
4982
  this.setState({
@@ -4860,7 +4989,7 @@ class App extends React.Component {
4860
4989
  }
4861
4990
  }
4862
4991
  }
4863
- lastPointerUp = null;
4992
+ this.missingPointerEventCleanupEmitter.clear();
4864
4993
  window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.eventListeners.onMove);
4865
4994
  window.removeEventListener(EVENT.POINTER_UP, pointerDownState.eventListeners.onUp);
4866
4995
  window.removeEventListener(EVENT.KEYDOWN, pointerDownState.eventListeners.onKeyDown);
@@ -4868,6 +4997,7 @@ class App extends React.Component {
4868
4997
  if (this.state.pendingImageElementId) {
4869
4998
  this.setState({ pendingImageElementId: null });
4870
4999
  }
5000
+ this.props?.onPointerUp?.(activeTool, pointerDownState);
4871
5001
  this.onPointerUpEmitter.trigger(this.state.activeTool, pointerDownState, childEvent);
4872
5002
  if (draggingElement?.type === "freedraw") {
4873
5003
  const pointerCoords = viewportCoordsToSceneCoords(childEvent, this.state);
@@ -4934,7 +5064,7 @@ class App extends React.Component {
4934
5064
  else if (pointerDownState.drag.hasOccurred && !multiElement) {
4935
5065
  if (isBindingEnabled(this.state) &&
4936
5066
  isBindingElement(draggingElement, false)) {
4937
- maybeBindLinearElement(draggingElement, this.state, this.scene, pointerCoords);
5067
+ maybeBindLinearElement(draggingElement, this.state, this.scene, pointerCoords, elementsMap);
4938
5068
  }
4939
5069
  this.setState({ suggestedBindings: [], startBoundElement: null });
4940
5070
  if (!activeTool.locked) {
@@ -4948,7 +5078,7 @@ class App extends React.Component {
4948
5078
  ...prevState.selectedElementIds,
4949
5079
  [draggingElement.id]: true,
4950
5080
  }, prevState),
4951
- selectedLinearElement: new LinearElementEditor(draggingElement, this.scene),
5081
+ selectedLinearElement: new LinearElementEditor(draggingElement),
4952
5082
  }));
4953
5083
  }
4954
5084
  else {
@@ -4981,15 +5111,16 @@ class App extends React.Component {
4981
5111
  this.state.selectedLinearElement.isDragging) {
4982
5112
  const linearElement = this.scene.getElement(this.state.selectedLinearElement.elementId);
4983
5113
  if (linearElement?.frameId) {
4984
- const frame = getContainingFrame(linearElement);
5114
+ const frame = getContainingFrame(linearElement, elementsMap);
4985
5115
  if (frame && linearElement) {
4986
- if (!elementOverlapsWithFrame(linearElement, frame)) {
5116
+ if (!elementOverlapsWithFrame(linearElement, frame, this.scene.getNonDeletedElementsMap())) {
4987
5117
  // remove the linear element from all groups
4988
5118
  // before removing it from the frame as well
4989
5119
  mutateElement(linearElement, {
4990
5120
  groupIds: [],
4991
5121
  });
4992
- this.scene.replaceAllElements(removeElementsFromFrame(this.scene.getElementsIncludingDeleted(), [linearElement], this.state));
5122
+ removeElementsFromFrame([linearElement], this.scene.getNonDeletedElementsMap());
5123
+ this.scene.informMutation();
4993
5124
  }
4994
5125
  }
4995
5126
  }
@@ -4998,7 +5129,7 @@ class App extends React.Component {
4998
5129
  // update the relationships between selected elements and frames
4999
5130
  const topLayerFrame = this.getTopLayerFrameAtSceneCoords(sceneCoords);
5000
5131
  const selectedElements = this.scene.getSelectedElements(this.state);
5001
- let nextElements = this.scene.getElementsIncludingDeleted();
5132
+ let nextElements = this.scene.getElementsMapIncludingDeleted();
5002
5133
  const updateGroupIdsAfterEditingGroup = (elements) => {
5003
5134
  if (elements.length > 0) {
5004
5135
  for (const element of elements) {
@@ -5041,8 +5172,8 @@ class App extends React.Component {
5041
5172
  }
5042
5173
  }
5043
5174
  if (isFrameLikeElement(draggingElement)) {
5044
- const elementsInsideFrame = getElementsInNewFrame(this.scene.getElementsIncludingDeleted(), draggingElement);
5045
- this.scene.replaceAllElements(addElementsToFrame(this.scene.getElementsIncludingDeleted(), elementsInsideFrame, draggingElement));
5175
+ const elementsInsideFrame = getElementsInNewFrame(this.scene.getElementsIncludingDeleted(), draggingElement, this.scene.getNonDeletedElementsMap());
5176
+ this.scene.replaceAllElements(addElementsToFrame(this.scene.getElementsMapIncludingDeleted(), elementsInsideFrame, draggingElement));
5046
5177
  }
5047
5178
  mutateElement(draggingElement, getNormalizedDimensions(draggingElement));
5048
5179
  }
@@ -5061,7 +5192,7 @@ class App extends React.Component {
5061
5192
  .getSelectedElements(this.state)
5062
5193
  .filter((element) => isFrameLikeElement(element));
5063
5194
  for (const frame of selectedFrames) {
5064
- nextElements = replaceAllElementsInFrame(nextElements, getElementsInResizingFrame(this.scene.getElementsIncludingDeleted(), frame, this.state), frame, this.state);
5195
+ nextElements = replaceAllElementsInFrame(nextElements, getElementsInResizingFrame(this.scene.getElementsIncludingDeleted(), frame, this.state, elementsMap), frame, this);
5065
5196
  }
5066
5197
  this.scene.replaceAllElements(nextElements);
5067
5198
  }
@@ -5075,28 +5206,28 @@ class App extends React.Component {
5075
5206
  // the one we've hit
5076
5207
  if (selectedELements.length === 1) {
5077
5208
  this.setState({
5078
- selectedLinearElement: new LinearElementEditor(hitElement, this.scene),
5209
+ selectedLinearElement: new LinearElementEditor(hitElement),
5079
5210
  });
5080
5211
  }
5081
5212
  }
5082
- if (isEraserActive(this.state)) {
5083
- const draggedDistance = distance2d(this.lastPointerDownEvent.clientX, this.lastPointerDownEvent.clientY, this.lastPointerUpEvent.clientX, this.lastPointerUpEvent.clientY);
5213
+ const pointerStart = this.lastPointerDownEvent;
5214
+ const pointerEnd = this.lastPointerUpEvent || this.lastPointerMoveEvent;
5215
+ if (isEraserActive(this.state) && pointerStart && pointerEnd) {
5216
+ this.eraserTrail.endPath();
5217
+ const draggedDistance = distance2d(pointerStart.clientX, pointerStart.clientY, pointerEnd.clientX, pointerEnd.clientY);
5084
5218
  if (draggedDistance === 0) {
5085
5219
  const scenePointer = viewportCoordsToSceneCoords({
5086
- clientX: this.lastPointerUpEvent.clientX,
5087
- clientY: this.lastPointerUpEvent.clientY,
5220
+ clientX: pointerEnd.clientX,
5221
+ clientY: pointerEnd.clientY,
5088
5222
  }, this.state);
5089
5223
  const hitElements = this.getElementsAtPosition(scenePointer.x, scenePointer.y);
5090
- hitElements.forEach((hitElement) => (pointerDownState.elementIdsToErase[hitElement.id] = {
5091
- erase: true,
5092
- opacity: hitElement.opacity,
5093
- }));
5224
+ hitElements.forEach((hitElement) => this.elementsPendingErasure.add(hitElement.id));
5094
5225
  }
5095
- this.eraseElements(pointerDownState);
5226
+ this.eraseElements();
5096
5227
  return;
5097
5228
  }
5098
- else if (Object.keys(pointerDownState.elementIdsToErase).length) {
5099
- this.restoreReadyToEraseElements(pointerDownState);
5229
+ else if (this.elementsPendingErasure.size) {
5230
+ this.restoreReadyToEraseElements();
5100
5231
  }
5101
5232
  if (hitElement &&
5102
5233
  !pointerDownState.drag.hasOccurred &&
@@ -5148,7 +5279,7 @@ class App extends React.Component {
5148
5279
  // set selectedLinearElement only if thats the only element selected
5149
5280
  selectedLinearElement: newSelectedElements.length === 1 &&
5150
5281
  isLinearElement(newSelectedElements[0])
5151
- ? new LinearElementEditor(newSelectedElements[0], this.scene)
5282
+ ? new LinearElementEditor(newSelectedElements[0])
5152
5283
  : prevState.selectedLinearElement,
5153
5284
  };
5154
5285
  });
@@ -5202,7 +5333,7 @@ class App extends React.Component {
5202
5333
  // Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1.
5203
5334
  // Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized
5204
5335
  prevState.selectedLinearElement?.elementId !== hitElement.id
5205
- ? new LinearElementEditor(hitElement, this.scene)
5336
+ ? new LinearElementEditor(hitElement)
5206
5337
  : prevState.selectedLinearElement,
5207
5338
  }));
5208
5339
  }
@@ -5210,7 +5341,7 @@ class App extends React.Component {
5210
5341
  if (!pointerDownState.drag.hasOccurred &&
5211
5342
  !this.state.isResizing &&
5212
5343
  ((hitElement &&
5213
- isHittingElementBoundingBoxWithoutHittingElement(hitElement, this.state, this.frameNameBoundsCache, pointerDownState.origin.x, pointerDownState.origin.y)) ||
5344
+ isHittingElementBoundingBoxWithoutHittingElement(hitElement, this.state, this.frameNameBoundsCache, pointerDownState.origin.x, pointerDownState.origin.y, this.scene.getNonDeletedElementsMap())) ||
5214
5345
  (!hitElement &&
5215
5346
  pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements))) {
5216
5347
  if (this.state.editingLinearElement) {
@@ -5246,12 +5377,12 @@ class App extends React.Component {
5246
5377
  this.history.resumeRecording();
5247
5378
  }
5248
5379
  if (pointerDownState.drag.hasOccurred || isResizing || isRotating) {
5249
- (isBindingEnabled(this.state)
5250
- ? bindOrUnbindSelectedElements
5251
- : unbindLinearElements)(this.scene.getSelectedElements(this.state));
5380
+ isBindingEnabled(this.state)
5381
+ ? bindOrUnbindSelectedElements(this.scene.getSelectedElements(this.state), this.scene.getNonDeletedElements(), elementsMap)
5382
+ : unbindLinearElements(this.scene.getSelectedElements(this.state), elementsMap);
5252
5383
  }
5253
5384
  if (activeTool.type === "laser") {
5254
- this.laserPathManager.endPath();
5385
+ this.laserTrails.endPath();
5255
5386
  return;
5256
5387
  }
5257
5388
  if (!activeTool.locked && activeTool.type !== "freedraw") {
@@ -5281,52 +5412,27 @@ class App extends React.Component {
5281
5412
  }
5282
5413
  });
5283
5414
  }
5284
- restoreReadyToEraseElements = (pointerDownState) => {
5285
- const elements = this.scene.getElementsIncludingDeleted().map((ele) => {
5286
- if (pointerDownState.elementIdsToErase[ele.id] &&
5287
- pointerDownState.elementIdsToErase[ele.id].erase) {
5288
- return newElementWith(ele, {
5289
- opacity: pointerDownState.elementIdsToErase[ele.id].opacity,
5290
- });
5291
- }
5292
- else if (isBoundToContainer(ele) &&
5293
- pointerDownState.elementIdsToErase[ele.containerId] &&
5294
- pointerDownState.elementIdsToErase[ele.containerId].erase) {
5295
- return newElementWith(ele, {
5296
- opacity: pointerDownState.elementIdsToErase[ele.containerId].opacity,
5297
- });
5298
- }
5299
- else if (ele.frameId &&
5300
- pointerDownState.elementIdsToErase[ele.frameId] &&
5301
- pointerDownState.elementIdsToErase[ele.frameId].erase) {
5302
- return newElementWith(ele, {
5303
- opacity: pointerDownState.elementIdsToErase[ele.frameId].opacity,
5304
- });
5305
- }
5306
- return ele;
5307
- });
5308
- this.scene.replaceAllElements(elements);
5415
+ restoreReadyToEraseElements = () => {
5416
+ this.elementsPendingErasure = new Set();
5417
+ this.onSceneUpdated();
5309
5418
  };
5310
- eraseElements = (pointerDownState) => {
5419
+ eraseElements = () => {
5420
+ let didChange = false;
5311
5421
  const elements = this.scene.getElementsIncludingDeleted().map((ele) => {
5312
- if (pointerDownState.elementIdsToErase[ele.id] &&
5313
- pointerDownState.elementIdsToErase[ele.id].erase) {
5314
- return newElementWith(ele, { isDeleted: true });
5315
- }
5316
- else if (isBoundToContainer(ele) &&
5317
- pointerDownState.elementIdsToErase[ele.containerId] &&
5318
- pointerDownState.elementIdsToErase[ele.containerId].erase) {
5319
- return newElementWith(ele, { isDeleted: true });
5320
- }
5321
- else if (ele.frameId &&
5322
- pointerDownState.elementIdsToErase[ele.frameId] &&
5323
- pointerDownState.elementIdsToErase[ele.frameId].erase) {
5422
+ if (this.elementsPendingErasure.has(ele.id) ||
5423
+ (ele.frameId && this.elementsPendingErasure.has(ele.frameId)) ||
5424
+ (isBoundToContainer(ele) &&
5425
+ this.elementsPendingErasure.has(ele.containerId))) {
5426
+ didChange = true;
5324
5427
  return newElementWith(ele, { isDeleted: true });
5325
5428
  }
5326
5429
  return ele;
5327
5430
  });
5328
- this.history.resumeRecording();
5329
- this.scene.replaceAllElements(elements);
5431
+ this.elementsPendingErasure = new Set();
5432
+ if (didChange) {
5433
+ this.history.resumeRecording();
5434
+ this.scene.replaceAllElements(elements);
5435
+ }
5330
5436
  };
5331
5437
  initializeImage = async ({ imageFile, imageElement: _imageElement, showCursorImagePreview = false, }) => {
5332
5438
  // at this point this should be guaranteed image file, but we do this check
@@ -5450,9 +5556,18 @@ class App extends React.Component {
5450
5556
  // mustn't be larger than 128 px
5451
5557
  // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Basic_User_Interface/Using_URL_values_for_the_cursor_property
5452
5558
  const cursorImageSizePx = 96;
5453
- const imagePreview = await resizeImageFile(imageFile, {
5454
- maxWidthOrHeight: cursorImageSizePx,
5455
- });
5559
+ let imagePreview;
5560
+ try {
5561
+ imagePreview = await resizeImageFile(imageFile, {
5562
+ maxWidthOrHeight: cursorImageSizePx,
5563
+ });
5564
+ }
5565
+ catch (e) {
5566
+ if (e.cause === "UNSUPPORTED") {
5567
+ throw new Error(t("errors.unsupportedFileType"));
5568
+ }
5569
+ throw e;
5570
+ }
5456
5571
  let previewDataURL = await getDataURL(imagePreview);
5457
5572
  // SVG cannot be resized via `resizeImageFile` so we resize by rendering to
5458
5573
  // a small canvas
@@ -5607,7 +5722,7 @@ class App extends React.Component {
5607
5722
  }
5608
5723
  };
5609
5724
  maybeSuggestBindingAtCursor = (pointerCoords) => {
5610
- const hoveredBindableElement = getHoveredElementForBinding(pointerCoords, this.scene);
5725
+ const hoveredBindableElement = getHoveredElementForBinding(pointerCoords, this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap());
5611
5726
  this.setState({
5612
5727
  suggestedBindings: hoveredBindableElement != null ? [hoveredBindableElement] : [],
5613
5728
  });
@@ -5622,7 +5737,7 @@ class App extends React.Component {
5622
5737
  return;
5623
5738
  }
5624
5739
  const suggestedBindings = pointerCoords.reduce((acc, coords) => {
5625
- const hoveredBindableElement = getHoveredElementForBinding(coords, this.scene);
5740
+ const hoveredBindableElement = getHoveredElementForBinding(coords, this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap());
5626
5741
  if (hoveredBindableElement != null &&
5627
5742
  !isLinearElementSimpleAndAlreadyBound(linearElement, oppositeBindingBoundElement?.id, hoveredBindableElement)) {
5628
5743
  acc.push(hoveredBindableElement);
@@ -5635,7 +5750,7 @@ class App extends React.Component {
5635
5750
  if (selectedElements.length > 50) {
5636
5751
  return;
5637
5752
  }
5638
- const suggestedBindings = getEligibleElementsForBinding(selectedElements);
5753
+ const suggestedBindings = getEligibleElementsForBinding(selectedElements, this.scene.getNonDeletedElements(), this.scene.getNonDeletedElementsMap());
5639
5754
  this.setState({ suggestedBindings });
5640
5755
  }
5641
5756
  clearSelection(hitElement) {
@@ -5702,8 +5817,9 @@ class App extends React.Component {
5702
5817
  return;
5703
5818
  }
5704
5819
  catch (error) {
5820
+ // Don't throw for image scene daa
5705
5821
  if (error.name !== "EncodingError") {
5706
- throw error;
5822
+ throw new Error(t("alerts.couldNotLoadInvalidFile"));
5707
5823
  }
5708
5824
  }
5709
5825
  }
@@ -5764,7 +5880,32 @@ class App extends React.Component {
5764
5880
  loadFileToCanvas = async (file, fileHandle) => {
5765
5881
  file = await normalizeFile(file);
5766
5882
  try {
5767
- const ret = await loadSceneOrLibraryFromBlob(file, this.state, this.scene.getElementsIncludingDeleted(), fileHandle);
5883
+ let ret;
5884
+ try {
5885
+ ret = await loadSceneOrLibraryFromBlob(file, this.state, this.scene.getElementsIncludingDeleted(), fileHandle);
5886
+ }
5887
+ catch (error) {
5888
+ const imageSceneDataError = error instanceof ImageSceneDataError;
5889
+ if (imageSceneDataError &&
5890
+ error.code === "IMAGE_NOT_CONTAINS_SCENE_DATA" &&
5891
+ !this.isToolSupported("image")) {
5892
+ this.setState({
5893
+ isLoading: false,
5894
+ errorMessage: t("errors.imageToolNotSupported"),
5895
+ });
5896
+ return;
5897
+ }
5898
+ const errorMessage = imageSceneDataError
5899
+ ? t("alerts.cannotRestoreFromImage")
5900
+ : t("alerts.couldNotLoadInvalidFile");
5901
+ this.setState({
5902
+ isLoading: false,
5903
+ errorMessage,
5904
+ });
5905
+ }
5906
+ if (!ret) {
5907
+ return;
5908
+ }
5768
5909
  if (ret.type === MIME_TYPES.excalidraw) {
5769
5910
  this.setState({ isLoading: true });
5770
5911
  this.syncActionResult({
@@ -5791,15 +5932,6 @@ class App extends React.Component {
5791
5932
  }
5792
5933
  }
5793
5934
  catch (error) {
5794
- if (error instanceof ImageSceneDataError &&
5795
- error.code === "IMAGE_NOT_CONTAINS_SCENE_DATA" &&
5796
- !this.isToolSupported("image")) {
5797
- this.setState({
5798
- isLoading: false,
5799
- errorMessage: t("errors.imageToolNotSupported"),
5800
- });
5801
- return;
5802
- }
5803
5935
  this.setState({ isLoading: false, errorMessage: error.message });
5804
5936
  }
5805
5937
  };
@@ -5836,7 +5968,7 @@ class App extends React.Component {
5836
5968
  selectedElementIds: { [element.id]: true },
5837
5969
  }, this.scene.getNonDeletedElements(), this.state, this),
5838
5970
  selectedLinearElement: isLinearElement(element)
5839
- ? new LinearElementEditor(element, this.scene)
5971
+ ? new LinearElementEditor(element)
5840
5972
  : null,
5841
5973
  }
5842
5974
  : this.state),
@@ -5873,7 +6005,7 @@ class App extends React.Component {
5873
6005
  }, {
5874
6006
  x: gridX - pointerDownState.originInGrid.x,
5875
6007
  y: gridY - pointerDownState.originInGrid.y,
5876
- });
6008
+ }, this.scene.getNonDeletedElementsMap());
5877
6009
  gridX += snapOffset.x;
5878
6010
  gridY += snapOffset.y;
5879
6011
  this.setState({
@@ -5887,7 +6019,7 @@ class App extends React.Component {
5887
6019
  if (this.state.activeTool.type === TOOL_TYPE.frame ||
5888
6020
  this.state.activeTool.type === TOOL_TYPE.magicframe) {
5889
6021
  this.setState({
5890
- elementsToHighlight: getElementsInResizingFrame(this.scene.getNonDeletedElements(), draggingElement, this.state),
6022
+ elementsToHighlight: getElementsInResizingFrame(this.scene.getNonDeletedElements(), draggingElement, this.state, this.scene.getNonDeletedElementsMap()),
5891
6023
  });
5892
6024
  }
5893
6025
  }
@@ -5936,13 +6068,13 @@ class App extends React.Component {
5936
6068
  snapLines,
5937
6069
  });
5938
6070
  }
5939
- if (transformElements(pointerDownState, transformHandleType, selectedElements, pointerDownState.resize.arrowDirection, shouldRotateWithDiscreteAngle(event), shouldResizeFromCenter(event), selectedElements.length === 1 && isImageElement(selectedElements[0])
6071
+ if (transformElements(pointerDownState.originalElements, transformHandleType, selectedElements, this.scene.getElementsMapIncludingDeleted(), shouldRotateWithDiscreteAngle(event), shouldResizeFromCenter(event), selectedElements.length === 1 && isImageElement(selectedElements[0])
5940
6072
  ? !shouldMaintainAspectRatio(event)
5941
- : shouldMaintainAspectRatio(event), resizeX, resizeY, pointerDownState.resize.center.x, pointerDownState.resize.center.y, this.state)) {
6073
+ : shouldMaintainAspectRatio(event), resizeX, resizeY, pointerDownState.resize.center.x, pointerDownState.resize.center.y)) {
5942
6074
  this.maybeSuggestBindingForAll(selectedElements);
5943
6075
  const elementsToHighlight = new Set();
5944
6076
  selectedFrames.forEach((frame) => {
5945
- getElementsInResizingFrame(this.scene.getNonDeletedElements(), frame, this.state).forEach((element) => elementsToHighlight.add(element));
6077
+ getElementsInResizingFrame(this.scene.getNonDeletedElements(), frame, this.state, this.scene.getNonDeletedElementsMap()).forEach((element) => elementsToHighlight.add(element));
5946
6078
  });
5947
6079
  this.setState({
5948
6080
  elementsToHighlight: [...elementsToHighlight],
@@ -6075,7 +6207,7 @@ class App extends React.Component {
6075
6207
  if (container) {
6076
6208
  let elementCenterX = container.x + container.width / 2;
6077
6209
  let elementCenterY = container.y + container.height / 2;
6078
- const elementCenter = getContainerCenter(container, appState);
6210
+ const elementCenter = getContainerCenter(container, appState, this.scene.getNonDeletedElementsMap());
6079
6211
  if (elementCenter) {
6080
6212
  elementCenterX = elementCenter.x;
6081
6213
  elementCenterY = elementCenter.y;
@@ -6160,18 +6292,21 @@ class App extends React.Component {
6160
6292
  this.setAppState({});
6161
6293
  }
6162
6294
  }
6163
- if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
6164
- window.h = window.h || {};
6165
- Object.defineProperties(window.h, {
6166
- elements: {
6167
- configurable: true,
6168
- get() {
6169
- return this.app?.scene.getElementsIncludingDeleted();
6170
- },
6171
- set(elements) {
6172
- return this.app?.scene.replaceAllElements(elements);
6295
+ export const createTestHook = () => {
6296
+ if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
6297
+ window.h = window.h || {};
6298
+ Object.defineProperties(window.h, {
6299
+ elements: {
6300
+ configurable: true,
6301
+ get() {
6302
+ return this.app?.scene.getElementsIncludingDeleted();
6303
+ },
6304
+ set(elements) {
6305
+ return this.app?.scene.replaceAllElements(elements);
6306
+ },
6173
6307
  },
6174
- },
6175
- });
6176
- }
6308
+ });
6309
+ }
6310
+ };
6311
+ createTestHook();
6177
6312
  export default App;