@excalidraw/excalidraw 0.17.1-7500-ac247a0 → 0.17.1-b7babe5

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 (255) hide show
  1. package/CHANGELOG.md +56 -2
  2. package/dist/browser/dev/excalidraw-assets-dev/{chunk-2W5GQUR4.js → chunk-6NMK7JTV.js} +13 -6
  3. package/dist/browser/dev/excalidraw-assets-dev/chunk-6NMK7JTV.js.map +7 -0
  4. package/dist/browser/dev/excalidraw-assets-dev/chunk-CX3RATXT.js +20324 -0
  5. package/dist/browser/dev/excalidraw-assets-dev/chunk-CX3RATXT.js.map +7 -0
  6. package/dist/browser/dev/excalidraw-assets-dev/{en-OC6JWP3X.js → en-BZY7JRTM.js} +4 -2
  7. package/dist/browser/dev/excalidraw-assets-dev/{image-5TVMINCA.js → image-CVN3YKRW.js} +2 -4
  8. package/dist/browser/dev/excalidraw-assets-dev/image-LK4UNFRZ.css +6 -0
  9. package/dist/browser/dev/excalidraw-assets-dev/image-LK4UNFRZ.css.map +7 -0
  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 +34964 -37
  15. package/dist/browser/dev/index.js.map +4 -4
  16. package/dist/browser/prod/excalidraw-assets/chunk-VJAIK3AX.js +55 -0
  17. package/dist/browser/prod/excalidraw-assets/chunk-YYO5DFUW.js +11 -0
  18. package/dist/browser/prod/excalidraw-assets/en-O2YCQM2W.js +1 -0
  19. package/dist/browser/prod/excalidraw-assets/image-6FKY54X5.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-EY7E2L5O.json} +10 -5
  25. package/dist/dev/index.css +189 -129
  26. package/dist/dev/index.css.map +3 -3
  27. package/dist/dev/index.js +38702 -39409
  28. package/dist/dev/index.js.map +4 -4
  29. package/dist/excalidraw/actions/actionAddToLibrary.d.ts +15 -15
  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 +10 -10
  33. package/dist/excalidraw/actions/actionBoundText.js +8 -8
  34. package/dist/excalidraw/actions/actionCanvas.d.ts +58 -58
  35. package/dist/excalidraw/actions/actionClipboard.d.ts +34 -34
  36. package/dist/excalidraw/actions/actionClipboard.js +9 -2
  37. package/dist/excalidraw/actions/actionDeleteSelected.d.ts +15 -15
  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 +10 -10
  44. package/dist/excalidraw/actions/actionExport.d.ts +43 -43
  45. package/dist/excalidraw/actions/actionExport.js +4 -4
  46. package/dist/excalidraw/actions/actionFinalize.d.ts +9 -9
  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 +16 -16
  51. package/dist/excalidraw/actions/actionFrame.js +1 -1
  52. package/dist/excalidraw/actions/actionGroup.d.ts +10 -10
  53. package/dist/excalidraw/actions/actionGroup.js +3 -2
  54. package/dist/excalidraw/actions/actionLinearEditor.d.ts +5 -5
  55. package/dist/excalidraw/actions/actionLinearEditor.js +1 -1
  56. package/dist/excalidraw/{element/Hyperlink.d.ts → actions/actionLink.d.ts} +29 -51
  57. package/dist/excalidraw/actions/actionLink.js +40 -0
  58. package/dist/excalidraw/actions/actionMenu.d.ts +13 -13
  59. package/dist/excalidraw/actions/actionNavigate.d.ts +10 -10
  60. package/dist/excalidraw/actions/actionNavigate.js +1 -1
  61. package/dist/excalidraw/actions/actionProperties.d.ts +77 -77
  62. package/dist/excalidraw/actions/actionProperties.js +32 -27
  63. package/dist/excalidraw/actions/actionSelectAll.d.ts +5 -5
  64. package/dist/excalidraw/actions/actionSelectAll.js +1 -1
  65. package/dist/excalidraw/actions/actionStyles.d.ts +7 -7
  66. package/dist/excalidraw/actions/actionStyles.js +4 -4
  67. package/dist/excalidraw/actions/actionToggleGridMode.d.ts +5 -5
  68. package/dist/excalidraw/actions/actionToggleObjectsSnapMode.d.ts +5 -5
  69. package/dist/excalidraw/actions/actionToggleStats.d.ts +5 -5
  70. package/dist/excalidraw/actions/actionToggleViewMode.d.ts +5 -5
  71. package/dist/excalidraw/actions/actionToggleZenMode.d.ts +5 -5
  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 +23 -16
  87. package/dist/excalidraw/components/App.js +387 -272
  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 +17 -13
  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/PublishLibrary.js +1 -1
  102. package/dist/excalidraw/components/SVGLayer.d.ts +8 -0
  103. package/dist/excalidraw/components/SVGLayer.js +20 -0
  104. package/dist/excalidraw/components/ShareableLinkDialog.js +10 -10
  105. package/dist/excalidraw/components/Sidebar/Sidebar.d.ts +1 -1
  106. package/dist/excalidraw/components/Stack.d.ts +2 -2
  107. package/dist/excalidraw/components/TTDDialog/common.js +10 -1
  108. package/dist/excalidraw/components/TextField.d.ts +5 -2
  109. package/dist/excalidraw/components/TextField.js +6 -3
  110. package/dist/excalidraw/components/Toast.d.ts +3 -2
  111. package/dist/excalidraw/components/Toast.js +2 -2
  112. package/dist/excalidraw/components/ToolButton.js +2 -1
  113. package/dist/excalidraw/components/canvases/InteractiveCanvas.d.ts +2 -2
  114. package/dist/excalidraw/components/canvases/InteractiveCanvas.js +6 -5
  115. package/dist/excalidraw/components/canvases/StaticCanvas.d.ts +4 -3
  116. package/dist/excalidraw/components/canvases/StaticCanvas.js +7 -5
  117. package/dist/excalidraw/components/dropdownMenu/DropdownMenuContent.js +22 -2
  118. package/dist/excalidraw/components/hyperlink/Hyperlink.d.ts +19 -0
  119. package/dist/excalidraw/{element → components/hyperlink}/Hyperlink.js +40 -115
  120. package/dist/excalidraw/components/hyperlink/helpers.d.ts +7 -0
  121. package/dist/excalidraw/components/hyperlink/helpers.js +49 -0
  122. package/dist/excalidraw/components/icons.d.ts +2 -1
  123. package/dist/excalidraw/components/icons.js +2 -1
  124. package/dist/excalidraw/components/live-collaboration/LiveCollaborationTrigger.js +3 -2
  125. package/dist/excalidraw/components/main-menu/DefaultItems.js +5 -2
  126. package/dist/excalidraw/constants.d.ts +6 -0
  127. package/dist/excalidraw/constants.js +6 -0
  128. package/dist/excalidraw/data/blob.js +13 -14
  129. package/dist/excalidraw/data/filesystem.d.ts +1 -1
  130. package/dist/excalidraw/data/index.d.ts +2 -1
  131. package/dist/excalidraw/data/index.js +20 -16
  132. package/dist/excalidraw/data/json.d.ts +1 -1
  133. package/dist/excalidraw/data/json.js +5 -3
  134. package/dist/excalidraw/data/library.d.ts +60 -8
  135. package/dist/excalidraw/data/library.js +302 -33
  136. package/dist/excalidraw/data/resave.d.ts +1 -1
  137. package/dist/excalidraw/data/resave.js +2 -2
  138. package/dist/excalidraw/data/restore.js +8 -13
  139. package/dist/excalidraw/data/transform.js +13 -9
  140. package/dist/excalidraw/distribute.d.ts +2 -2
  141. package/dist/excalidraw/distribute.js +2 -2
  142. package/dist/excalidraw/element/ElementCanvasButtons.d.ts +3 -2
  143. package/dist/excalidraw/element/ElementCanvasButtons.js +4 -4
  144. package/dist/excalidraw/element/binding.d.ts +9 -9
  145. package/dist/excalidraw/element/binding.js +61 -59
  146. package/dist/excalidraw/element/bounds.d.ts +5 -5
  147. package/dist/excalidraw/element/bounds.js +29 -32
  148. package/dist/excalidraw/element/collision.d.ts +11 -11
  149. package/dist/excalidraw/element/collision.js +49 -46
  150. package/dist/excalidraw/element/containerCache.d.ts +11 -0
  151. package/dist/excalidraw/element/containerCache.js +14 -0
  152. package/dist/excalidraw/element/dragElements.js +10 -19
  153. package/dist/excalidraw/element/embeddable.d.ts +12 -13
  154. package/dist/excalidraw/element/embeddable.js +17 -27
  155. package/dist/excalidraw/element/image.js +1 -2
  156. package/dist/excalidraw/element/index.d.ts +8 -1
  157. package/dist/excalidraw/element/index.js +23 -1
  158. package/dist/excalidraw/element/linearElementEditor.d.ts +36 -36
  159. package/dist/excalidraw/element/linearElementEditor.js +79 -80
  160. package/dist/excalidraw/element/newElement.d.ts +4 -6
  161. package/dist/excalidraw/element/newElement.js +11 -16
  162. package/dist/excalidraw/element/resizeElements.d.ts +6 -6
  163. package/dist/excalidraw/element/resizeElements.js +40 -46
  164. package/dist/excalidraw/element/resizeTest.d.ts +3 -3
  165. package/dist/excalidraw/element/resizeTest.js +4 -4
  166. package/dist/excalidraw/element/sizeHelpers.d.ts +2 -2
  167. package/dist/excalidraw/element/sizeHelpers.js +2 -2
  168. package/dist/excalidraw/element/textElement.d.ts +34 -21
  169. package/dist/excalidraw/element/textElement.js +87 -111
  170. package/dist/excalidraw/element/textWysiwyg.d.ts +1 -6
  171. package/dist/excalidraw/element/textWysiwyg.js +15 -37
  172. package/dist/excalidraw/element/transformHandles.d.ts +4 -4
  173. package/dist/excalidraw/element/transformHandles.js +6 -6
  174. package/dist/excalidraw/element/typeChecks.js +4 -1
  175. package/dist/excalidraw/element/types.d.ts +24 -11
  176. package/dist/excalidraw/frame.d.ts +26 -20
  177. package/dist/excalidraw/frame.js +157 -84
  178. package/dist/excalidraw/groups.d.ts +3 -3
  179. package/dist/excalidraw/groups.js +11 -3
  180. package/dist/excalidraw/history.d.ts +1 -1
  181. package/dist/excalidraw/hooks/useLibraryItemSvg.js +1 -1
  182. package/dist/excalidraw/index.d.ts +9 -10
  183. package/dist/excalidraw/index.js +16 -12
  184. package/dist/excalidraw/laser-trails.d.ts +19 -0
  185. package/dist/excalidraw/laser-trails.js +95 -0
  186. package/dist/excalidraw/locales/en.json +10 -5
  187. package/dist/excalidraw/queue.d.ts +9 -0
  188. package/dist/excalidraw/queue.js +27 -0
  189. package/dist/excalidraw/reactUtils.d.ts +14 -0
  190. package/dist/excalidraw/reactUtils.js +45 -0
  191. package/dist/excalidraw/renderer/helpers.d.ts +13 -0
  192. package/dist/excalidraw/renderer/helpers.js +39 -0
  193. package/dist/excalidraw/renderer/interactiveScene.d.ts +20 -0
  194. package/dist/excalidraw/renderer/{renderScene.js → interactiveScene.js} +199 -474
  195. package/dist/excalidraw/renderer/renderElement.d.ts +6 -6
  196. package/dist/excalidraw/renderer/renderElement.js +54 -366
  197. package/dist/excalidraw/renderer/staticScene.d.ts +11 -0
  198. package/dist/excalidraw/renderer/staticScene.js +205 -0
  199. package/dist/excalidraw/renderer/staticSvgScene.d.ts +5 -0
  200. package/dist/excalidraw/renderer/staticSvgScene.js +385 -0
  201. package/dist/excalidraw/scene/Fonts.js +2 -1
  202. package/dist/excalidraw/scene/Renderer.d.ts +1 -1
  203. package/dist/excalidraw/scene/Renderer.js +32 -20
  204. package/dist/excalidraw/scene/Scene.d.ts +10 -9
  205. package/dist/excalidraw/scene/Scene.js +45 -21
  206. package/dist/excalidraw/scene/Shape.d.ts +3 -1
  207. package/dist/excalidraw/scene/Shape.js +7 -5
  208. package/dist/excalidraw/scene/ShapeCache.d.ts +2 -1
  209. package/dist/excalidraw/scene/ShapeCache.js +1 -0
  210. package/dist/excalidraw/scene/comparisons.js +2 -1
  211. package/dist/excalidraw/scene/export.d.ts +3 -0
  212. package/dist/excalidraw/scene/export.js +20 -40
  213. package/dist/excalidraw/scene/index.d.ts +0 -1
  214. package/dist/excalidraw/scene/index.js +0 -1
  215. package/dist/excalidraw/scene/scrollbars.d.ts +1 -1
  216. package/dist/excalidraw/scene/scrollbars.js +1 -1
  217. package/dist/excalidraw/scene/selection.d.ts +5 -5
  218. package/dist/excalidraw/scene/selection.js +16 -14
  219. package/dist/excalidraw/scene/types.d.ts +11 -5
  220. package/dist/excalidraw/snapping.d.ts +7 -7
  221. package/dist/excalidraw/snapping.js +21 -20
  222. package/dist/excalidraw/types.d.ts +16 -17
  223. package/dist/excalidraw/utility-types.d.ts +7 -0
  224. package/dist/excalidraw/utils.d.ts +21 -16
  225. package/dist/excalidraw/utils.js +43 -45
  226. package/dist/{dev/en-RLIAOBCI.json → prod/en-EY7E2L5O.json} +10 -5
  227. package/dist/prod/index.css +1 -1
  228. package/dist/prod/index.js +42 -42
  229. package/dist/utils/bbox.d.ts +2 -2
  230. package/dist/utils/export.d.ts +3 -3
  231. package/dist/utils/export.js +3 -13
  232. package/dist/utils/index.d.ts +2 -2
  233. package/dist/utils/index.js +2 -2
  234. package/dist/utils/withinBounds.d.ts +1 -1
  235. package/dist/utils/withinBounds.js +5 -2
  236. package/package.json +4 -4
  237. package/dist/browser/dev/excalidraw-assets-dev/chunk-2W5GQUR4.js.map +0 -7
  238. package/dist/browser/dev/excalidraw-assets-dev/chunk-KGZXLFLR.js +0 -53497
  239. package/dist/browser/dev/excalidraw-assets-dev/chunk-KGZXLFLR.js.map +0 -7
  240. package/dist/browser/dev/excalidraw-assets-dev/image-3MFRCKYM.css +0 -5797
  241. package/dist/browser/dev/excalidraw-assets-dev/image-3MFRCKYM.css.map +0 -7
  242. package/dist/browser/prod/excalidraw-assets/chunk-4YN2HN3S.js +0 -257
  243. package/dist/browser/prod/excalidraw-assets/chunk-OWLL6VOG.js +0 -11
  244. package/dist/browser/prod/excalidraw-assets/en-ERQOR3OC.js +0 -1
  245. package/dist/browser/prod/excalidraw-assets/image-LTLHTTSE.js +0 -1
  246. package/dist/browser/prod/excalidraw-assets/image-QBL334OA.css +0 -1
  247. package/dist/excalidraw/components/LaserTool/LaserPathManager.d.ts +0 -28
  248. package/dist/excalidraw/components/LaserTool/LaserPathManager.js +0 -225
  249. package/dist/excalidraw/components/LaserTool/LaserTool.d.ts +0 -8
  250. package/dist/excalidraw/components/LaserTool/LaserTool.js +0 -15
  251. package/dist/excalidraw/renderer/renderScene.d.ts +0 -25
  252. package/dist/excalidraw/vite.config.d.mts +0 -2
  253. package/dist/excalidraw/vite.config.mjs +0 -13
  254. /package/dist/browser/dev/excalidraw-assets-dev/{en-OC6JWP3X.js.map → en-BZY7JRTM.js.map} +0 -0
  255. /package/dist/browser/dev/excalidraw-assets-dev/{image-5TVMINCA.js.map → image-CVN3YKRW.js.map} +0 -0
@@ -1,15 +1,15 @@
1
1
  import { copyBlobToClipboardAsPng, copyTextToSystemClipboard, } from "../clipboard";
2
- import { DEFAULT_EXPORT_PADDING, isFirefox, MIME_TYPES } from "../constants";
2
+ import { DEFAULT_EXPORT_PADDING, DEFAULT_FILENAME, isFirefox, MIME_TYPES, } from "../constants";
3
3
  import { getNonDeletedElements } from "../element";
4
4
  import { isFrameLikeElement } from "../element/typeChecks";
5
5
  import { t } from "../i18n";
6
- import { elementsOverlappingBBox } from "../../utils/index";
7
6
  import { isSomeElementSelected, getSelectedElements } from "../scene";
8
7
  import { exportToCanvas, exportToSvg } from "../scene/export";
9
8
  import { cloneJSON } from "../utils";
10
9
  import { canvasToBlob } from "./blob";
11
10
  import { fileSave } from "./filesystem";
12
11
  import { serializeAsJSON } from "./json";
12
+ import { getElementsOverlappingFrame } from "../frame";
13
13
  export { loadFromBlob } from "./blob";
14
14
  export { loadFromJSON, saveAsJSON } from "./json";
15
15
  export const prepareElementsForExport = (elements, { selectedElementIds }, exportSelectionOnly) => {
@@ -26,11 +26,7 @@ export const prepareElementsForExport = (elements, { selectedElementIds }, expor
26
26
  if (exportedElements.length === 1 &&
27
27
  isFrameLikeElement(exportedElements[0])) {
28
28
  exportingFrame = exportedElements[0];
29
- exportedElements = elementsOverlappingBBox({
30
- elements,
31
- bounds: exportingFrame,
32
- type: "overlap",
33
- });
29
+ exportedElements = getElementsOverlappingFrame(elements, exportingFrame);
34
30
  }
35
31
  else if (exportedElements.length > 1) {
36
32
  exportedElements = getSelectedElements(elements, { selectedElementIds }, {
@@ -44,12 +40,12 @@ export const prepareElementsForExport = (elements, { selectedElementIds }, expor
44
40
  exportedElements: cloneJSON(exportedElements),
45
41
  };
46
42
  };
47
- export const exportCanvas = async (type, elements, appState, files, { exportBackground, exportPadding = DEFAULT_EXPORT_PADDING, viewBackgroundColor, name, fileHandle = null, exportingFrame = null, }) => {
43
+ export const exportCanvas = async (type, elements, appState, files, { exportBackground, exportPadding = DEFAULT_EXPORT_PADDING, viewBackgroundColor, name = appState.name || DEFAULT_FILENAME, fileHandle = null, exportingFrame = null, }) => {
48
44
  if (elements.length === 0) {
49
45
  throw new Error(t("alerts.cannotExportEmptyCanvas"));
50
46
  }
51
47
  if (type === "svg" || type === "clipboard-svg") {
52
- const tempSvg = await exportToSvg(elements, {
48
+ const svgPromise = exportToSvg(elements, {
53
49
  exportBackground,
54
50
  exportWithDarkMode: appState.exportWithDarkMode,
55
51
  viewBackgroundColor,
@@ -58,7 +54,9 @@ export const exportCanvas = async (type, elements, appState, files, { exportBack
58
54
  exportEmbedScene: appState.exportEmbedScene && type === "svg",
59
55
  }, files, { exportingFrame });
60
56
  if (type === "svg") {
61
- return await fileSave(new Blob([tempSvg.outerHTML], { type: MIME_TYPES.svg }), {
57
+ return fileSave(svgPromise.then((svg) => {
58
+ return new Blob([svg.outerHTML], { type: MIME_TYPES.svg });
59
+ }), {
62
60
  description: "Export to SVG",
63
61
  name,
64
62
  extension: appState.exportEmbedScene ? "excalidraw.svg" : "svg",
@@ -66,7 +64,13 @@ export const exportCanvas = async (type, elements, appState, files, { exportBack
66
64
  });
67
65
  }
68
66
  else if (type === "clipboard-svg") {
69
- await copyTextToSystemClipboard(tempSvg.outerHTML);
67
+ const svg = await svgPromise.then((svg) => svg.outerHTML);
68
+ try {
69
+ await copyTextToSystemClipboard(svg);
70
+ }
71
+ catch (e) {
72
+ throw new Error(t("errors.copyToSystemClipboardFailed"));
73
+ }
70
74
  return;
71
75
  }
72
76
  }
@@ -77,14 +81,14 @@ export const exportCanvas = async (type, elements, appState, files, { exportBack
77
81
  exportingFrame,
78
82
  });
79
83
  if (type === "png") {
80
- let blob = await canvasToBlob(tempCanvas);
84
+ let blob = canvasToBlob(tempCanvas);
81
85
  if (appState.exportEmbedScene) {
82
- blob = await (await import("./image")).encodePngMetadata({
86
+ blob = blob.then((blob) => import("./image").then(({ encodePngMetadata }) => encodePngMetadata({
83
87
  blob,
84
88
  metadata: serializeAsJSON(elements, appState, files, "local"),
85
- });
89
+ })));
86
90
  }
87
- return await fileSave(blob, {
91
+ return fileSave(blob, {
88
92
  description: "Export to PNG",
89
93
  name,
90
94
  // FIXME reintroduce `excalidraw.png` when most people upgrade away
@@ -101,7 +105,7 @@ export const exportCanvas = async (type, elements, appState, files, { exportBack
101
105
  catch (error) {
102
106
  console.warn(error);
103
107
  if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
104
- throw error;
108
+ throw new Error(t("canvasError.canvasTooBig"));
105
109
  }
106
110
  // TypeError *probably* suggests ClipboardItem not defined, which
107
111
  // people on Firefox can enable through a flag, so let's tell them.
@@ -2,7 +2,7 @@ import { ExcalidrawElement } from "../element/types";
2
2
  import { AppState, BinaryFiles, LibraryItems } from "../types";
3
3
  import { ImportedDataState, ImportedLibraryData } from "./types";
4
4
  export declare const serializeAsJSON: (elements: readonly ExcalidrawElement[], appState: Partial<AppState>, files: BinaryFiles, type: "local" | "database") => string;
5
- export declare const saveAsJSON: (elements: readonly ExcalidrawElement[], appState: AppState, files: BinaryFiles) => Promise<{
5
+ export declare const saveAsJSON: (elements: readonly ExcalidrawElement[], appState: AppState, files: BinaryFiles, name?: string) => Promise<{
6
6
  fileHandle: import("browser-fs-access").FileSystemHandle | null;
7
7
  }>;
8
8
  export declare const loadFromJSON: (localAppState: AppState, localElements: readonly ExcalidrawElement[] | null) => Promise<import("./restore").RestoredDataState>;
@@ -1,6 +1,6 @@
1
1
  import { fileOpen, fileSave } from "./filesystem";
2
2
  import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState";
3
- import { EXPORT_DATA_TYPES, EXPORT_SOURCE, MIME_TYPES, VERSIONS, } from "../constants";
3
+ import { DEFAULT_FILENAME, EXPORT_DATA_TYPES, EXPORT_SOURCE, MIME_TYPES, VERSIONS, } from "../constants";
4
4
  import { clearElementsForDatabase, clearElementsForExport } from "../element";
5
5
  import { isImageFileHandle, loadFromBlob, normalizeFile } from "./blob";
6
6
  /**
@@ -36,13 +36,15 @@ export const serializeAsJSON = (elements, appState, files, type) => {
36
36
  };
37
37
  return JSON.stringify(data, null, 2);
38
38
  };
39
- export const saveAsJSON = async (elements, appState, files) => {
39
+ export const saveAsJSON = async (elements, appState, files,
40
+ /** filename */
41
+ name = appState.name || DEFAULT_FILENAME) => {
40
42
  const serialized = serializeAsJSON(elements, appState, files, "local");
41
43
  const blob = new Blob([serialized], {
42
44
  type: MIME_TYPES.excalidraw,
43
45
  });
44
46
  const fileHandle = await fileSave(blob, {
45
- name: appState.name,
47
+ name,
46
48
  extension: "excalidraw",
47
49
  description: "Excalidraw file",
48
50
  fileHandle: isImageFileHandle(appState.fileHandle)
@@ -1,13 +1,53 @@
1
- import { LibraryItems, ExcalidrawImperativeAPI, LibraryItemsSource } from "../types";
1
+ import { LibraryItems, ExcalidrawImperativeAPI, LibraryItemsSource, LibraryItems_anyVersion } from "../types";
2
2
  import type App from "../components/App";
3
3
  import { ExcalidrawElement } from "../element/types";
4
+ import { MaybePromise } from "../utility-types";
5
+ export type LibraryPersistedData = {
6
+ libraryItems: LibraryItems;
7
+ };
8
+ export type LibraryAdatapterSource = "load" | "save";
9
+ export interface LibraryPersistenceAdapter {
10
+ /**
11
+ * Should load data that were previously saved into the database using the
12
+ * `save` method. Should throw if saving fails.
13
+ *
14
+ * Will be used internally in multiple places, such as during save to
15
+ * in order to reconcile changes with latest store data.
16
+ */
17
+ load(metadata: {
18
+ /**
19
+ * Indicates whether we're loading data for save purposes, or reading
20
+ * purposes, in which case host app can implement more aggressive caching.
21
+ */
22
+ source: LibraryAdatapterSource;
23
+ }): MaybePromise<{
24
+ libraryItems: LibraryItems_anyVersion;
25
+ } | null>;
26
+ /** Should persist to the database as is (do no change the data structure). */
27
+ save(libraryData: LibraryPersistedData): MaybePromise<void>;
28
+ }
29
+ export interface LibraryMigrationAdapter {
30
+ /**
31
+ * loads data from legacy data source. Returns `null` if no data is
32
+ * to be migrated.
33
+ */
34
+ load(): MaybePromise<{
35
+ libraryItems: LibraryItems_anyVersion;
36
+ } | null>;
37
+ /** clears entire storage afterwards */
38
+ clear(): MaybePromise<void>;
39
+ }
4
40
  export declare const libraryItemsAtom: import("jotai").PrimitiveAtom<{
5
41
  status: "loading" | "loaded";
42
+ /** indicates whether library is initialized with library items (has gone
43
+ * through at least one update). Used in UI. Specific to this atom only. */
6
44
  isInitialized: boolean;
7
45
  libraryItems: LibraryItems;
8
46
  }> & {
9
47
  init: {
10
48
  status: "loading" | "loaded";
49
+ /** indicates whether library is initialized with library items (has gone
50
+ * through at least one update). Used in UI. Specific to this atom only. */
11
51
  isInitialized: boolean;
12
52
  libraryItems: LibraryItems;
13
53
  };
@@ -17,10 +57,9 @@ export declare const libraryItemsAtom: import("jotai").PrimitiveAtom<{
17
57
  export declare const mergeLibraryItems: (localItems: LibraryItems, otherItems: LibraryItems) => LibraryItems;
18
58
  declare class Library {
19
59
  /** latest libraryItems */
20
- private lastLibraryItems;
21
- /** indicates whether library is initialized with library items (has gone
22
- * though at least one update) */
23
- private isInitialized;
60
+ private currLibraryItems;
61
+ /** snapshot of library items since last onLibraryChange call */
62
+ private prevLibraryItems;
24
63
  private app;
25
64
  constructor(app: App);
26
65
  private updateQueue;
@@ -48,7 +87,20 @@ export declare const parseLibraryTokensFromUrl: () => {
48
87
  libraryUrl: string;
49
88
  idToken: string | null;
50
89
  } | null;
51
- export declare const useHandleLibrary: ({ excalidrawAPI, getInitialLibraryItems, }: {
90
+ export declare const getLibraryItemsHash: (items: LibraryItems) => number;
91
+ export declare const useHandleLibrary: (opts: {
52
92
  excalidrawAPI: ExcalidrawImperativeAPI | null;
53
- getInitialLibraryItems?: (() => LibraryItemsSource) | undefined;
54
- }) => void;
93
+ } & ({
94
+ /** @deprecated we recommend using `opts.adapter` instead */
95
+ getInitialLibraryItems?: () => MaybePromise<LibraryItemsSource>;
96
+ } | {
97
+ adapter: LibraryPersistenceAdapter;
98
+ /**
99
+ * Adapter that takes care of loading data from legacy data store.
100
+ * Supply this if you want to migrate data on initial load from legacy
101
+ * data store.
102
+ *
103
+ * Can be a different LibraryPersistenceAdapter.
104
+ */
105
+ migrationAdapter?: LibraryMigrationAdapter;
106
+ })) => void;
@@ -8,8 +8,12 @@ import { t } from "../i18n";
8
8
  import { useEffect, useRef } from "react";
9
9
  import { URL_HASH_KEYS, URL_QUERY_KEYS, APP_NAME, EVENT, DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_TAB, } from "../constants";
10
10
  import { libraryItemSvgsCache } from "../hooks/useLibraryItemSvg";
11
- import { cloneJSON } from "../utils";
12
- export const libraryItemsAtom = atom({ status: "loaded", isInitialized: true, libraryItems: [] });
11
+ import { arrayToMap, cloneJSON, preventUnload, promiseTry, resolvablePromise, } from "../utils";
12
+ import { Emitter } from "../emitter";
13
+ import { Queue } from "../queue";
14
+ import { hashElementsVersion, hashString } from "../element";
15
+ const onLibraryUpdateEmitter = new Emitter();
16
+ export const libraryItemsAtom = atom({ status: "loaded", isInitialized: false, libraryItems: [] });
13
17
  const cloneLibraryItems = (libraryItems) => cloneJSON(libraryItems);
14
18
  /**
15
19
  * checks if library item does not exist already in current library
@@ -39,12 +43,36 @@ export const mergeLibraryItems = (localItems, otherItems) => {
39
43
  }
40
44
  return [...newItems, ...localItems];
41
45
  };
46
+ /**
47
+ * Returns { deletedItems, addedItems } maps of all added and deleted items
48
+ * since last onLibraryChange event.
49
+ *
50
+ * Host apps are recommended to diff with the latest state they have.
51
+ */
52
+ const createLibraryUpdate = (prevLibraryItems, nextLibraryItems) => {
53
+ const nextItemsMap = arrayToMap(nextLibraryItems);
54
+ const update = {
55
+ deletedItems: new Map(),
56
+ addedItems: new Map(),
57
+ };
58
+ for (const item of prevLibraryItems) {
59
+ if (!nextItemsMap.has(item.id)) {
60
+ update.deletedItems.set(item.id, item);
61
+ }
62
+ }
63
+ const prevItemsMap = arrayToMap(prevLibraryItems);
64
+ for (const item of nextLibraryItems) {
65
+ if (!prevItemsMap.has(item.id)) {
66
+ update.addedItems.set(item.id, item);
67
+ }
68
+ }
69
+ return update;
70
+ };
42
71
  class Library {
43
72
  /** latest libraryItems */
44
- lastLibraryItems = [];
45
- /** indicates whether library is initialized with library items (has gone
46
- * though at least one update) */
47
- isInitialized = false;
73
+ currLibraryItems = [];
74
+ /** snapshot of library items since last onLibraryChange call */
75
+ prevLibraryItems = cloneLibraryItems(this.currLibraryItems);
48
76
  app;
49
77
  constructor(app) {
50
78
  this.app = app;
@@ -55,21 +83,25 @@ class Library {
55
83
  };
56
84
  notifyListeners = () => {
57
85
  if (this.updateQueue.length > 0) {
58
- jotaiStore.set(libraryItemsAtom, {
86
+ jotaiStore.set(libraryItemsAtom, (s) => ({
59
87
  status: "loading",
60
- libraryItems: this.lastLibraryItems,
61
- isInitialized: this.isInitialized,
62
- });
88
+ libraryItems: this.currLibraryItems,
89
+ isInitialized: s.isInitialized,
90
+ }));
63
91
  }
64
92
  else {
65
- this.isInitialized = true;
66
93
  jotaiStore.set(libraryItemsAtom, {
67
94
  status: "loaded",
68
- libraryItems: this.lastLibraryItems,
69
- isInitialized: this.isInitialized,
95
+ libraryItems: this.currLibraryItems,
96
+ isInitialized: true,
70
97
  });
71
98
  try {
72
- this.app.props.onLibraryChange?.(cloneLibraryItems(this.lastLibraryItems));
99
+ const prevLibraryItems = this.prevLibraryItems;
100
+ this.prevLibraryItems = cloneLibraryItems(this.currLibraryItems);
101
+ const nextLibraryItems = cloneLibraryItems(this.currLibraryItems);
102
+ this.app.props.onLibraryChange?.(nextLibraryItems);
103
+ // for internal use in `useHandleLibrary` hook
104
+ onLibraryUpdateEmitter.trigger(createLibraryUpdate(prevLibraryItems, nextLibraryItems), nextLibraryItems);
73
105
  }
74
106
  catch (error) {
75
107
  console.error(error);
@@ -78,9 +110,8 @@ class Library {
78
110
  };
79
111
  /** call on excalidraw instance unmount */
80
112
  destroy = () => {
81
- this.isInitialized = false;
82
113
  this.updateQueue = [];
83
- this.lastLibraryItems = [];
114
+ this.currLibraryItems = [];
84
115
  jotaiStore.set(libraryItemSvgsCache, new Map());
85
116
  // TODO uncomment after/if we make jotai store scoped to each excal instance
86
117
  // jotaiStore.set(libraryItemsAtom, {
@@ -99,7 +130,7 @@ class Library {
99
130
  return new Promise(async (resolve) => {
100
131
  try {
101
132
  const libraryItems = await (this.getLastUpdateTask() ||
102
- this.lastLibraryItems);
133
+ this.currLibraryItems);
103
134
  if (this.updateQueue.length > 0) {
104
135
  resolve(this.getLatestLibrary());
105
136
  }
@@ -108,7 +139,7 @@ class Library {
108
139
  }
109
140
  }
110
141
  catch (error) {
111
- return resolve(this.lastLibraryItems);
142
+ return resolve(this.currLibraryItems);
112
143
  }
113
144
  });
114
145
  };
@@ -126,7 +157,7 @@ class Library {
126
157
  try {
127
158
  const source = await (typeof libraryItems === "function" &&
128
159
  !(libraryItems instanceof Blob)
129
- ? libraryItems(this.lastLibraryItems)
160
+ ? libraryItems(this.currLibraryItems)
130
161
  : libraryItems);
131
162
  let nextItems;
132
163
  if (source instanceof Blob) {
@@ -146,7 +177,7 @@ class Library {
146
177
  this.app.focusContainer();
147
178
  }
148
179
  if (merge) {
149
- resolve(mergeLibraryItems(this.lastLibraryItems, nextItems));
180
+ resolve(mergeLibraryItems(this.currLibraryItems, nextItems));
150
181
  }
151
182
  else {
152
183
  resolve(nextItems);
@@ -178,10 +209,10 @@ class Library {
178
209
  try {
179
210
  await this.getLastUpdateTask();
180
211
  if (typeof libraryItems === "function") {
181
- libraryItems = libraryItems(this.lastLibraryItems);
212
+ libraryItems = libraryItems(this.currLibraryItems);
182
213
  }
183
- this.lastLibraryItems = cloneLibraryItems(await libraryItems);
184
- resolve(this.lastLibraryItems);
214
+ this.currLibraryItems = cloneLibraryItems(await libraryItems);
215
+ resolve(this.currLibraryItems);
185
216
  }
186
217
  catch (error) {
187
218
  reject(error);
@@ -190,7 +221,7 @@ class Library {
190
221
  .catch((error) => {
191
222
  if (error.name === "AbortError") {
192
223
  console.warn("Library update aborted by user");
193
- return this.lastLibraryItems;
224
+ return this.currLibraryItems;
194
225
  }
195
226
  throw error;
196
227
  })
@@ -291,12 +322,105 @@ export const parseLibraryTokensFromUrl = () => {
291
322
  : null;
292
323
  return libraryUrl ? { libraryUrl, idToken } : null;
293
324
  };
294
- export const useHandleLibrary = ({ excalidrawAPI, getInitialLibraryItems, }) => {
295
- const getInitialLibraryRef = useRef(getInitialLibraryItems);
325
+ class AdapterTransaction {
326
+ static queue = new Queue();
327
+ static async getLibraryItems(adapter, source, _queue = true) {
328
+ const task = () => new Promise(async (resolve, reject) => {
329
+ try {
330
+ const data = await adapter.load({ source });
331
+ resolve(restoreLibraryItems(data?.libraryItems || [], "published"));
332
+ }
333
+ catch (error) {
334
+ reject(error);
335
+ }
336
+ });
337
+ if (_queue) {
338
+ return AdapterTransaction.queue.push(task);
339
+ }
340
+ return task();
341
+ }
342
+ static run = async (adapter, fn) => {
343
+ const transaction = new AdapterTransaction(adapter);
344
+ return AdapterTransaction.queue.push(() => fn(transaction));
345
+ };
346
+ // ------------------
347
+ adapter;
348
+ constructor(adapter) {
349
+ this.adapter = adapter;
350
+ }
351
+ getLibraryItems(source) {
352
+ return AdapterTransaction.getLibraryItems(this.adapter, source, false);
353
+ }
354
+ }
355
+ let lastSavedLibraryItemsHash = 0;
356
+ let librarySaveCounter = 0;
357
+ export const getLibraryItemsHash = (items) => {
358
+ return hashString(items
359
+ .map((item) => {
360
+ return `${item.id}:${hashElementsVersion(item.elements)}`;
361
+ })
362
+ .sort()
363
+ .join());
364
+ };
365
+ const persistLibraryUpdate = async (adapter, update) => {
366
+ try {
367
+ librarySaveCounter++;
368
+ return await AdapterTransaction.run(adapter, async (transaction) => {
369
+ const nextLibraryItemsMap = arrayToMap(await transaction.getLibraryItems("save"));
370
+ for (const [id] of update.deletedItems) {
371
+ nextLibraryItemsMap.delete(id);
372
+ }
373
+ const addedItems = [];
374
+ // we want to merge current library items with the ones stored in the
375
+ // DB so that we don't lose any elements that for some reason aren't
376
+ // in the current editor library, which could happen when:
377
+ //
378
+ // 1. we haven't received an update deleting some elements
379
+ // (in which case it's still better to keep them in the DB lest
380
+ // it was due to a different reason)
381
+ // 2. we keep a single DB for all active editors, but the editors'
382
+ // libraries aren't synced or there's a race conditions during
383
+ // syncing
384
+ // 3. some other race condition, e.g. during init where emit updates
385
+ // for partial updates (e.g. you install a 3rd party library and
386
+ // init from DB only after — we emit events for both updates)
387
+ for (const [id, item] of update.addedItems) {
388
+ if (nextLibraryItemsMap.has(id)) {
389
+ // replace item with latest version
390
+ // TODO we could prefer the newer item instead
391
+ nextLibraryItemsMap.set(id, item);
392
+ }
393
+ else {
394
+ // we want to prepend the new items with the ones that are already
395
+ // in DB to preserve the ordering we do in editor (newly added
396
+ // items are added to the beginning)
397
+ addedItems.push(item);
398
+ }
399
+ }
400
+ const nextLibraryItems = addedItems.concat(Array.from(nextLibraryItemsMap.values()));
401
+ const version = getLibraryItemsHash(nextLibraryItems);
402
+ if (version !== lastSavedLibraryItemsHash) {
403
+ await adapter.save({ libraryItems: nextLibraryItems });
404
+ }
405
+ lastSavedLibraryItemsHash = version;
406
+ return nextLibraryItems;
407
+ });
408
+ }
409
+ finally {
410
+ librarySaveCounter--;
411
+ }
412
+ };
413
+ export const useHandleLibrary = (opts) => {
414
+ const { excalidrawAPI } = opts;
415
+ const optsRef = useRef(opts);
416
+ optsRef.current = opts;
417
+ const isLibraryLoadedRef = useRef(false);
296
418
  useEffect(() => {
297
419
  if (!excalidrawAPI) {
298
420
  return;
299
421
  }
422
+ // reset on editor remount (excalidrawAPI changed)
423
+ isLibraryLoadedRef.current = false;
300
424
  const importLibraryFromURL = async ({ libraryUrl, idToken, }) => {
301
425
  const libraryPromise = new Promise(async (resolve, reject) => {
302
426
  try {
@@ -357,20 +481,165 @@ export const useHandleLibrary = ({ excalidrawAPI, getInitialLibraryItems, }) =>
357
481
  }
358
482
  };
359
483
  // -------------------------------------------------------------------------
360
- // ------ init load --------------------------------------------------------
361
- if (getInitialLibraryRef.current) {
362
- excalidrawAPI.updateLibrary({
363
- libraryItems: getInitialLibraryRef.current(),
364
- });
365
- }
484
+ // ---------------------------------- init ---------------------------------
485
+ // -------------------------------------------------------------------------
366
486
  const libraryUrlTokens = parseLibraryTokensFromUrl();
367
487
  if (libraryUrlTokens) {
368
488
  importLibraryFromURL(libraryUrlTokens);
369
489
  }
490
+ // ------ (A) init load (legacy) -------------------------------------------
491
+ if ("getInitialLibraryItems" in optsRef.current &&
492
+ optsRef.current.getInitialLibraryItems) {
493
+ console.warn("useHandleLibrar `opts.getInitialLibraryItems` is deprecated. Use `opts.adapter` instead.");
494
+ Promise.resolve(optsRef.current.getInitialLibraryItems())
495
+ .then((libraryItems) => {
496
+ excalidrawAPI.updateLibrary({
497
+ libraryItems,
498
+ // merge with current library items because we may have already
499
+ // populated it (e.g. by installing 3rd party library which can
500
+ // happen before the DB data is loaded)
501
+ merge: true,
502
+ });
503
+ })
504
+ .catch((error) => {
505
+ console.error(`UseHandeLibrary getInitialLibraryItems failed: ${error?.message}`);
506
+ });
507
+ }
508
+ // -------------------------------------------------------------------------
370
509
  // --------------------------------------------------------- init load -----
510
+ // -------------------------------------------------------------------------
511
+ // ------ (B) data source adapter ------------------------------------------
512
+ if ("adapter" in optsRef.current && optsRef.current.adapter) {
513
+ const adapter = optsRef.current.adapter;
514
+ const migrationAdapter = optsRef.current.migrationAdapter;
515
+ const initDataPromise = resolvablePromise();
516
+ // migrate from old data source if needed
517
+ // (note, if `migrate` function is defined, we always migrate even
518
+ // if the data has already been migrated. In that case it'll be a no-op,
519
+ // though with several unnecessary steps — we will still load latest
520
+ // DB data during the `persistLibraryChange()` step)
521
+ // -----------------------------------------------------------------------
522
+ if (migrationAdapter) {
523
+ initDataPromise.resolve(promiseTry(migrationAdapter.load)
524
+ .then(async (libraryData) => {
525
+ let restoredData = null;
526
+ try {
527
+ // if no library data to migrate, assume no migration needed
528
+ // and skip persisting to new data store, as well as well
529
+ // clearing the old store via `migrationAdapter.clear()`
530
+ if (!libraryData) {
531
+ return AdapterTransaction.getLibraryItems(adapter, "load");
532
+ }
533
+ restoredData = restoreLibraryItems(libraryData.libraryItems || [], "published");
534
+ // we don't queue this operation because it's running inside
535
+ // a promise that's running inside Library update queue itself
536
+ const nextItems = await persistLibraryUpdate(adapter, createLibraryUpdate([], restoredData));
537
+ try {
538
+ await migrationAdapter.clear();
539
+ }
540
+ catch (error) {
541
+ console.error(`couldn't delete legacy library data: ${error.message}`);
542
+ }
543
+ // migration suceeded, load migrated data
544
+ return nextItems;
545
+ }
546
+ catch (error) {
547
+ console.error(`couldn't migrate legacy library data: ${error.message}`);
548
+ // migration failed, load data from previous store, if any
549
+ return restoredData;
550
+ }
551
+ })
552
+ // errors caught during `migrationAdapter.load()`
553
+ .catch((error) => {
554
+ console.error(`error during library migration: ${error.message}`);
555
+ // as a default, load latest library from current data source
556
+ return AdapterTransaction.getLibraryItems(adapter, "load");
557
+ }));
558
+ }
559
+ else {
560
+ initDataPromise.resolve(promiseTry(AdapterTransaction.getLibraryItems, adapter, "load"));
561
+ }
562
+ // load initial (or migrated) library
563
+ excalidrawAPI
564
+ .updateLibrary({
565
+ libraryItems: initDataPromise.then((libraryItems) => {
566
+ const _libraryItems = libraryItems || [];
567
+ lastSavedLibraryItemsHash = getLibraryItemsHash(_libraryItems);
568
+ return _libraryItems;
569
+ }),
570
+ // merge with current library items because we may have already
571
+ // populated it (e.g. by installing 3rd party library which can
572
+ // happen before the DB data is loaded)
573
+ merge: true,
574
+ })
575
+ .finally(() => {
576
+ isLibraryLoadedRef.current = true;
577
+ });
578
+ }
579
+ // ---------------------------------------------- data source datapter -----
371
580
  window.addEventListener(EVENT.HASHCHANGE, onHashChange);
372
581
  return () => {
373
582
  window.removeEventListener(EVENT.HASHCHANGE, onHashChange);
374
583
  };
375
- }, [excalidrawAPI]);
584
+ }, [
585
+ // important this useEffect only depends on excalidrawAPI so it only reruns
586
+ // on editor remounts (the excalidrawAPI changes)
587
+ excalidrawAPI,
588
+ ]);
589
+ // This effect is run without excalidrawAPI dependency so that host apps
590
+ // can run this hook outside of an active editor instance and the library
591
+ // update queue/loop survives editor remounts
592
+ //
593
+ // This effect is still only meant to be run if host apps supply an persitence
594
+ // adapter. If we don't have access to it, it the update listener doesn't
595
+ // do anything.
596
+ useEffect(() => {
597
+ // on update, merge with current library items and persist
598
+ // -----------------------------------------------------------------------
599
+ const unsubOnLibraryUpdate = onLibraryUpdateEmitter.on(async (update, nextLibraryItems) => {
600
+ const isLoaded = isLibraryLoadedRef.current;
601
+ // we want to operate with the latest adapter, but we don't want this
602
+ // effect to rerun on every adapter change in case host apps' adapter
603
+ // isn't stable
604
+ const adapter = ("adapter" in optsRef.current && optsRef.current.adapter) || null;
605
+ try {
606
+ if (adapter) {
607
+ if (
608
+ // if nextLibraryItems hash identical to previously saved hash,
609
+ // exit early, even if actual upstream state ends up being
610
+ // different (e.g. has more data than we have locally), as it'd
611
+ // be low-impact scenario.
612
+ lastSavedLibraryItemsHash !==
613
+ getLibraryItemsHash(nextLibraryItems)) {
614
+ await persistLibraryUpdate(adapter, update);
615
+ }
616
+ }
617
+ }
618
+ catch (error) {
619
+ console.error(`couldn't persist library update: ${error.message}`, update);
620
+ // currently we only show error if an editor is loaded
621
+ if (isLoaded && optsRef.current.excalidrawAPI) {
622
+ optsRef.current.excalidrawAPI.updateScene({
623
+ appState: {
624
+ errorMessage: t("errors.saveLibraryError"),
625
+ },
626
+ });
627
+ }
628
+ }
629
+ });
630
+ const onUnload = (event) => {
631
+ if (librarySaveCounter) {
632
+ preventUnload(event);
633
+ }
634
+ };
635
+ window.addEventListener(EVENT.BEFORE_UNLOAD, onUnload);
636
+ return () => {
637
+ window.removeEventListener(EVENT.BEFORE_UNLOAD, onUnload);
638
+ unsubOnLibraryUpdate();
639
+ lastSavedLibraryItemsHash = 0;
640
+ librarySaveCounter = 0;
641
+ };
642
+ }, [
643
+ // this effect must not have any deps so it doesn't rerun
644
+ ]);
376
645
  };