@tldraw/editor 3.9.0-canary.ffd990988638 → 3.9.0-internal.7f0e15f4f7d9

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 (132) hide show
  1. package/dist-cjs/index.d.ts +228 -3
  2. package/dist-cjs/index.js +9 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/TldrawEditor.js +33 -6
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/Shape.js +7 -0
  7. package/dist-cjs/lib/components/Shape.js.map +2 -2
  8. package/dist-cjs/lib/editor/Editor.js +63 -8
  9. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  10. package/dist-cjs/lib/editor/managers/FontManager.js +167 -0
  11. package/dist-cjs/lib/editor/managers/FontManager.js.map +7 -0
  12. package/dist-cjs/lib/editor/managers/TextManager.js +23 -17
  13. package/dist-cjs/lib/editor/managers/TextManager.js.map +2 -2
  14. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +11 -0
  15. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  16. package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
  17. package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
  18. package/dist-cjs/lib/exports/FontEmbedder.js +7 -2
  19. package/dist-cjs/lib/exports/FontEmbedder.js.map +2 -2
  20. package/dist-cjs/lib/exports/StyleEmbedder.js +1 -1
  21. package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
  22. package/dist-cjs/lib/exports/exportToSvg.js +3 -2
  23. package/dist-cjs/lib/exports/exportToSvg.js.map +2 -2
  24. package/dist-cjs/lib/exports/getSvgAsImage.js +1 -1
  25. package/dist-cjs/lib/exports/getSvgAsImage.js.map +2 -2
  26. package/dist-cjs/lib/exports/getSvgJsx.js +18 -1
  27. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  28. package/dist-cjs/lib/exports/parseCss.js +1 -0
  29. package/dist-cjs/lib/exports/parseCss.js.map +2 -2
  30. package/dist-cjs/lib/globals/environment.js +3 -1
  31. package/dist-cjs/lib/globals/environment.js.map +2 -2
  32. package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -2
  33. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  34. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js +1 -1
  35. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  36. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js +48 -0
  37. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +7 -0
  38. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  39. package/dist-cjs/lib/hooks/useViewportHeight.js +56 -0
  40. package/dist-cjs/lib/hooks/useViewportHeight.js.map +7 -0
  41. package/dist-cjs/lib/license/LicenseManager.js +1 -1
  42. package/dist-cjs/lib/license/LicenseManager.js.map +2 -2
  43. package/dist-cjs/lib/options.js +2 -1
  44. package/dist-cjs/lib/options.js.map +2 -2
  45. package/dist-cjs/lib/utils/browserCanvasMaxSize.js +104 -28
  46. package/dist-cjs/lib/utils/browserCanvasMaxSize.js.map +3 -3
  47. package/dist-cjs/lib/utils/dom.js +1 -1
  48. package/dist-cjs/lib/utils/dom.js.map +2 -2
  49. package/dist-cjs/lib/utils/richText.js +46 -0
  50. package/dist-cjs/lib/utils/richText.js.map +7 -0
  51. package/dist-cjs/version.js +3 -3
  52. package/dist-cjs/version.js.map +1 -1
  53. package/dist-esm/index.d.mts +228 -3
  54. package/dist-esm/index.mjs +13 -1
  55. package/dist-esm/index.mjs.map +2 -2
  56. package/dist-esm/lib/TldrawEditor.mjs +34 -7
  57. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  58. package/dist-esm/lib/components/Shape.mjs +8 -1
  59. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  60. package/dist-esm/lib/editor/Editor.mjs +71 -9
  61. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  62. package/dist-esm/lib/editor/managers/FontManager.mjs +153 -0
  63. package/dist-esm/lib/editor/managers/FontManager.mjs.map +7 -0
  64. package/dist-esm/lib/editor/managers/TextManager.mjs +23 -17
  65. package/dist-esm/lib/editor/managers/TextManager.mjs.map +2 -2
  66. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +11 -0
  67. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  68. package/dist-esm/lib/exports/FontEmbedder.mjs +7 -2
  69. package/dist-esm/lib/exports/FontEmbedder.mjs.map +2 -2
  70. package/dist-esm/lib/exports/StyleEmbedder.mjs +1 -1
  71. package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
  72. package/dist-esm/lib/exports/exportToSvg.mjs +3 -2
  73. package/dist-esm/lib/exports/exportToSvg.mjs.map +2 -2
  74. package/dist-esm/lib/exports/getSvgAsImage.mjs +1 -1
  75. package/dist-esm/lib/exports/getSvgAsImage.mjs.map +2 -2
  76. package/dist-esm/lib/exports/getSvgJsx.mjs +19 -2
  77. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  78. package/dist-esm/lib/exports/parseCss.mjs +1 -0
  79. package/dist-esm/lib/exports/parseCss.mjs.map +2 -2
  80. package/dist-esm/lib/globals/environment.mjs +3 -1
  81. package/dist-esm/lib/globals/environment.mjs.map +2 -2
  82. package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -2
  83. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  84. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs +1 -1
  85. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  86. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs +28 -0
  87. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +7 -0
  88. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  89. package/dist-esm/lib/hooks/useViewportHeight.mjs +36 -0
  90. package/dist-esm/lib/hooks/useViewportHeight.mjs.map +7 -0
  91. package/dist-esm/lib/license/LicenseManager.mjs +1 -1
  92. package/dist-esm/lib/license/LicenseManager.mjs.map +2 -2
  93. package/dist-esm/lib/options.mjs +2 -1
  94. package/dist-esm/lib/options.mjs.map +2 -2
  95. package/dist-esm/lib/utils/browserCanvasMaxSize.mjs +104 -18
  96. package/dist-esm/lib/utils/browserCanvasMaxSize.mjs.map +2 -2
  97. package/dist-esm/lib/utils/dom.mjs +1 -1
  98. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  99. package/dist-esm/lib/utils/richText.mjs +26 -0
  100. package/dist-esm/lib/utils/richText.mjs.map +7 -0
  101. package/dist-esm/version.mjs +3 -3
  102. package/dist-esm/version.mjs.map +1 -1
  103. package/editor.css +127 -13
  104. package/package.json +10 -9
  105. package/src/index.ts +15 -0
  106. package/src/lib/TldrawEditor.tsx +52 -4
  107. package/src/lib/components/Shape.tsx +9 -1
  108. package/src/lib/editor/Editor.ts +91 -7
  109. package/src/lib/editor/managers/FontManager.ts +252 -0
  110. package/src/lib/editor/managers/TextManager.ts +42 -17
  111. package/src/lib/editor/shapes/ShapeUtil.ts +13 -0
  112. package/src/lib/editor/types/emit-types.ts +1 -0
  113. package/src/lib/editor/types/external-content.ts +1 -0
  114. package/src/lib/exports/FontEmbedder.ts +13 -1
  115. package/src/lib/exports/StyleEmbedder.ts +1 -1
  116. package/src/lib/exports/exportToSvg.tsx +4 -3
  117. package/src/lib/exports/getSvgAsImage.ts +1 -1
  118. package/src/lib/exports/getSvgJsx.tsx +22 -2
  119. package/src/lib/exports/parseCss.ts +1 -0
  120. package/src/lib/globals/environment.ts +3 -0
  121. package/src/lib/hooks/useCanvasEvents.ts +2 -1
  122. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +1 -0
  123. package/src/lib/hooks/usePassThroughMouseOverEvents.ts +29 -0
  124. package/src/lib/hooks/usePassThroughWheelEvents.ts +0 -1
  125. package/src/lib/hooks/useViewportHeight.ts +37 -0
  126. package/src/lib/license/LicenseManager.test.ts +16 -13
  127. package/src/lib/license/LicenseManager.ts +2 -2
  128. package/src/lib/options.ts +7 -0
  129. package/src/lib/utils/browserCanvasMaxSize.ts +121 -21
  130. package/src/lib/utils/dom.ts +1 -1
  131. package/src/lib/utils/richText.ts +72 -0
  132. package/src/version.ts +3 -3
@@ -0,0 +1,252 @@
1
+ import { atom, Atom, EMPTY_ARRAY, transact } from '@tldraw/state'
2
+ import { TLShape, TLShapeId } from '@tldraw/tlschema'
3
+ import {
4
+ areArraysShallowEqual,
5
+ compact,
6
+ FileHelpers,
7
+ mapObjectMapValues,
8
+ objectMapEntries,
9
+ } from '@tldraw/utils'
10
+ import { Editor } from '../Editor'
11
+
12
+ /**
13
+ * Represents the `src` property of a {@link TLFontFace}.
14
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/src | `src`} for details of the properties here.
15
+ * @public
16
+ */
17
+ export interface TLFontFaceSource {
18
+ /**
19
+ * A URL from which to load the font. If the value here is a key in
20
+ * {@link tldraw#TLEditorAssetUrls.fonts}, the value from there will be used instead.
21
+ */
22
+ url: string
23
+ format?: string
24
+ tech?: string
25
+ }
26
+
27
+ /**
28
+ * A font face that can be used in the editor. The properties of this are largely the same as the
29
+ * ones in the
30
+ * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face | css `@font-face` rule}.
31
+ * @public
32
+ */
33
+ export interface TLFontFace {
34
+ /**
35
+ * How this font can be referred to in CSS.
36
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-family | `font-family`}.
37
+ */
38
+ readonly family: string
39
+ /**
40
+ * The source of the font. This
41
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/src | `src`}.
42
+ */
43
+ readonly src: TLFontFaceSource
44
+ /**
45
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/ascent-override | `ascent-override`}.
46
+ */
47
+ readonly ascentOverride?: string
48
+ /**
49
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/descent-override | `descent-override`}.
50
+ */
51
+ readonly descentOverride?: string
52
+ /**
53
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-stretch | `font-stretch`}.
54
+ */
55
+ readonly stretch?: string
56
+ /**
57
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-style | `font-style`}.
58
+ */
59
+ readonly style?: string
60
+ /**
61
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-weight | `font-weight`}.
62
+ */
63
+ readonly weight?: string
64
+ /**
65
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-feature-settings | `font-feature-settings`}.
66
+ */
67
+ readonly featureSettings?: string
68
+ /**
69
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/line-gap-override | `line-gap-override`}.
70
+ */
71
+ readonly lineGapOverride?: string
72
+ /**
73
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range | `unicode-range`}.
74
+ */
75
+ readonly unicodeRange?: string
76
+ }
77
+
78
+ interface FontState {
79
+ readonly state: 'loading' | 'ready' | 'error'
80
+ readonly instance: FontFace
81
+ readonly loadingPromise: Promise<void>
82
+ }
83
+
84
+ /** @public */
85
+ export class FontManager {
86
+ constructor(
87
+ private readonly editor: Editor,
88
+ private readonly assetUrls?: { [key: string]: string | undefined }
89
+ ) {
90
+ this.shapeFontFacesCache = editor.store.createComputedCache(
91
+ 'shape font faces',
92
+ (shape: TLShape) => {
93
+ const shapeUtil = this.editor.getShapeUtil(shape)
94
+ return shapeUtil.getFontFaces(shape)
95
+ },
96
+ { areResultsEqual: areArraysShallowEqual }
97
+ )
98
+
99
+ this.shapeFontLoadStateCache = editor.store.createComputedCache(
100
+ 'shape font load state',
101
+ (shape: TLShape) => {
102
+ const states = this.getShapeFontFaces(shape).map((face) => this.getFontState(face))
103
+ return states
104
+ },
105
+ { areResultsEqual: areArraysShallowEqual }
106
+ )
107
+ }
108
+
109
+ private readonly shapeFontFacesCache
110
+ private readonly shapeFontLoadStateCache
111
+
112
+ getShapeFontFaces(shape: TLShape | TLShapeId): TLFontFace[] {
113
+ const shapeId = typeof shape === 'string' ? shape : shape.id
114
+ return this.shapeFontFacesCache.get(shapeId) ?? EMPTY_ARRAY
115
+ }
116
+
117
+ trackFontsForShape(shape: TLShape | TLShapeId) {
118
+ const shapeId = typeof shape === 'string' ? shape : shape.id
119
+ this.shapeFontLoadStateCache.get(shapeId)
120
+ }
121
+
122
+ async loadRequiredFontsForCurrentPage(limit = Infinity) {
123
+ const neededFonts = new Set<TLFontFace>()
124
+ for (const shapeId of this.editor.getCurrentPageShapeIds()) {
125
+ for (const font of this.getShapeFontFaces(this.editor.getShape(shapeId)!)) {
126
+ neededFonts.add(font)
127
+ }
128
+ }
129
+
130
+ if (neededFonts.size > limit) {
131
+ return
132
+ }
133
+
134
+ const promises = Array.from(neededFonts, (font) => this.ensureFontIsLoaded(font))
135
+ await Promise.all(promises)
136
+ }
137
+
138
+ private readonly fontStates = atom<ReadonlyMap<TLFontFace, Atom<FontState>>>(
139
+ 'font states',
140
+ new Map()
141
+ )
142
+ private getFontState(font: TLFontFace): FontState | null {
143
+ return this.fontStates.get().get(font)?.get() ?? null
144
+ }
145
+
146
+ ensureFontIsLoaded(font: TLFontFace): Promise<void> {
147
+ const state = this.getFontState(font)
148
+ if (state) return state.loadingPromise
149
+
150
+ const instance = this.findOrCreateFontFace(font)
151
+ const stateAtom = atom<FontState>('font state', {
152
+ state: 'loading',
153
+ instance,
154
+ loadingPromise: instance
155
+ .load()
156
+ .then(() => {
157
+ document.fonts.add(instance)
158
+ stateAtom.update((s) => ({ ...s, state: 'ready' }))
159
+ })
160
+ .catch((err) => {
161
+ console.error(err)
162
+ stateAtom.update((s) => ({ ...s, state: 'error' }))
163
+ }),
164
+ })
165
+ this.fontStates.update((map) => {
166
+ const newMap = new Map(map)
167
+ newMap.set(font, stateAtom)
168
+ return newMap
169
+ })
170
+
171
+ return stateAtom.get().loadingPromise
172
+ }
173
+
174
+ private fontsToLoad = new Set<TLFontFace>()
175
+ requestFonts(fonts: TLFontFace[]) {
176
+ if (!this.fontsToLoad.size) {
177
+ queueMicrotask(() => {
178
+ if (this.editor.isDisposed) return
179
+ const toLoad = this.fontsToLoad
180
+ this.fontsToLoad = new Set()
181
+ transact(() => {
182
+ for (const font of toLoad) {
183
+ this.ensureFontIsLoaded(font)
184
+ }
185
+ })
186
+ })
187
+ }
188
+ for (const font of fonts) {
189
+ this.fontsToLoad.add(font)
190
+ }
191
+ }
192
+
193
+ private findOrCreateFontFace(font: TLFontFace) {
194
+ for (const existing of document.fonts) {
195
+ if (
196
+ existing.family === font.family &&
197
+ objectMapEntries(defaultFontFaceDescriptors).every(
198
+ ([key, defaultValue]) => existing[key] === (font[key] ?? defaultValue)
199
+ )
200
+ ) {
201
+ return existing
202
+ }
203
+ }
204
+
205
+ const url = this.assetUrls?.[font.src.url] ?? font.src.url
206
+ const instance = new FontFace(font.family, `url(${JSON.stringify(url)})`, {
207
+ ...mapObjectMapValues(defaultFontFaceDescriptors, (key) => font[key]),
208
+ display: 'swap',
209
+ })
210
+
211
+ document.fonts.add(instance)
212
+
213
+ return instance
214
+ }
215
+
216
+ async toEmbeddedCssDeclaration(font: TLFontFace) {
217
+ const url = this.assetUrls?.[font.src.url] ?? font.src.url
218
+ const dataUrl = await FileHelpers.urlToDataUrl(url)
219
+
220
+ const src = compact([
221
+ `url("${dataUrl}")`,
222
+ font.src.format ? `format(${font.src.format})` : null,
223
+ font.src.tech ? `tech(${font.src.tech})` : null,
224
+ ]).join(' ')
225
+ return compact([
226
+ `@font-face {`,
227
+ ` font-family: ${font.family};`,
228
+ ` src: ${src};`,
229
+ font.ascentOverride ? ` ascent-override: ${font.ascentOverride};` : null,
230
+ font.descentOverride ? ` descent-override: ${font.descentOverride};` : null,
231
+ font.stretch ? ` font-stretch: ${font.stretch};` : null,
232
+ font.style ? ` font-style: ${font.style};` : null,
233
+ font.weight ? ` font-weight: ${font.weight};` : null,
234
+ font.featureSettings ? ` font-feature-settings: ${font.featureSettings};` : null,
235
+ font.lineGapOverride ? ` line-gap-override: ${font.lineGapOverride};` : null,
236
+ font.unicodeRange ? ` unicode-range: ${font.unicodeRange};` : null,
237
+ `}`,
238
+ ]).join('\n')
239
+ }
240
+ }
241
+
242
+ // From https://drafts.csswg.org/css-font-loading/#fontface-interface
243
+ const defaultFontFaceDescriptors = {
244
+ style: 'normal',
245
+ weight: 'normal',
246
+ stretch: 'normal',
247
+ unicodeRange: 'U+0-10FFFF',
248
+ featureSettings: 'normal',
249
+ ascentOverride: 'normal',
250
+ descentOverride: 'normal',
251
+ lineGapOverride: 'normal',
252
+ }
@@ -65,32 +65,57 @@ export class TextManager {
65
65
  padding: string
66
66
  disableOverflowWrapBreaking?: boolean
67
67
  }
68
+ ): BoxModel & { scrollWidth: number } {
69
+ const div = document.createElement('div')
70
+ div.textContent = normalizeTextForDom(textToMeasure)
71
+ return this.measureHtml(div.innerHTML, opts)
72
+ }
73
+
74
+ measureHtml(
75
+ html: string,
76
+ opts: {
77
+ fontStyle: string
78
+ fontWeight: string
79
+ fontFamily: string
80
+ fontSize: number
81
+ lineHeight: number
82
+ /**
83
+ * When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth
84
+ * is null, the text will be measured without wrapping, but explicit line breaks and
85
+ * space are preserved.
86
+ */
87
+ maxWidth: null | number
88
+ minWidth?: null | number
89
+ padding: string
90
+ disableOverflowWrapBreaking?: boolean
91
+ }
68
92
  ): BoxModel & { scrollWidth: number } {
69
93
  // Duplicate our base element; we don't need to clone deep
70
- const elm = this.baseElem.cloneNode() as HTMLDivElement
71
- this.editor.getContainer().appendChild(elm)
94
+ const wrapperElm = this.baseElem.cloneNode() as HTMLDivElement
95
+ this.editor.getContainer().appendChild(wrapperElm)
96
+ wrapperElm.innerHTML = html
97
+ this.baseElem.insertAdjacentElement('afterend', wrapperElm)
72
98
 
73
- elm.setAttribute('dir', 'auto')
99
+ wrapperElm.setAttribute('dir', 'auto')
74
100
  // N.B. This property, while discouraged ("intended for Document Type Definition (DTD) designers")
75
101
  // is necessary for ensuring correct mixed RTL/LTR behavior when exporting SVGs.
76
- elm.style.setProperty('unicode-bidi', 'plaintext')
77
- elm.style.setProperty('font-family', opts.fontFamily)
78
- elm.style.setProperty('font-style', opts.fontStyle)
79
- elm.style.setProperty('font-weight', opts.fontWeight)
80
- elm.style.setProperty('font-size', opts.fontSize + 'px')
81
- elm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px')
82
- elm.style.setProperty('max-width', opts.maxWidth === null ? null : opts.maxWidth + 'px')
83
- elm.style.setProperty('min-width', opts.minWidth === null ? null : opts.minWidth + 'px')
84
- elm.style.setProperty('padding', opts.padding)
85
- elm.style.setProperty(
102
+ wrapperElm.style.setProperty('unicode-bidi', 'plaintext')
103
+ wrapperElm.style.setProperty('font-family', opts.fontFamily)
104
+ wrapperElm.style.setProperty('font-style', opts.fontStyle)
105
+ wrapperElm.style.setProperty('font-weight', opts.fontWeight)
106
+ wrapperElm.style.setProperty('font-size', opts.fontSize + 'px')
107
+ wrapperElm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px')
108
+ wrapperElm.style.setProperty('max-width', opts.maxWidth === null ? null : opts.maxWidth + 'px')
109
+ wrapperElm.style.setProperty('min-width', opts.minWidth === null ? null : opts.minWidth + 'px')
110
+ wrapperElm.style.setProperty('padding', opts.padding)
111
+ wrapperElm.style.setProperty(
86
112
  'overflow-wrap',
87
113
  opts.disableOverflowWrapBreaking ? 'normal' : 'break-word'
88
114
  )
89
115
 
90
- elm.textContent = normalizeTextForDom(textToMeasure)
91
- const scrollWidth = elm.scrollWidth
92
- const rect = elm.getBoundingClientRect()
93
- elm.remove()
116
+ const scrollWidth = wrapperElm.scrollWidth
117
+ const rect = wrapperElm.getBoundingClientRect()
118
+ wrapperElm.remove()
94
119
 
95
120
  return {
96
121
  x: 0,
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ import { EMPTY_ARRAY } from '@tldraw/state'
2
3
  import { LegacyMigrations, MigrationSequence } from '@tldraw/store'
3
4
  import {
4
5
  RecordProps,
@@ -14,6 +15,7 @@ import { Box, SelectionHandle } from '../../primitives/Box'
14
15
  import { Vec } from '../../primitives/Vec'
15
16
  import { Geometry2d } from '../../primitives/geometry/Geometry2d'
16
17
  import type { Editor } from '../Editor'
18
+ import { TLFontFace } from '../managers/FontManager'
17
19
  import { BoundsSnapGeometry } from '../managers/SnapManager/BoundsSnaps'
18
20
  import { HandleSnapGeometry } from '../managers/SnapManager/HandleSnaps'
19
21
  import { SvgExportContext } from '../types/SvgExportContext'
@@ -148,6 +150,17 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
148
150
  */
149
151
  abstract indicator(shape: Shape): any
150
152
 
153
+ /**
154
+ * Get the font faces that should be rendered in the document in order for this shape to render
155
+ * correctly.
156
+ *
157
+ * @param shape - The shape.
158
+ * @public
159
+ */
160
+ getFontFaces(shape: Shape): TLFontFace[] {
161
+ return EMPTY_ARRAY
162
+ }
163
+
151
164
  /**
152
165
  * Whether the shape can be snapped to by another shape.
153
166
  *
@@ -17,6 +17,7 @@ export interface TLEventMap {
17
17
  tick: [number]
18
18
  frame: [number]
19
19
  'select-all-text': [{ shapeId: TLShapeId }]
20
+ 'place-caret': [{ shapeId: TLShapeId; point: { x: number; y: number } }]
20
21
  }
21
22
 
22
23
  /** @public */
@@ -45,6 +45,7 @@ export interface TLBaseExternalContent {
45
45
  export interface TLTextExternalContent extends TLBaseExternalContent {
46
46
  type: 'text'
47
47
  text: string
48
+ html?: string
48
49
  }
49
50
 
50
51
  /** @public */
@@ -2,6 +2,8 @@ import { assert, bind, compact } from '@tldraw/utils'
2
2
  import { fetchCache, resourceToDataUrl } from './fetchCache'
3
3
  import { ParsedFontFace, parseCss, parseCssFontFaces, parseCssFontFamilyValue } from './parseCss'
4
4
 
5
+ export const SVG_EXPORT_CLASSNAME = 'tldraw-svg-export'
6
+
5
7
  /**
6
8
  * Because SVGs cannot refer to external CSS/font resources, any web fonts used in the SVG must be
7
9
  * embedded as data URLs in inlined @font-face declarations. This class is responsible for
@@ -81,7 +83,17 @@ export class FontEmbedder {
81
83
  async function getCurrentDocumentFontFaces() {
82
84
  const fontFaces: (ParsedFontFace[] | Promise<ParsedFontFace[] | null>)[] = []
83
85
 
84
- for (const styleSheet of document.styleSheets) {
86
+ // In exportToSvg we add the exported node to the DOM temporarily.
87
+ // Because of this, and because we do a setTimeout to delay removing that node from the
88
+ // DOM, when looking at document.styleSheets the number of nodes and stylesheets
89
+ // can grow unbounded (especially when using "Debug svg" and moving shapes around).
90
+ // To avoid this, we filter out the stylesheets that are part of the SVG export.
91
+ const styleSheetsWithoutSvgExports = Array.from(document.styleSheets).filter(
92
+ (styleSheet) =>
93
+ !(styleSheet.ownerNode as HTMLElement | null)?.closest(`.${SVG_EXPORT_CLASSNAME}`)
94
+ )
95
+
96
+ for (const styleSheet of styleSheetsWithoutSvgExports) {
85
97
  let cssRules
86
98
  try {
87
99
  cssRules = styleSheet.cssRules
@@ -242,7 +242,7 @@ function styleFromComputedStyle(
242
242
  { defaultStyles, parentStyles }: ReadStyleOpts
243
243
  ) {
244
244
  const styles: Record<string, string> = {}
245
- for (const property of style) {
245
+ for (const [property, _] of Object.entries(style)) {
246
246
  if (!shouldIncludeCssProperty(property)) continue
247
247
 
248
248
  const value = style.getPropertyValue(property)
@@ -4,6 +4,7 @@ import { flushSync } from 'react-dom'
4
4
  import { createRoot } from 'react-dom/client'
5
5
  import { Editor } from '../editor/Editor'
6
6
  import { TLSvgExportOptions } from '../editor/types/misc-types'
7
+ import { SVG_EXPORT_CLASSNAME } from './FontEmbedder'
7
8
  import { StyleEmbedder } from './StyleEmbedder'
8
9
  import { embedMedia } from './embedMedia'
9
10
  import { getSvgJsx } from './getSvgJsx'
@@ -26,7 +27,7 @@ export async function exportToSvg(
26
27
  // <foreignObject> elements have their styles and content inlined correctly.
27
28
  const container = editor.getContainer()
28
29
  const renderTarget = document.createElement('div')
29
- renderTarget.className = 'tldraw-svg-export'
30
+ renderTarget.className = SVG_EXPORT_CLASSNAME
30
31
  // we hide the element visually, but we don't want it to be focusable or interactive in any way either
31
32
  renderTarget.inert = true
32
33
  renderTarget.tabIndex = -1
@@ -83,9 +84,9 @@ export async function exportToSvg(
83
84
 
84
85
  async function applyChangesToForeignObjects(svg: SVGSVGElement) {
85
86
  // If any shapes have their own <foreignObject> elements, we don't want to mess with them. Our
86
- // ones that we need to embed will have a class of `tl-shape-foreign-object`.
87
+ // ones that we need to embed will have a class of `tl-export-embed-styles`.
87
88
  const foreignObjectChildren = [
88
- ...svg.querySelectorAll('foreignObject.tl-shape-foreign-object > *'),
89
+ ...svg.querySelectorAll('foreignObject.tl-export-embed-styles > *'),
89
90
  ]
90
91
  if (!foreignObjectChildren.length) return
91
92
 
@@ -16,7 +16,7 @@ export async function getSvgAsImage(
16
16
  ) {
17
17
  const { type, width, height, quality = 1, pixelRatio = 2 } = options
18
18
 
19
- let [clampedWidth, clampedHeight] = await clampToBrowserMaxCanvasSize(
19
+ let [clampedWidth, clampedHeight] = clampToBrowserMaxCanvasSize(
20
20
  width * pixelRatio,
21
21
  height * pixelRatio
22
22
  )
@@ -6,7 +6,7 @@ import {
6
6
  TLShapeId,
7
7
  getDefaultColorTheme,
8
8
  } from '@tldraw/tlschema'
9
- import { hasOwnProperty, promiseWithResolve } from '@tldraw/utils'
9
+ import { hasOwnProperty, promiseWithResolve, uniqueId } from '@tldraw/utils'
10
10
  import {
11
11
  ComponentType,
12
12
  Fragment,
@@ -21,6 +21,7 @@ import { flushSync } from 'react-dom'
21
21
  import { ErrorBoundary } from '../components/ErrorBoundary'
22
22
  import { InnerShape, InnerShapeBackground } from '../components/Shape'
23
23
  import { Editor, TLRenderingShape } from '../editor/Editor'
24
+ import { TLFontFace } from '../editor/managers/FontManager'
24
25
  import { ShapeUtil } from '../editor/shapes/ShapeUtil'
25
26
  import {
26
27
  SvgExportContext,
@@ -339,6 +340,25 @@ function SvgExport({
339
340
  })()
340
341
  }, [bbox, editor, exportContext, masksId, renderingShapes, singleFrameShapeId, stateAtom])
341
342
 
343
+ useEffect(() => {
344
+ const fontsInUse = new Set<TLFontFace>()
345
+ for (const { id } of renderingShapes) {
346
+ for (const font of editor.fonts.getShapeFontFaces(id)) {
347
+ fontsInUse.add(font)
348
+ }
349
+ }
350
+
351
+ for (const font of fontsInUse) {
352
+ addExportDef({
353
+ key: uniqueId(),
354
+ getElement: async () => {
355
+ const declaration = await editor.fonts.toEmbeddedCssDeclaration(font)
356
+ return <style>{declaration}</style>
357
+ },
358
+ })
359
+ }
360
+ }, [editor, renderingShapes, addExportDef])
361
+
342
362
  useEffect(() => {
343
363
  if (shapeElements === null) return
344
364
  onMount()
@@ -407,7 +427,7 @@ function ForeignObjectShape({
407
427
  y={bbox.minY}
408
428
  width={bbox.w}
409
429
  height={bbox.h}
410
- className="tl-shape-foreign-object"
430
+ className="tl-shape-foreign-object tl-export-embed-styles"
411
431
  >
412
432
  <div
413
433
  className={className}
@@ -134,6 +134,7 @@ const inheritedProperties = new Set([
134
134
  'list-style-type',
135
135
  'list-style',
136
136
  'orphans',
137
+ 'overflow-wrap',
137
138
  'quotes',
138
139
  'tab-size',
139
140
  'text-align',
@@ -11,6 +11,7 @@ const tlenv = {
11
11
  isAndroid: false,
12
12
  isWebview: false,
13
13
  isDarwin: false,
14
+ hasCanvasSupport: false,
14
15
  }
15
16
 
16
17
  if (typeof window !== 'undefined' && 'navigator' in window) {
@@ -20,6 +21,8 @@ if (typeof window !== 'undefined' && 'navigator' in window) {
20
21
  tlenv.isFirefox = /firefox/i.test(navigator.userAgent)
21
22
  tlenv.isAndroid = /android/i.test(navigator.userAgent)
22
23
  tlenv.isDarwin = window.navigator.userAgent.toLowerCase().indexOf('mac') > -1
24
+ tlenv.hasCanvasSupport =
25
+ typeof window !== 'undefined' && 'Promise' in window && 'HTMLCanvasElement' in window
23
26
  }
24
27
 
25
28
  export { tlenv }
@@ -100,8 +100,9 @@ export function useCanvasEvents() {
100
100
  if (
101
101
  e.target.tagName !== 'A' &&
102
102
  e.target.tagName !== 'TEXTAREA' &&
103
+ e.target.isContentEditable &&
103
104
  // When in EditingShape state, we are actually clicking on a 'DIV'
104
- // not A/TEXTAREA element yet. So, to preserve cursor position
105
+ // not A/TEXTAREA/contenteditable element yet. So, to preserve cursor position
105
106
  // for edit mode on mobile we need to not preventDefault.
106
107
  // TODO: Find out if we still need this preventDefault in general though.
107
108
  !(editor.getEditingShape() && e.target.className.includes('tl-text-content'))
@@ -25,6 +25,7 @@ export function useFixSafariDoubleTapZoomPencilEvents(ref: React.RefObject<HTMLE
25
25
  // Allow events to propagate if the app is editing a shape, or if the event is occurring in a text area or input
26
26
  if (
27
27
  IGNORED_TAGS.includes((target as Element).tagName?.toLocaleLowerCase()) ||
28
+ (target as HTMLElement).isContentEditable ||
28
29
  editor.isIn('select.editing_shape')
29
30
  ) {
30
31
  return
@@ -0,0 +1,29 @@
1
+ import { RefObject, useEffect } from 'react'
2
+ import { preventDefault } from '../utils/dom'
3
+ import { useContainer } from './useContainer'
4
+
5
+ /** @public */
6
+ export function usePassThroughMouseOverEvents(ref: RefObject<HTMLElement>) {
7
+ if (!ref) throw Error('usePassThroughWheelEvents must be passed a ref')
8
+ const container = useContainer()
9
+
10
+ useEffect(() => {
11
+ function onMouseOver(e: MouseEvent) {
12
+ if ((e as any).isSpecialRedispatchedEvent) return
13
+ preventDefault(e)
14
+ const cvs = container.querySelector('.tl-canvas')
15
+ if (!cvs) return
16
+ const newEvent = new PointerEvent(e.type, e as any)
17
+ ;(newEvent as any).isSpecialRedispatchedEvent = true
18
+ cvs.dispatchEvent(newEvent)
19
+ }
20
+
21
+ const elm = ref.current
22
+ if (!elm) return
23
+
24
+ elm.addEventListener('mouseover', onMouseOver, { passive: false })
25
+ return () => {
26
+ elm.removeEventListener('mouseover', onMouseOver)
27
+ }
28
+ }, [container, ref])
29
+ }
@@ -5,7 +5,6 @@ import { useContainer } from './useContainer'
5
5
  /** @public */
6
6
  export function usePassThroughWheelEvents(ref: RefObject<HTMLElement>) {
7
7
  if (!ref) throw Error('usePassThroughWheelEvents must be passed a ref')
8
-
9
8
  const container = useContainer()
10
9
 
11
10
  useEffect(() => {
@@ -0,0 +1,37 @@
1
+ import { useLayoutEffect, useState } from 'react'
2
+
3
+ /*!
4
+ * BSD License: https://github.com/outline/rich-markdown-editor/blob/main/LICENSE
5
+ * Copyright (c) 2020 General Outline, Inc (https://www.getoutline.com/) and individual contributors.
6
+ *
7
+ * Returns the height of the viewport.
8
+ * This is mainly to account for virtual keyboards on mobile devices.
9
+ *
10
+ * N.B. On iOS, you have to take into account the offsetTop as well so that you get an accurate position
11
+ * while using the virtual keyboard.
12
+ */
13
+ /** @public */
14
+ export function useViewportHeight(): number {
15
+ const visualViewport = window.visualViewport
16
+ const [height, setHeight] = useState<number>(() =>
17
+ visualViewport ? visualViewport.height + visualViewport.offsetTop : window.innerHeight
18
+ )
19
+
20
+ useLayoutEffect(() => {
21
+ const handleResize = () => {
22
+ const visualViewport = window.visualViewport
23
+ setHeight(() =>
24
+ visualViewport ? visualViewport.height + visualViewport.offsetTop : window.innerHeight
25
+ )
26
+ }
27
+
28
+ window.visualViewport?.addEventListener('resize', handleResize)
29
+ window.visualViewport?.addEventListener('scroll', handleResize)
30
+
31
+ return () => {
32
+ window.visualViewport?.removeEventListener('resize', handleResize)
33
+ window.visualViewport?.removeEventListener('scroll', handleResize)
34
+ }
35
+ }, [])
36
+ return height
37
+ }
@@ -56,19 +56,22 @@ describe('LicenseManager', () => {
56
56
  })
57
57
 
58
58
  it('Signals that it is development mode when appropriate', async () => {
59
- // @ts-ignore
60
- delete window.location
61
- // @ts-ignore
62
- window.location = new URL('http://localhost:3000')
63
-
64
- const testEnvLicenseManager = new LicenseManager('', keyPair.publicKey, 'development')
65
- const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
66
- const result = await testEnvLicenseManager.getLicenseFromKey(licenseKey)
67
- expect(result).toMatchObject({
68
- isLicenseParseable: true,
69
- isDomainValid: false,
70
- isDevelopment: true,
71
- })
59
+ const schemes = ['http', 'https']
60
+ for (const scheme of schemes) {
61
+ // @ts-ignore
62
+ delete window.location
63
+ // @ts-ignore
64
+ window.location = new URL(`${scheme}://localhost:3000`)
65
+
66
+ const testEnvLicenseManager = new LicenseManager('', keyPair.publicKey, 'development')
67
+ const licenseKey = await generateLicenseKey(STANDARD_LICENSE_INFO, keyPair)
68
+ const result = await testEnvLicenseManager.getLicenseFromKey(licenseKey)
69
+ expect(result).toMatchObject({
70
+ isLicenseParseable: true,
71
+ isDomainValid: false,
72
+ isDevelopment: true,
73
+ })
74
+ }
72
75
  })
73
76
 
74
77
  it('Cleanses out valid keys that accidentally have zero-width characters or newlines', async () => {