@tldraw/editor 4.5.2 → 4.6.0-canary.4ec045c286e1

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 (226) hide show
  1. package/dist-cjs/index.d.ts +37 -6
  2. package/dist-cjs/index.js +6 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +7 -5
  5. package/dist-cjs/lib/TldrawEditor.js.map +3 -3
  6. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js +3 -2
  7. package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -1
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +8 -5
  11. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
  12. package/dist-cjs/lib/config/TLSessionStateSnapshot.js +8 -5
  13. package/dist-cjs/lib/config/TLSessionStateSnapshot.js.map +2 -2
  14. package/dist-cjs/lib/config/TLUserPreferences.js +3 -2
  15. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  16. package/dist-cjs/lib/config/createTLStore.js +1 -0
  17. package/dist-cjs/lib/config/createTLStore.js.map +2 -2
  18. package/dist-cjs/lib/editor/Editor.js +52 -16
  19. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  20. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +62 -6
  21. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
  22. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +4 -3
  23. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
  24. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js +5 -0
  25. package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +2 -2
  26. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +2 -2
  27. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  28. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +3 -2
  29. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  30. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  31. package/dist-cjs/lib/exports/FontEmbedder.js +9 -8
  32. package/dist-cjs/lib/exports/FontEmbedder.js.map +2 -2
  33. package/dist-cjs/lib/exports/StyleEmbedder.js +27 -15
  34. package/dist-cjs/lib/exports/StyleEmbedder.js.map +3 -3
  35. package/dist-cjs/lib/exports/domUtils.js +15 -0
  36. package/dist-cjs/lib/exports/domUtils.js.map +2 -2
  37. package/dist-cjs/lib/exports/embedMedia.js +15 -12
  38. package/dist-cjs/lib/exports/embedMedia.js.map +2 -2
  39. package/dist-cjs/lib/exports/exportToSvg.js +8 -7
  40. package/dist-cjs/lib/exports/exportToSvg.js.map +2 -2
  41. package/dist-cjs/lib/exports/getSvgAsImage.js +181 -29
  42. package/dist-cjs/lib/exports/getSvgAsImage.js.map +3 -3
  43. package/dist-cjs/lib/exports/getSvgJsx.js +21 -9
  44. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  45. package/dist-cjs/lib/globals/environment.js +4 -3
  46. package/dist-cjs/lib/globals/environment.js.map +2 -2
  47. package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -2
  48. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  49. package/dist-cjs/lib/hooks/useDocumentEvents.js +13 -11
  50. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  51. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js +3 -2
  52. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  53. package/dist-cjs/lib/hooks/useScreenBounds.js +10 -6
  54. package/dist-cjs/lib/hooks/useScreenBounds.js.map +2 -2
  55. package/dist-cjs/lib/hooks/useViewportHeight.js +13 -11
  56. package/dist-cjs/lib/hooks/useViewportHeight.js.map +3 -3
  57. package/dist-cjs/lib/license/Watermark.js +10 -0
  58. package/dist-cjs/lib/license/Watermark.js.map +2 -2
  59. package/dist-cjs/lib/primitives/Vec.js +35 -22
  60. package/dist-cjs/lib/primitives/Vec.js.map +2 -2
  61. package/dist-cjs/lib/primitives/geometry/Arc2d.js +6 -13
  62. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  63. package/dist-cjs/lib/primitives/geometry/Circle2d.js +31 -2
  64. package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
  65. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +9 -0
  66. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  67. package/dist-cjs/lib/primitives/geometry/CubicSpline2d.js +9 -0
  68. package/dist-cjs/lib/primitives/geometry/CubicSpline2d.js.map +2 -2
  69. package/dist-cjs/lib/primitives/geometry/Edge2d.js +32 -18
  70. package/dist-cjs/lib/primitives/geometry/Edge2d.js.map +2 -2
  71. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +12 -0
  72. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
  73. package/dist-cjs/lib/primitives/geometry/Polyline2d.js +51 -12
  74. package/dist-cjs/lib/primitives/geometry/Polyline2d.js.map +2 -2
  75. package/dist-cjs/lib/primitives/geometry/Stadium2d.js +12 -0
  76. package/dist-cjs/lib/primitives/geometry/Stadium2d.js.map +2 -2
  77. package/dist-cjs/lib/primitives/geometry/geometry.bench.js +133 -0
  78. package/dist-cjs/lib/primitives/geometry/geometry.bench.js.map +7 -0
  79. package/dist-cjs/lib/primitives/intersect.js +16 -15
  80. package/dist-cjs/lib/primitives/intersect.js.map +2 -2
  81. package/dist-cjs/lib/primitives/utils.js +0 -1
  82. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  83. package/dist-cjs/lib/utils/browserCanvasMaxSize.js +3 -2
  84. package/dist-cjs/lib/utils/browserCanvasMaxSize.js.map +2 -2
  85. package/dist-cjs/lib/utils/dom.js +15 -2
  86. package/dist-cjs/lib/utils/dom.js.map +2 -2
  87. package/dist-cjs/version.js +3 -3
  88. package/dist-cjs/version.js.map +1 -1
  89. package/dist-esm/index.d.mts +37 -6
  90. package/dist-esm/index.mjs +8 -1
  91. package/dist-esm/index.mjs.map +2 -2
  92. package/dist-esm/lib/TldrawEditor.mjs +7 -5
  93. package/dist-esm/lib/TldrawEditor.mjs.map +3 -3
  94. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs +2 -1
  95. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs.map +2 -2
  96. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -1
  97. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  98. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +8 -5
  99. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
  100. package/dist-esm/lib/config/TLSessionStateSnapshot.mjs +8 -5
  101. package/dist-esm/lib/config/TLSessionStateSnapshot.mjs.map +2 -2
  102. package/dist-esm/lib/config/TLUserPreferences.mjs +3 -2
  103. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  104. package/dist-esm/lib/config/createTLStore.mjs +1 -0
  105. package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
  106. package/dist-esm/lib/editor/Editor.mjs +53 -17
  107. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  108. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +64 -6
  109. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
  110. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +4 -3
  111. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
  112. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs +5 -0
  113. package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +2 -2
  114. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +2 -2
  115. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  116. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +3 -2
  117. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  118. package/dist-esm/lib/exports/FontEmbedder.mjs +9 -8
  119. package/dist-esm/lib/exports/FontEmbedder.mjs.map +2 -2
  120. package/dist-esm/lib/exports/StyleEmbedder.mjs +29 -16
  121. package/dist-esm/lib/exports/StyleEmbedder.mjs.map +3 -3
  122. package/dist-esm/lib/exports/domUtils.mjs +15 -0
  123. package/dist-esm/lib/exports/domUtils.mjs.map +2 -2
  124. package/dist-esm/lib/exports/embedMedia.mjs +16 -13
  125. package/dist-esm/lib/exports/embedMedia.mjs.map +2 -2
  126. package/dist-esm/lib/exports/exportToSvg.mjs +8 -7
  127. package/dist-esm/lib/exports/exportToSvg.mjs.map +2 -2
  128. package/dist-esm/lib/exports/getSvgAsImage.mjs +181 -29
  129. package/dist-esm/lib/exports/getSvgAsImage.mjs.map +3 -3
  130. package/dist-esm/lib/exports/getSvgJsx.mjs +21 -9
  131. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  132. package/dist-esm/lib/globals/environment.mjs +4 -3
  133. package/dist-esm/lib/globals/environment.mjs.map +2 -2
  134. package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -2
  135. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  136. package/dist-esm/lib/hooks/useDocumentEvents.mjs +13 -11
  137. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  138. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs +3 -2
  139. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  140. package/dist-esm/lib/hooks/useScreenBounds.mjs +10 -6
  141. package/dist-esm/lib/hooks/useScreenBounds.mjs.map +2 -2
  142. package/dist-esm/lib/hooks/useViewportHeight.mjs +13 -11
  143. package/dist-esm/lib/hooks/useViewportHeight.mjs.map +3 -3
  144. package/dist-esm/lib/license/Watermark.mjs +10 -0
  145. package/dist-esm/lib/license/Watermark.mjs.map +2 -2
  146. package/dist-esm/lib/primitives/Vec.mjs +35 -22
  147. package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
  148. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +6 -13
  149. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  150. package/dist-esm/lib/primitives/geometry/Circle2d.mjs +31 -2
  151. package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
  152. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +9 -0
  153. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  154. package/dist-esm/lib/primitives/geometry/CubicSpline2d.mjs +9 -0
  155. package/dist-esm/lib/primitives/geometry/CubicSpline2d.mjs.map +2 -2
  156. package/dist-esm/lib/primitives/geometry/Edge2d.mjs +32 -18
  157. package/dist-esm/lib/primitives/geometry/Edge2d.mjs.map +2 -2
  158. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs +13 -1
  159. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
  160. package/dist-esm/lib/primitives/geometry/Polyline2d.mjs +51 -12
  161. package/dist-esm/lib/primitives/geometry/Polyline2d.mjs.map +2 -2
  162. package/dist-esm/lib/primitives/geometry/Stadium2d.mjs +13 -1
  163. package/dist-esm/lib/primitives/geometry/Stadium2d.mjs.map +2 -2
  164. package/dist-esm/lib/primitives/geometry/geometry.bench.mjs +132 -0
  165. package/dist-esm/lib/primitives/geometry/geometry.bench.mjs.map +7 -0
  166. package/dist-esm/lib/primitives/intersect.mjs +17 -16
  167. package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
  168. package/dist-esm/lib/primitives/utils.mjs +0 -1
  169. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  170. package/dist-esm/lib/utils/browserCanvasMaxSize.mjs +3 -2
  171. package/dist-esm/lib/utils/browserCanvasMaxSize.mjs.map +2 -2
  172. package/dist-esm/lib/utils/dom.mjs +15 -2
  173. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  174. package/dist-esm/version.mjs +3 -3
  175. package/dist-esm/version.mjs.map +1 -1
  176. package/package.json +7 -7
  177. package/src/index.ts +3 -0
  178. package/src/lib/TldrawEditor.tsx +7 -5
  179. package/src/lib/components/default-components/CanvasShapeIndicators.tsx +2 -1
  180. package/src/lib/components/default-components/DefaultCanvas.tsx +1 -1
  181. package/src/lib/components/default-components/DefaultErrorFallback.tsx +8 -5
  182. package/src/lib/config/TLSessionStateSnapshot.ts +8 -5
  183. package/src/lib/config/TLUserPreferences.ts +3 -2
  184. package/src/lib/config/createTLStore.ts +3 -0
  185. package/src/lib/editor/Editor.ts +53 -15
  186. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +7 -6
  187. package/src/lib/editor/managers/FocusManager/FocusManager.ts +10 -7
  188. package/src/lib/editor/managers/FontManager/FontManager.test.ts +1 -0
  189. package/src/lib/editor/managers/FontManager/FontManager.ts +4 -3
  190. package/src/lib/editor/managers/HistoryManager/HistoryManager.test.ts +16 -0
  191. package/src/lib/editor/managers/HistoryManager/HistoryManager.ts +7 -2
  192. package/src/lib/editor/managers/TextManager/TextManager.test.ts +4 -5
  193. package/src/lib/editor/managers/TextManager/TextManager.ts +2 -2
  194. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +3 -2
  195. package/src/lib/editor/types/misc-types.ts +8 -2
  196. package/src/lib/exports/FontEmbedder.ts +10 -9
  197. package/src/lib/exports/StyleEmbedder.ts +33 -15
  198. package/src/lib/exports/domUtils.ts +20 -0
  199. package/src/lib/exports/embedMedia.ts +23 -17
  200. package/src/lib/exports/exportToSvg.tsx +8 -7
  201. package/src/lib/exports/getSvgAsImage.ts +292 -32
  202. package/src/lib/exports/getSvgJsx.test.ts +103 -101
  203. package/src/lib/exports/getSvgJsx.tsx +33 -10
  204. package/src/lib/globals/environment.ts +4 -3
  205. package/src/lib/hooks/useCanvasEvents.ts +2 -3
  206. package/src/lib/hooks/useDocumentEvents.ts +16 -11
  207. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +3 -3
  208. package/src/lib/hooks/useScreenBounds.ts +10 -6
  209. package/src/lib/hooks/useViewportHeight.ts +13 -11
  210. package/src/lib/license/Watermark.tsx +10 -0
  211. package/src/lib/primitives/Vec.ts +51 -24
  212. package/src/lib/primitives/geometry/Arc2d.ts +10 -15
  213. package/src/lib/primitives/geometry/Circle2d.ts +40 -2
  214. package/src/lib/primitives/geometry/CubicBezier2d.ts +10 -0
  215. package/src/lib/primitives/geometry/CubicSpline2d.ts +10 -0
  216. package/src/lib/primitives/geometry/Edge2d.ts +41 -18
  217. package/src/lib/primitives/geometry/Ellipse2d.ts +14 -1
  218. package/src/lib/primitives/geometry/Polyline2d.ts +60 -12
  219. package/src/lib/primitives/geometry/Stadium2d.ts +14 -1
  220. package/src/lib/primitives/geometry/geometry.bench.ts +179 -0
  221. package/src/lib/primitives/intersect.ts +27 -27
  222. package/src/lib/primitives/utils.ts +4 -4
  223. package/src/lib/test/TestEditor.ts +1 -0
  224. package/src/lib/utils/browserCanvasMaxSize.ts +4 -2
  225. package/src/lib/utils/dom.ts +34 -2
  226. package/src/version.ts +3 -3
@@ -172,8 +172,8 @@ export class HistoryManager<R extends UnknownRecord> {
172
172
  }
173
173
 
174
174
  if (!didFindMark && toMark) {
175
- // whoops, we didn't find the mark we were looking for
176
- // don't do anything
175
+ // we didn't find the mark we were looking for — restore state and bail
176
+ this.pendingDiff.restore(pendingDiff)
177
177
  return this
178
178
  }
179
179
 
@@ -338,6 +338,11 @@ class PendingDiff<R extends UnknownRecord> {
338
338
  return diff
339
339
  }
340
340
 
341
+ restore(diff: RecordsDiff<R>) {
342
+ this.diff = diff
343
+ this.isEmptyAtom.set(isRecordsDiffEmpty(diff))
344
+ }
345
+
341
346
  isEmpty() {
342
347
  return this.isEmptyAtom.get()
343
348
  }
@@ -58,17 +58,16 @@ const mockCreateElement = vi.fn(() => {
58
58
  })
59
59
 
60
60
  // Mock editor
61
+ const mockDocument = {
62
+ createElement: mockCreateElement,
63
+ }
61
64
  const mockEditor = {
62
65
  getContainer: vi.fn(() => ({
63
66
  appendChild: vi.fn(),
64
67
  })),
68
+ getContainerDocument: vi.fn(() => mockDocument),
65
69
  } as unknown as Editor
66
70
 
67
- // Setup global mocks
68
- global.document = {
69
- createElement: mockCreateElement,
70
- } as any
71
-
72
71
  global.Range = vi.fn(() => ({
73
72
  setStart: vi.fn(),
74
73
  setEnd: vi.fn(),
@@ -75,7 +75,7 @@ export class TextManager {
75
75
  private elm: HTMLDivElement
76
76
 
77
77
  constructor(public editor: Editor) {
78
- const elm = document.createElement('div')
78
+ const elm = editor.getContainerDocument().createElement('div')
79
79
  elm.classList.add('tl-text')
80
80
  elm.classList.add('tl-text-measure')
81
81
  elm.setAttribute('dir', 'auto')
@@ -111,7 +111,7 @@ export class TextManager {
111
111
  }
112
112
 
113
113
  measureText(textToMeasure: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
114
- const div = document.createElement('div')
114
+ const div = this.editor.getContainerDocument().createElement('div')
115
115
  div.textContent = normalizeTextForDom(textToMeasure)
116
116
  return this.measureHtml(div.innerHTML, opts)
117
117
  }
@@ -1,6 +1,7 @@
1
1
  import { atom, computed } from '@tldraw/state'
2
2
  import { TLUserPreferences, defaultUserPreferences } from '../../../config/TLUserPreferences'
3
3
  import { TLUser } from '../../../config/createTLUser'
4
+ import { getGlobalWindow } from '../../../utils/dom'
4
5
 
5
6
  /** @public */
6
7
  export class UserPreferencesManager {
@@ -13,9 +14,9 @@ export class UserPreferencesManager {
13
14
  private readonly user: TLUser,
14
15
  private readonly inferDarkMode: boolean
15
16
  ) {
16
- if (typeof window === 'undefined' || !window.matchMedia) return
17
+ if (typeof window === 'undefined' || !getGlobalWindow().matchMedia) return
17
18
 
18
- const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
19
+ const darkModeMediaQuery = getGlobalWindow().matchMedia('(prefers-color-scheme: dark)')
19
20
  if (darkModeMediaQuery?.matches) {
20
21
  this.systemColorScheme.set('dark')
21
22
  }
@@ -40,9 +40,15 @@ export interface TLSvgExportOptions {
40
40
  background?: boolean
41
41
 
42
42
  /**
43
- * How much padding to include around the bounds of exports? Defaults to 32px.
43
+ * How much padding to include around the bounds of exports.
44
+ *
45
+ * - `'auto'` (default) — trim to visual content bounds, capturing overflow (like thick
46
+ * strokes or arrowheads) without extra whitespace.
47
+ * - `number` (e.g. `32`) — fixed padding in px. No trimming; overflow beyond the padding
48
+ * region is clipped.
49
+ * - `0` — no padding, no trimming, overflow is clipped.
44
50
  */
45
- padding?: number
51
+ padding?: number | 'auto'
46
52
 
47
53
  /**
48
54
  * Should the export be rendered in dark mode (true) or light mode (false)? Defaults to the
@@ -11,7 +11,7 @@ export const SVG_EXPORT_CLASSNAME = 'tldraw-svg-export'
11
11
  * SVG.
12
12
  *
13
13
  * It works in three steps:
14
- * 1. `startFindingCurrentDocumentFontFaces` - this traverses the current document, finding all the
14
+ * 1. `startFindingDocumentFontFaces` - this traverses the given document, finding all the
15
15
  * stylesheets in use (including those imported via `@import` rules etc) and extracting the
16
16
  * @font-face declarations from them.
17
17
  * 2. `onFontFamilyValue` - as `StyleEmbedder` traverses the SVG, it will call this method with the
@@ -26,9 +26,9 @@ export class FontEmbedder {
26
26
  private readonly fontFacesToEmbed = new Set<ParsedFontFace>()
27
27
  private readonly pendingPromises: Promise<void>[] = []
28
28
 
29
- startFindingCurrentDocumentFontFaces() {
29
+ startFindingDocumentFontFaces(doc: Document) {
30
30
  assert(!this.fontFacesPromise, 'FontEmbedder already started')
31
- this.fontFacesPromise = getCurrentDocumentFontFaces()
31
+ this.fontFacesPromise = getDocumentFontFaces(doc)
32
32
  }
33
33
 
34
34
  @bind onFontFamilyValue(fontFamilyValue: string) {
@@ -80,7 +80,8 @@ export class FontEmbedder {
80
80
  }
81
81
  }
82
82
 
83
- async function getCurrentDocumentFontFaces() {
83
+ async function getDocumentFontFaces(doc: Document) {
84
+ const win = doc.defaultView ?? globalThis
84
85
  const fontFaces: (ParsedFontFace[] | Promise<ParsedFontFace[] | null>)[] = []
85
86
 
86
87
  // In exportToSvg we add the exported node to the DOM temporarily.
@@ -88,7 +89,7 @@ async function getCurrentDocumentFontFaces() {
88
89
  // DOM, when looking at document.styleSheets the number of nodes and stylesheets
89
90
  // can grow unbounded (especially when using "Debug svg" and moving shapes around).
90
91
  // To avoid this, we filter out the stylesheets that are part of the SVG export.
91
- const styleSheetsWithoutSvgExports = Array.from(document.styleSheets).filter(
92
+ const styleSheetsWithoutSvgExports = Array.from(doc.styleSheets).filter(
92
93
  (styleSheet) =>
93
94
  !(styleSheet.ownerNode as HTMLElement | null)?.closest(`.${SVG_EXPORT_CLASSNAME}`)
94
95
  )
@@ -103,10 +104,10 @@ async function getCurrentDocumentFontFaces() {
103
104
 
104
105
  if (cssRules) {
105
106
  for (const rule of styleSheet.cssRules) {
106
- if (rule instanceof CSSFontFaceRule) {
107
- fontFaces.push(parseCssFontFaces(rule.cssText, styleSheet.href ?? document.baseURI))
108
- } else if (rule instanceof CSSImportRule) {
109
- const absoluteUrl = new URL(rule.href, rule.parentStyleSheet?.href ?? document.baseURI)
107
+ if (rule instanceof win.CSSFontFaceRule) {
108
+ fontFaces.push(parseCssFontFaces(rule.cssText, styleSheet.href ?? doc.baseURI))
109
+ } else if (rule instanceof win.CSSImportRule) {
110
+ const absoluteUrl = new URL(rule.href, rule.parentStyleSheet?.href ?? doc.baseURI)
110
111
  fontFaces.push(fetchCssFontFaces(absoluteUrl.href))
111
112
  }
112
113
  }
@@ -6,6 +6,7 @@ import {
6
6
  getComputedStyle,
7
7
  getRenderedChildNodes,
8
8
  getRenderedChildren,
9
+ isElement,
9
10
  } from './domUtils'
10
11
  import { resourceToDataUrl } from './fetchCache'
11
12
  import { parseCssValueUrls, shouldIncludeCssProperty } from './parseCss'
@@ -49,7 +50,7 @@ export class StyleEmbedder {
49
50
  { shouldRespectDefaults = true, shouldSkipInheritedParentStyles = true }
50
51
  ) {
51
52
  const defaultStyles = shouldRespectDefaults
52
- ? getDefaultStylesForTagName(element.tagName.toLowerCase())
53
+ ? getDefaultStylesForTagName(element.ownerDocument, element.tagName.toLowerCase())
53
54
  : NO_STYLES
54
55
 
55
56
  const parentStyles = Object.assign({}, NO_STYLES) as Styles
@@ -116,14 +117,14 @@ export class StyleEmbedder {
116
117
  const shadowRoot = element.shadowRoot
117
118
 
118
119
  if (shadowRoot) {
119
- const clonedCustomEl = document.createElement('div')
120
+ const clonedCustomEl = element.ownerDocument.createElement('div')
120
121
  this.styles.set(clonedCustomEl, this.styles.get(element)!)
121
122
 
122
123
  clonedCustomEl.setAttribute('data-tl-custom-element', element.tagName)
123
124
  ;(clonedParent ?? element.parentElement!).appendChild(clonedCustomEl)
124
125
 
125
126
  for (const child of shadowRoot.childNodes) {
126
- if (child instanceof Element) {
127
+ if (isElement(child)) {
127
128
  visit(child, clonedCustomEl)
128
129
  } else {
129
130
  clonedCustomEl.appendChild(child.cloneNode(true))
@@ -144,7 +145,7 @@ export class StyleEmbedder {
144
145
  clonedParent.appendChild(clonedEl)
145
146
 
146
147
  for (const child of getRenderedChildNodes(element)) {
147
- if (child instanceof Element) {
148
+ if (isElement(child)) {
148
149
  visit(child, clonedEl)
149
150
  } else {
150
151
  clonedEl.appendChild(child.cloneNode(true))
@@ -295,36 +296,53 @@ function formatCss(style: ReadonlyStyles) {
295
296
  // when we're figuring out the default values for a tag, we need read them from a separate document
296
297
  // so they're not affected by the current document's styles
297
298
  let defaultStyleFrame:
298
- | { iframe: HTMLIFrameElement; foreignObject: SVGForeignObjectElement; document: Document }
299
+ | {
300
+ iframe: HTMLIFrameElement
301
+ foreignObject: SVGForeignObjectElement
302
+ document: Document
303
+ ownerDocument: Document
304
+ }
299
305
  | undefined
300
306
  const defaultStylesByTagName: Record<string, ReadonlyStyles> = {}
301
- function getDefaultStyleFrame() {
302
- if (!defaultStyleFrame) {
303
- const frame = document.createElement('iframe')
307
+ function getDefaultStyleFrame(ownerDoc: Document) {
308
+ if (!defaultStyleFrame || defaultStyleFrame.ownerDocument !== ownerDoc) {
309
+ destroyDefaultStyleFrame()
310
+ const frame = ownerDoc.createElement('iframe')
304
311
  frame.style.display = 'none'
305
- document.body.appendChild(frame)
312
+ ownerDoc.body.appendChild(frame)
306
313
  const frameDocument = assertExists(frame.contentDocument, 'frame must have a document')
307
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
308
- const foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject')
314
+ const svg = frameDocument.createElementNS('http://www.w3.org/2000/svg', 'svg')
315
+ const foreignObject = frameDocument.createElementNS(
316
+ 'http://www.w3.org/2000/svg',
317
+ 'foreignObject'
318
+ )
309
319
  svg.appendChild(foreignObject)
310
320
  frameDocument.body.appendChild(svg)
311
- defaultStyleFrame = { iframe: frame, foreignObject, document: frameDocument }
321
+ defaultStyleFrame = {
322
+ iframe: frame,
323
+ foreignObject,
324
+ document: frameDocument,
325
+ ownerDocument: ownerDoc,
326
+ }
312
327
  }
313
328
  return defaultStyleFrame
314
329
  }
315
330
 
316
331
  function destroyDefaultStyleFrame() {
317
332
  if (defaultStyleFrame) {
318
- document.body.removeChild(defaultStyleFrame.iframe)
333
+ defaultStyleFrame.iframe.remove()
319
334
  defaultStyleFrame = undefined
320
335
  }
336
+ for (const tagName in defaultStylesByTagName) {
337
+ delete defaultStylesByTagName[tagName]
338
+ }
321
339
  }
322
340
 
323
341
  const defaultStyleReadOptions: ReadStyleOpts = { defaultStyles: NO_STYLES, parentStyles: NO_STYLES }
324
- function getDefaultStylesForTagName(tagName: string) {
342
+ function getDefaultStylesForTagName(ownerDoc: Document, tagName: string) {
325
343
  let existing = defaultStylesByTagName[tagName]
326
344
  if (!existing) {
327
- const { foreignObject, document } = getDefaultStyleFrame()
345
+ const { foreignObject, document } = getDefaultStyleFrame(ownerDoc)
328
346
  const element = document.createElement(tagName)
329
347
  foreignObject.appendChild(element)
330
348
  existing = element.computedStyleMap
@@ -22,6 +22,26 @@ export function* getRenderedChildren(node: Element) {
22
22
  }
23
23
  }
24
24
 
25
+ /** @internal */
26
+ export function getOwnerWindow(
27
+ nodeOrDocument: Node | Document | null | undefined
28
+ ): Window & typeof globalThis {
29
+ if (!nodeOrDocument) return globalThis as Window & typeof globalThis
30
+ const doc = isDocument(nodeOrDocument) ? nodeOrDocument : nodeOrDocument.ownerDocument
31
+ return (doc?.defaultView ?? globalThis) as Window & typeof globalThis
32
+ }
33
+
34
+ /** @internal */
35
+ export function getOwnerDocument(nodeOrDocument: Node | Document | null | undefined): Document {
36
+ if (!nodeOrDocument) return globalThis.document
37
+ if (isDocument(nodeOrDocument)) return nodeOrDocument
38
+ return nodeOrDocument.ownerDocument ?? globalThis.document
39
+ }
40
+
41
+ function isDocument(node: Node | Document): node is Document {
42
+ return node.nodeType === Node.DOCUMENT_NODE
43
+ }
44
+
25
45
  function getWindow(node: Node) {
26
46
  return node.ownerDocument?.defaultView ?? globalThis
27
47
  }
@@ -1,5 +1,5 @@
1
1
  import { MediaHelpers } from '@tldraw/utils'
2
- import { getRenderedChildren } from './domUtils'
2
+ import { getOwnerWindow, getRenderedChildren } from './domUtils'
3
3
  import { resourceToDataUrl } from './fetchCache'
4
4
 
5
5
  function copyAttrs(source: Element, target: Element) {
@@ -14,8 +14,12 @@ function replace(original: HTMLElement, replacement: HTMLElement) {
14
14
  return replacement
15
15
  }
16
16
 
17
- async function createImage(dataUrl: string | null, cloneAttributesFrom?: HTMLElement) {
18
- const image = document.createElement('img')
17
+ async function createImage(
18
+ doc: Document,
19
+ dataUrl: string | null,
20
+ cloneAttributesFrom?: HTMLElement
21
+ ) {
22
+ const image = doc.createElement('img')
19
23
 
20
24
  if (cloneAttributesFrom) {
21
25
  copyAttrs(cloneAttributesFrom, image)
@@ -34,52 +38,54 @@ async function createImage(dataUrl: string | null, cloneAttributesFrom?: HTMLEle
34
38
  }
35
39
 
36
40
  async function getCanvasReplacement(canvas: HTMLCanvasElement) {
41
+ const doc = canvas.ownerDocument
37
42
  try {
38
43
  const dataURL = canvas.toDataURL()
39
- return await createImage(dataURL, canvas)
44
+ return await createImage(doc, dataURL, canvas)
40
45
  } catch {
41
- return await createImage(null, canvas)
46
+ return await createImage(doc, null, canvas)
42
47
  }
43
48
  }
44
49
 
45
50
  async function getVideoReplacement(video: HTMLVideoElement) {
51
+ const doc = video.ownerDocument
46
52
  try {
47
53
  const dataUrl = await MediaHelpers.getVideoFrameAsDataUrl(video)
48
- return createImage(dataUrl, video)
54
+ return createImage(doc, dataUrl, video)
49
55
  } catch (err) {
50
56
  console.error('Could not get video frame', err)
51
57
  }
52
58
 
53
59
  if (video.poster) {
54
60
  const dataUrl = await resourceToDataUrl(video.poster)
55
- return createImage(dataUrl, video)
61
+ return createImage(doc, dataUrl, video)
56
62
  }
57
63
 
58
- return createImage(null, video)
64
+ return createImage(doc, null, video)
59
65
  }
60
66
 
61
67
  export async function embedMedia(node: HTMLElement) {
62
- if (node instanceof HTMLCanvasElement) {
68
+ const win = getOwnerWindow(node)
69
+ if (node instanceof win.HTMLCanvasElement) {
63
70
  return replace(node, await getCanvasReplacement(node))
64
- } else if (node instanceof HTMLVideoElement) {
71
+ } else if (node instanceof win.HTMLVideoElement) {
65
72
  return replace(node, await getVideoReplacement(node))
66
- } else if (node instanceof HTMLImageElement) {
73
+ } else if (node instanceof win.HTMLImageElement) {
67
74
  const src = node.currentSrc || node.src
68
75
  const dataUrl = await resourceToDataUrl(src)
69
76
  node.setAttribute('src', dataUrl ?? 'data:')
70
77
  node.setAttribute('decoding', 'sync')
71
78
  node.setAttribute('loading', 'eager')
72
79
  try {
73
- await node.decode()
80
+ await (node as HTMLImageElement).decode()
74
81
  } catch {
75
82
  // this is fine
76
83
  }
77
84
  return node
78
- } else if (node instanceof HTMLInputElement) {
79
- // if an input has a value, make sure it's serialized when we convert to svg
80
- node.setAttribute('value', node.value)
81
- } else if (node instanceof HTMLTextAreaElement) {
82
- node.textContent = node.value
85
+ } else if (node instanceof win.HTMLInputElement) {
86
+ node.setAttribute('value', (node as HTMLInputElement).value)
87
+ } else if (node instanceof win.HTMLTextAreaElement) {
88
+ node.textContent = (node as HTMLTextAreaElement).value
83
89
  }
84
90
 
85
91
  await Promise.all(
@@ -4,10 +4,11 @@ import { flushSync } from 'react-dom'
4
4
  import { createRoot } from 'react-dom/client'
5
5
  import type { Editor } from '../editor/Editor'
6
6
  import { TLSvgExportOptions } from '../editor/types/misc-types'
7
- import { SVG_EXPORT_CLASSNAME } from './FontEmbedder'
8
- import { StyleEmbedder } from './StyleEmbedder'
7
+ import { getOwnerWindow } from './domUtils'
9
8
  import { embedMedia } from './embedMedia'
9
+ import { SVG_EXPORT_CLASSNAME } from './FontEmbedder'
10
10
  import { getSvgJsx } from './getSvgJsx'
11
+ import { StyleEmbedder } from './StyleEmbedder'
11
12
 
12
13
  let idCounter = 1
13
14
 
@@ -26,7 +27,7 @@ export async function exportToSvg(
26
27
  // without this CSS and layout aren't computed correctly, which we need to make sure any
27
28
  // <foreignObject> elements have their styles and content inlined correctly.
28
29
  const container = editor.getContainer()
29
- const renderTarget = document.createElement('div')
30
+ const renderTarget = container.ownerDocument.createElement('div')
30
31
  renderTarget.className = SVG_EXPORT_CLASSNAME
31
32
  // we hide the element visually, but we don't want it to be focusable or interactive in any way either
32
33
  renderTarget.inert = true
@@ -60,7 +61,7 @@ export async function exportToSvg(
60
61
 
61
62
  // Extract the rendered SVG element from the react root
62
63
  const svg = renderTarget.firstElementChild
63
- assert(svg instanceof SVGSVGElement, 'Expected an SVG element')
64
+ assert(svg instanceof getOwnerWindow(container).SVGSVGElement, 'Expected an SVG element')
64
65
 
65
66
  // And apply any changes to <foreignObject> elements that we need to make. while we're in
66
67
  // the document, these elements work exactly as we'd expect from other dom elements - they
@@ -70,7 +71,7 @@ export async function exportToSvg(
70
71
  // apply any styles directly to the elements themselves.
71
72
  await applyChangesToForeignObjects(svg)
72
73
 
73
- return { svg, width: result.width, height: result.height }
74
+ return { svg, width: result.width, height: result.height, trimPadding: result.trimPadding }
74
75
  } finally {
75
76
  // eslint-disable-next-line no-restricted-globals
76
77
  setTimeout(() => {
@@ -95,7 +96,7 @@ async function applyChangesToForeignObjects(svg: SVGSVGElement) {
95
96
 
96
97
  try {
97
98
  // begin traversing stylesheets to find @font-face declarations we might need to embed
98
- styleEmbedder.fonts.startFindingCurrentDocumentFontFaces()
99
+ styleEmbedder.fonts.startFindingDocumentFontFaces(svg.ownerDocument)
99
100
 
100
101
  // embed any media elements in the foreignObject children. images will get converted to data
101
102
  // urls, and things like videos will be converted to images.
@@ -126,7 +127,7 @@ async function applyChangesToForeignObjects(svg: SVGSVGElement) {
126
127
 
127
128
  // add the CSS to the SVG
128
129
  if (fontCss || pseudoCss) {
129
- const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')
130
+ const style = svg.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'style')
130
131
  style.textContent = `${fontCss}\n${pseudoCss}`
131
132
  svg.prepend(style)
132
133
  }