@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.
- package/dist-cjs/index.d.ts +24 -2
- package/dist-cjs/index.js +5 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +5 -4
- package/dist-cjs/lib/TldrawEditor.js.map +3 -3
- package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js +3 -2
- package/dist-cjs/lib/components/default-components/CanvasShapeIndicators.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +5 -3
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +2 -2
- package/dist-cjs/lib/config/TLSessionStateSnapshot.js +2 -1
- package/dist-cjs/lib/config/TLSessionStateSnapshot.js.map +2 -2
- package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +58 -18
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js +62 -6
- package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +4 -3
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +2 -3
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
- package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
- package/dist-cjs/lib/exports/FontEmbedder.js +9 -8
- package/dist-cjs/lib/exports/FontEmbedder.js.map +2 -2
- package/dist-cjs/lib/exports/StyleEmbedder.js +27 -15
- package/dist-cjs/lib/exports/StyleEmbedder.js.map +3 -3
- package/dist-cjs/lib/exports/domUtils.js +15 -0
- package/dist-cjs/lib/exports/domUtils.js.map +2 -2
- package/dist-cjs/lib/exports/embedMedia.js +15 -12
- package/dist-cjs/lib/exports/embedMedia.js.map +2 -2
- package/dist-cjs/lib/exports/exportToSvg.js +8 -7
- package/dist-cjs/lib/exports/exportToSvg.js.map +2 -2
- package/dist-cjs/lib/exports/getSvgAsImage.js +180 -29
- package/dist-cjs/lib/exports/getSvgAsImage.js.map +3 -3
- package/dist-cjs/lib/exports/getSvgJsx.js +21 -9
- package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
- package/dist-cjs/lib/globals/environment.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +4 -4
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useDocumentEvents.js +13 -11
- package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js +3 -3
- package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useScreenBounds.js +10 -6
- package/dist-cjs/lib/hooks/useScreenBounds.js.map +2 -2
- package/dist-cjs/lib/hooks/useViewportHeight.js +13 -11
- package/dist-cjs/lib/hooks/useViewportHeight.js.map +3 -3
- package/dist-cjs/lib/utils/browserCanvasMaxSize.js.map +2 -2
- package/dist-cjs/lib/utils/dom.js +8 -4
- package/dist-cjs/lib/utils/dom.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +24 -2
- package/dist-esm/index.mjs +6 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +5 -4
- package/dist-esm/lib/TldrawEditor.mjs.map +3 -3
- package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs +2 -1
- package/dist-esm/lib/components/default-components/CanvasShapeIndicators.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +5 -3
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +2 -2
- package/dist-esm/lib/config/TLSessionStateSnapshot.mjs +2 -1
- package/dist-esm/lib/config/TLSessionStateSnapshot.mjs.map +2 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +59 -19
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs +64 -6
- package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +4 -3
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +2 -3
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
- package/dist-esm/lib/exports/FontEmbedder.mjs +9 -8
- package/dist-esm/lib/exports/FontEmbedder.mjs.map +2 -2
- package/dist-esm/lib/exports/StyleEmbedder.mjs +29 -16
- package/dist-esm/lib/exports/StyleEmbedder.mjs.map +3 -3
- package/dist-esm/lib/exports/domUtils.mjs +15 -0
- package/dist-esm/lib/exports/domUtils.mjs.map +2 -2
- package/dist-esm/lib/exports/embedMedia.mjs +16 -13
- package/dist-esm/lib/exports/embedMedia.mjs.map +2 -2
- package/dist-esm/lib/exports/exportToSvg.mjs +8 -7
- package/dist-esm/lib/exports/exportToSvg.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgAsImage.mjs +180 -29
- package/dist-esm/lib/exports/getSvgAsImage.mjs.map +3 -3
- package/dist-esm/lib/exports/getSvgJsx.mjs +21 -9
- package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
- package/dist-esm/lib/globals/environment.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +10 -5
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useDocumentEvents.mjs +13 -11
- package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs +4 -4
- package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useScreenBounds.mjs +10 -6
- package/dist-esm/lib/hooks/useScreenBounds.mjs.map +2 -2
- package/dist-esm/lib/hooks/useViewportHeight.mjs +13 -11
- package/dist-esm/lib/hooks/useViewportHeight.mjs.map +3 -3
- package/dist-esm/lib/utils/browserCanvasMaxSize.mjs.map +2 -2
- package/dist-esm/lib/utils/dom.mjs +8 -4
- package/dist-esm/lib/utils/dom.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +2 -0
- package/src/lib/TldrawEditor.tsx +6 -4
- package/src/lib/components/default-components/CanvasShapeIndicators.tsx +2 -1
- package/src/lib/components/default-components/DefaultCanvas.tsx +1 -1
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +7 -3
- package/src/lib/config/TLSessionStateSnapshot.ts +3 -1
- package/src/lib/config/TLUserPreferences.ts +2 -0
- package/src/lib/editor/Editor.ts +62 -15
- package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +7 -6
- package/src/lib/editor/managers/FocusManager/FocusManager.ts +10 -7
- package/src/lib/editor/managers/FontManager/FontManager.test.ts +1 -0
- package/src/lib/editor/managers/FontManager/FontManager.ts +4 -3
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +4 -5
- package/src/lib/editor/managers/TextManager/TextManager.ts +2 -2
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +2 -0
- package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +3 -3
- package/src/lib/editor/types/misc-types.ts +8 -2
- package/src/lib/exports/FontEmbedder.ts +10 -9
- package/src/lib/exports/StyleEmbedder.ts +33 -15
- package/src/lib/exports/domUtils.ts +20 -0
- package/src/lib/exports/embedMedia.ts +23 -17
- package/src/lib/exports/exportToSvg.tsx +8 -7
- package/src/lib/exports/getSvgAsImage.ts +292 -31
- package/src/lib/exports/getSvgJsx.test.ts +103 -101
- package/src/lib/exports/getSvgJsx.tsx +33 -10
- package/src/lib/globals/environment.ts +3 -0
- package/src/lib/hooks/useCanvasEvents.ts +10 -7
- package/src/lib/hooks/useDocumentEvents.ts +16 -11
- package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +4 -7
- package/src/lib/hooks/useScreenBounds.ts +10 -6
- package/src/lib/hooks/useViewportHeight.ts +13 -11
- package/src/lib/test/TestEditor.ts +1 -0
- package/src/lib/utils/browserCanvasMaxSize.ts +2 -0
- package/src/lib/utils/dom.ts +17 -8
- 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
|
|
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
3803
|
+
!approximately(doc.body.scrollWidth, screenBounds.maxX, 1),
|
|
3773
3804
|
// bottom
|
|
3774
|
-
!approximately(
|
|
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([
|
|
9613
|
-
width:
|
|
9614
|
-
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
|
|
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 (!
|
|
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({},
|
|
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
|
-
|
|
35
|
-
|
|
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 =
|
|
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
|
-
|
|
88
|
-
|
|
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
|
}
|
|
@@ -160,7 +160,7 @@ export class FontManager {
|
|
|
160
160
|
loadingPromise: instance
|
|
161
161
|
.load()
|
|
162
162
|
.then(() => {
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
110
|
+
const scale = this.editor.getResizeScaleFactor()
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
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. `
|
|
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
|
-
|
|
29
|
+
startFindingDocumentFontFaces(doc: Document) {
|
|
30
30
|
assert(!this.fontFacesPromise, 'FontEmbedder already started')
|
|
31
|
-
this.fontFacesPromise =
|
|
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
|
|
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(
|
|
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 ??
|
|
108
|
-
} else if (rule instanceof CSSImportRule) {
|
|
109
|
-
const absoluteUrl = new URL(rule.href, rule.parentStyleSheet?.href ??
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
| {
|
|
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
|
-
|
|
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
|
-
|
|
312
|
+
ownerDoc.body.appendChild(frame)
|
|
306
313
|
const frameDocument = assertExists(frame.contentDocument, 'frame must have a document')
|
|
307
|
-
const svg =
|
|
308
|
-
const 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 = {
|
|
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
|
-
|
|
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(
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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(
|