@tldraw/editor 4.5.0-canary.fe0a82f04c21 → 4.5.0-internal.b3f4cb985cc3

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 (147) hide show
  1. package/dist-cjs/index.d.ts +24 -2
  2. package/dist-cjs/index.js +5 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +5 -4
  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 +5 -3
  11. package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
  12. package/dist-cjs/lib/config/TLSessionStateSnapshot.js +2 -1
  13. package/dist-cjs/lib/config/TLSessionStateSnapshot.js.map +2 -2
  14. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  15. package/dist-cjs/lib/editor/Editor.js +58 -18
  16. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  17. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +62 -6
  18. package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
  19. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +4 -3
  20. package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
  21. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +2 -2
  22. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  23. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  24. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +2 -3
  25. package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
  26. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  27. package/dist-cjs/lib/exports/FontEmbedder.js +9 -8
  28. package/dist-cjs/lib/exports/FontEmbedder.js.map +2 -2
  29. package/dist-cjs/lib/exports/StyleEmbedder.js +27 -15
  30. package/dist-cjs/lib/exports/StyleEmbedder.js.map +3 -3
  31. package/dist-cjs/lib/exports/domUtils.js +15 -0
  32. package/dist-cjs/lib/exports/domUtils.js.map +2 -2
  33. package/dist-cjs/lib/exports/embedMedia.js +15 -12
  34. package/dist-cjs/lib/exports/embedMedia.js.map +2 -2
  35. package/dist-cjs/lib/exports/exportToSvg.js +8 -7
  36. package/dist-cjs/lib/exports/exportToSvg.js.map +2 -2
  37. package/dist-cjs/lib/exports/getSvgAsImage.js +180 -29
  38. package/dist-cjs/lib/exports/getSvgAsImage.js.map +3 -3
  39. package/dist-cjs/lib/exports/getSvgJsx.js +21 -9
  40. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  41. package/dist-cjs/lib/globals/environment.js.map +2 -2
  42. package/dist-cjs/lib/hooks/useCanvasEvents.js +4 -4
  43. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  44. package/dist-cjs/lib/hooks/useDocumentEvents.js +13 -11
  45. package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
  46. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js +3 -3
  47. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  48. package/dist-cjs/lib/hooks/useScreenBounds.js +10 -6
  49. package/dist-cjs/lib/hooks/useScreenBounds.js.map +2 -2
  50. package/dist-cjs/lib/hooks/useViewportHeight.js +13 -11
  51. package/dist-cjs/lib/hooks/useViewportHeight.js.map +3 -3
  52. package/dist-cjs/lib/utils/browserCanvasMaxSize.js.map +2 -2
  53. package/dist-cjs/lib/utils/dom.js +8 -4
  54. package/dist-cjs/lib/utils/dom.js.map +2 -2
  55. package/dist-cjs/version.js +3 -3
  56. package/dist-cjs/version.js.map +1 -1
  57. package/dist-esm/index.d.mts +24 -2
  58. package/dist-esm/index.mjs +6 -1
  59. package/dist-esm/index.mjs.map +2 -2
  60. package/dist-esm/lib/TldrawEditor.mjs +5 -4
  61. package/dist-esm/lib/TldrawEditor.mjs.map +3 -3
  62. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs +2 -1
  63. package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs.map +2 -2
  64. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -1
  65. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  66. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +5 -3
  67. package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
  68. package/dist-esm/lib/config/TLSessionStateSnapshot.mjs +2 -1
  69. package/dist-esm/lib/config/TLSessionStateSnapshot.mjs.map +2 -2
  70. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  71. package/dist-esm/lib/editor/Editor.mjs +59 -19
  72. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  73. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +64 -6
  74. package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
  75. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +4 -3
  76. package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
  77. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +2 -2
  78. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  79. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  80. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +2 -3
  81. package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
  82. package/dist-esm/lib/exports/FontEmbedder.mjs +9 -8
  83. package/dist-esm/lib/exports/FontEmbedder.mjs.map +2 -2
  84. package/dist-esm/lib/exports/StyleEmbedder.mjs +29 -16
  85. package/dist-esm/lib/exports/StyleEmbedder.mjs.map +3 -3
  86. package/dist-esm/lib/exports/domUtils.mjs +15 -0
  87. package/dist-esm/lib/exports/domUtils.mjs.map +2 -2
  88. package/dist-esm/lib/exports/embedMedia.mjs +16 -13
  89. package/dist-esm/lib/exports/embedMedia.mjs.map +2 -2
  90. package/dist-esm/lib/exports/exportToSvg.mjs +8 -7
  91. package/dist-esm/lib/exports/exportToSvg.mjs.map +2 -2
  92. package/dist-esm/lib/exports/getSvgAsImage.mjs +180 -29
  93. package/dist-esm/lib/exports/getSvgAsImage.mjs.map +3 -3
  94. package/dist-esm/lib/exports/getSvgJsx.mjs +21 -9
  95. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  96. package/dist-esm/lib/globals/environment.mjs.map +2 -2
  97. package/dist-esm/lib/hooks/useCanvasEvents.mjs +10 -5
  98. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  99. package/dist-esm/lib/hooks/useDocumentEvents.mjs +13 -11
  100. package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
  101. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs +4 -4
  102. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  103. package/dist-esm/lib/hooks/useScreenBounds.mjs +10 -6
  104. package/dist-esm/lib/hooks/useScreenBounds.mjs.map +2 -2
  105. package/dist-esm/lib/hooks/useViewportHeight.mjs +13 -11
  106. package/dist-esm/lib/hooks/useViewportHeight.mjs.map +3 -3
  107. package/dist-esm/lib/utils/browserCanvasMaxSize.mjs.map +2 -2
  108. package/dist-esm/lib/utils/dom.mjs +8 -4
  109. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  110. package/dist-esm/version.mjs +3 -3
  111. package/dist-esm/version.mjs.map +1 -1
  112. package/package.json +7 -7
  113. package/src/index.ts +2 -0
  114. package/src/lib/TldrawEditor.tsx +6 -4
  115. package/src/lib/components/default-components/CanvasShapeIndicators.tsx +2 -1
  116. package/src/lib/components/default-components/DefaultCanvas.tsx +1 -1
  117. package/src/lib/components/default-components/DefaultErrorFallback.tsx +7 -3
  118. package/src/lib/config/TLSessionStateSnapshot.ts +3 -1
  119. package/src/lib/config/TLUserPreferences.ts +2 -0
  120. package/src/lib/editor/Editor.ts +62 -15
  121. package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +7 -6
  122. package/src/lib/editor/managers/FocusManager/FocusManager.ts +10 -7
  123. package/src/lib/editor/managers/FontManager/FontManager.test.ts +1 -0
  124. package/src/lib/editor/managers/FontManager/FontManager.ts +4 -3
  125. package/src/lib/editor/managers/TextManager/TextManager.test.ts +4 -5
  126. package/src/lib/editor/managers/TextManager/TextManager.ts +2 -2
  127. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +2 -0
  128. package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +3 -3
  129. package/src/lib/editor/types/misc-types.ts +8 -2
  130. package/src/lib/exports/FontEmbedder.ts +10 -9
  131. package/src/lib/exports/StyleEmbedder.ts +33 -15
  132. package/src/lib/exports/domUtils.ts +20 -0
  133. package/src/lib/exports/embedMedia.ts +23 -17
  134. package/src/lib/exports/exportToSvg.tsx +8 -7
  135. package/src/lib/exports/getSvgAsImage.ts +292 -31
  136. package/src/lib/exports/getSvgJsx.test.ts +103 -101
  137. package/src/lib/exports/getSvgJsx.tsx +33 -10
  138. package/src/lib/globals/environment.ts +3 -0
  139. package/src/lib/hooks/useCanvasEvents.ts +10 -7
  140. package/src/lib/hooks/useDocumentEvents.ts +16 -11
  141. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +4 -7
  142. package/src/lib/hooks/useScreenBounds.ts +10 -6
  143. package/src/lib/hooks/useViewportHeight.ts +13 -11
  144. package/src/lib/test/TestEditor.ts +1 -0
  145. package/src/lib/utils/browserCanvasMaxSize.ts +2 -0
  146. package/src/lib/utils/dom.ts +17 -8
  147. package/src/version.ts +3 -3
@@ -37,10 +37,11 @@ function iOS() {
37
37
  if (!window) return false
38
38
  return (
39
39
  ['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod'].includes(
40
- // eslint-disable-next-line @typescript-eslint/no-deprecated
40
+ // eslint-disable-next-line @typescript-eslint/no-deprecated, no-restricted-syntax
41
41
  window.navigator.platform
42
42
  ) ||
43
43
  // iPad on iOS 13 detection
44
+ // eslint-disable-next-line no-restricted-globals
44
45
  (tlenv.isDarwin && 'ontouchend' in document)
45
46
  )
46
47
  }
@@ -69,6 +70,7 @@ if (window) {
69
70
  }
70
71
  }
71
72
 
73
+ // eslint-disable-next-line no-restricted-syntax
72
74
  window?.addEventListener('beforeunload', () => {
73
75
  setInSessionStorage(tabIdKey, TAB_ID)
74
76
  })
@@ -154,7 +154,9 @@ function getRandomColor() {
154
154
 
155
155
  /** @internal */
156
156
  export function userPrefersReducedMotion() {
157
+ // eslint-disable-next-line no-restricted-syntax
157
158
  if (typeof window !== 'undefined' && window.matchMedia) {
159
+ // eslint-disable-next-line no-restricted-syntax
158
160
  return window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false
159
161
  }
160
162
 
@@ -108,8 +108,9 @@ import {
108
108
  RIGHT_MOUSE_BUTTON,
109
109
  STYLUS_ERASER_BUTTON,
110
110
  } from '../constants'
111
+ import { getOwnerWindow } from '../exports/domUtils'
111
112
  import { exportToSvg } from '../exports/exportToSvg'
112
- import { getSvgAsImage } from '../exports/getSvgAsImage'
113
+ import { getSvgAsImageWithOptions, trimSvgToContent } from '../exports/getSvgAsImage'
113
114
  import { tlmenus } from '../globals/menus'
114
115
  import { tltime } from '../globals/time'
115
116
  import { TldrawOptions, defaultTldrawOptions } from '../options'
@@ -999,6 +1000,26 @@ export class Editor extends EventEmitter<TLEventMap> {
999
1000
  */
1000
1001
  getContainer: () => HTMLElement
1001
1002
 
1003
+ /**
1004
+ * The document that the editor's container element belongs to.
1005
+ * Use this instead of the global `document` to support cross-window embedding.
1006
+ *
1007
+ * @internal
1008
+ */
1009
+ getContainerDocument(): Document {
1010
+ return this.getContainer().ownerDocument
1011
+ }
1012
+
1013
+ /**
1014
+ * The window that the editor's container element belongs to.
1015
+ * Use this instead of the global `window` to support cross-window embedding.
1016
+ *
1017
+ * @internal
1018
+ */
1019
+ getContainerWindow(): Window & typeof globalThis {
1020
+ return getOwnerWindow(this.getContainer())
1021
+ }
1022
+
1002
1023
  /**
1003
1024
  * Dispose the editor.
1004
1025
  *
@@ -2772,6 +2793,15 @@ export class Editor extends EventEmitter<TLEventMap> {
2772
2793
  return this.getCamera().z
2773
2794
  }
2774
2795
 
2796
+ /**
2797
+ * Get the scale factor used when creating or resizing shapes in dynamic size mode.
2798
+ *
2799
+ * @public
2800
+ */
2801
+ @computed getResizeScaleFactor() {
2802
+ return this.user.getIsDynamicResizeMode() ? 1 / this.getZoomLevel() : 1
2803
+ }
2804
+
2775
2805
  private _debouncedZoomLevel = atom('debounced zoom level', 1)
2776
2806
 
2777
2807
  /**
@@ -3765,13 +3795,14 @@ export class Editor extends EventEmitter<TLEventMap> {
3765
3795
  screenBounds.height = Math.max(screenBounds.height, 1)
3766
3796
  }
3767
3797
 
3798
+ const doc = this.getContainerDocument()
3768
3799
  const insets = [
3769
3800
  // top
3770
3801
  screenBounds.minY !== 0,
3771
3802
  // right
3772
- !approximately(document.body.scrollWidth, screenBounds.maxX, 1),
3803
+ !approximately(doc.body.scrollWidth, screenBounds.maxX, 1),
3773
3804
  // bottom
3774
- !approximately(document.body.scrollHeight, screenBounds.maxY, 1),
3805
+ !approximately(doc.body.scrollHeight, screenBounds.maxY, 1),
3775
3806
  // left
3776
3807
  screenBounds.minX !== 0,
3777
3808
  ]
@@ -9584,6 +9615,7 @@ export class Editor extends EventEmitter<TLEventMap> {
9584
9615
  svg: serializer.serializeToString(result.svg),
9585
9616
  width: result.width,
9586
9617
  height: result.height,
9618
+ trimPadding: result.trimPadding,
9587
9619
  }
9588
9620
  }
9589
9621
 
@@ -9607,30 +9639,45 @@ export class Editor extends EventEmitter<TLEventMap> {
9607
9639
  if (!result) throw new Error('Could not create SVG')
9608
9640
 
9609
9641
  switch (withDefaults.format) {
9610
- case 'svg':
9642
+ case 'svg': {
9643
+ let svg = result.svg
9644
+ let w = result.width
9645
+ let h = result.height
9646
+ if (result.trimPadding > 0) {
9647
+ const trimmed = await trimSvgToContent(svg, {
9648
+ width: w,
9649
+ height: h,
9650
+ trimPadding: result.trimPadding,
9651
+ scale: withDefaults.scale,
9652
+ })
9653
+ if (trimmed) {
9654
+ svg = trimmed.svg
9655
+ w = trimmed.width
9656
+ h = trimmed.height
9657
+ }
9658
+ }
9611
9659
  return {
9612
- blob: new Blob([result.svg], { type: 'image/svg+xml' }),
9613
- width: result.width,
9614
- height: result.height,
9660
+ blob: new Blob([svg], { type: 'image/svg+xml' }),
9661
+ width: w,
9662
+ height: h,
9615
9663
  }
9664
+ }
9616
9665
  case 'jpeg':
9617
9666
  case 'png':
9618
9667
  case 'webp': {
9619
- const blob = await getSvgAsImage(result.svg, {
9668
+ const imageResult = await getSvgAsImageWithOptions(result.svg, {
9620
9669
  type: withDefaults.format,
9621
9670
  quality: withDefaults.quality,
9622
9671
  pixelRatio: withDefaults.pixelRatio,
9623
9672
  width: result.width,
9624
9673
  height: result.height,
9674
+ trimPadding: result.trimPadding,
9675
+ scale: withDefaults.scale,
9625
9676
  })
9626
- if (!blob) {
9677
+ if (!imageResult) {
9627
9678
  throw new Error('Could not construct image.')
9628
9679
  }
9629
- return {
9630
- blob,
9631
- width: result.width,
9632
- height: result.height,
9633
- }
9680
+ return imageResult
9634
9681
  }
9635
9682
  default: {
9636
9683
  exhaustiveSwitchError(withDefaults.format)
@@ -10069,7 +10116,7 @@ export class Editor extends EventEmitter<TLEventMap> {
10069
10116
  to: opts?.getTarget?.(this),
10070
10117
  })
10071
10118
 
10072
- window.history.replaceState({}, document.title, url.toString())
10119
+ window.history.replaceState({}, this.getContainerDocument().title, url.toString())
10073
10120
  })
10074
10121
 
10075
10122
  const scheduleEffect = debounce((execute: () => void) => execute(), opts?.debounceMs ?? 500)
@@ -36,6 +36,12 @@ describe('FocusManager', () => {
36
36
  // Create mock dispose function
37
37
  mockDispose = vi.fn()
38
38
 
39
+ // Mock document.body event listeners
40
+ originalAddEventListener = document.body.addEventListener
41
+ originalRemoveEventListener = document.body.removeEventListener
42
+ document.body.addEventListener = vi.fn()
43
+ document.body.removeEventListener = vi.fn()
44
+
39
45
  // Mock editor
40
46
  editor = {
41
47
  sideEffects: {
@@ -44,16 +50,11 @@ describe('FocusManager', () => {
44
50
  getInstanceState: vi.fn(() => ({ isFocused: false })),
45
51
  updateInstanceState: vi.fn(),
46
52
  getContainer: vi.fn(() => mockContainer),
53
+ getContainerDocument: vi.fn(() => document),
47
54
  isIn: vi.fn(() => false),
48
55
  getSelectedShapeIds: vi.fn(() => []),
49
56
  complete: vi.fn(),
50
57
  } as any
51
-
52
- // Mock document.body event listeners
53
- originalAddEventListener = document.body.addEventListener
54
- originalRemoveEventListener = document.body.removeEventListener
55
- document.body.addEventListener = vi.fn()
56
- document.body.removeEventListener = vi.fn()
57
58
  })
58
59
 
59
60
  afterEach(() => {
@@ -1,3 +1,4 @@
1
+ import { bind } from '@tldraw/utils'
1
2
  import type { Editor } from '../../Editor'
2
3
 
3
4
  /**
@@ -31,8 +32,9 @@ export class FocusManager {
31
32
  }
32
33
  this.updateContainerClass()
33
34
 
34
- document.body.addEventListener('keydown', this.handleKeyDown.bind(this))
35
- document.body.addEventListener('mousedown', this.handleMouseDown.bind(this))
35
+ const body = editor.getContainerDocument().body
36
+ body.addEventListener('keydown', this.handleKeyDown)
37
+ body.addEventListener('mousedown', this.handleMouseDown)
36
38
  }
37
39
 
38
40
  /**
@@ -56,9 +58,9 @@ export class FocusManager {
56
58
  container.classList.add('tl-container__no-focus-ring')
57
59
  }
58
60
 
59
- private handleKeyDown(keyEvent: KeyboardEvent) {
61
+ @bind private handleKeyDown(keyEvent: KeyboardEvent) {
60
62
  const container = this.editor.getContainer()
61
- const activeEl = document.activeElement
63
+ const activeEl = container.ownerDocument.activeElement
62
64
  // Edit mode should remove the focus ring, however if the active element's
63
65
  // parent is the contextual toolbar, then allow it.
64
66
  if (this.editor.isIn('select.editing_shape') && !activeEl?.closest('.tlui-contextual-toolbar'))
@@ -69,7 +71,7 @@ export class FocusManager {
69
71
  }
70
72
  }
71
73
 
72
- private handleMouseDown() {
74
+ @bind private handleMouseDown() {
73
75
  const container = this.editor.getContainer()
74
76
  container.classList.add('tl-container__no-focus-ring')
75
77
  }
@@ -84,8 +86,9 @@ export class FocusManager {
84
86
  }
85
87
 
86
88
  dispose() {
87
- document.body.removeEventListener('keydown', this.handleKeyDown.bind(this))
88
- document.body.removeEventListener('mousedown', this.handleMouseDown.bind(this))
89
+ const body = this.editor.getContainerDocument().body
90
+ body.removeEventListener('keydown', this.handleKeyDown)
91
+ body.removeEventListener('mousedown', this.handleMouseDown)
89
92
  this.disposeSideEffectListener?.()
90
93
  }
91
94
  }
@@ -85,6 +85,7 @@ describe('FontManager', () => {
85
85
  getShapeUtil: vi.fn(() => mockShapeUtil),
86
86
  getCurrentPageShapeIds: vi.fn(() => new Set()),
87
87
  getShape: vi.fn(),
88
+ getContainerDocument: vi.fn(() => document),
88
89
  isDisposed: false,
89
90
  } as any
90
91
 
@@ -160,7 +160,7 @@ export class FontManager {
160
160
  loadingPromise: instance
161
161
  .load()
162
162
  .then(() => {
163
- document.fonts.add(instance)
163
+ this.editor.getContainerDocument().fonts.add(instance)
164
164
  this.fontStates.update(font, (s) => ({ ...s, state: 'ready' }))
165
165
  })
166
166
  .catch((err) => {
@@ -193,7 +193,8 @@ export class FontManager {
193
193
  }
194
194
 
195
195
  private findOrCreateFontFace(font: TLFontFace) {
196
- for (const existing of document.fonts) {
196
+ const fonts = this.editor.getContainerDocument().fonts
197
+ for (const existing of fonts) {
197
198
  if (
198
199
  existing.family === font.family &&
199
200
  objectMapEntries(defaultFontFaceDescriptors).every(
@@ -210,7 +211,7 @@ export class FontManager {
210
211
  display: 'swap',
211
212
  })
212
213
 
213
- document.fonts.add(instance)
214
+ fonts.add(instance)
214
215
 
215
216
  return instance
216
217
  }
@@ -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
  }
@@ -13,8 +13,10 @@ export class UserPreferencesManager {
13
13
  private readonly user: TLUser,
14
14
  private readonly inferDarkMode: boolean
15
15
  ) {
16
+ // eslint-disable-next-line no-restricted-syntax
16
17
  if (typeof window === 'undefined' || !window.matchMedia) return
17
18
 
19
+ // eslint-disable-next-line no-restricted-syntax
18
20
  const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
19
21
  if (darkModeMediaQuery?.matches) {
20
22
  this.systemColorScheme.set('dark')
@@ -107,10 +107,10 @@ export class Pointing extends StateNode {
107
107
  const delta = new Vec(w / 2, h / 2)
108
108
  const parentTransform = this.editor.getShapeParentTransform(shape)
109
109
  if (parentTransform) delta.rot(-parentTransform.rotation())
110
- let scale = 1
110
+ const scale = this.editor.getResizeScaleFactor()
111
111
 
112
- if (this.editor.user.getIsDynamicResizeMode()) {
113
- scale = 1 / this.editor.getZoomLevel()
112
+ // A scale factor of 1 means dynamic sizing is not affecting this shape.
113
+ if (scale !== 1) {
114
114
  w *= scale
115
115
  h *= scale
116
116
  delta.mul(scale)
@@ -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(