@tldraw/editor 3.9.0 → 3.10.0-canary.15f6aaa3d2d3

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 (131) hide show
  1. package/dist-cjs/index.d.ts +232 -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 +32 -6
  5. package/dist-cjs/lib/TldrawEditor.js.map +2 -2
  6. package/dist-cjs/lib/components/LiveCollaborators.js +5 -0
  7. package/dist-cjs/lib/components/LiveCollaborators.js.map +2 -2
  8. package/dist-cjs/lib/components/Shape.js +7 -0
  9. package/dist-cjs/lib/components/Shape.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultBrush.js.map +2 -2
  11. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +2 -2
  12. package/dist-cjs/lib/components/default-components/DefaultCursor.js.map +2 -2
  13. package/dist-cjs/lib/components/default-components/DefaultScribble.js.map +2 -2
  14. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  15. package/dist-cjs/lib/editor/Editor.js +63 -7
  16. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  17. package/dist-cjs/lib/editor/managers/FontManager.js +166 -0
  18. package/dist-cjs/lib/editor/managers/FontManager.js.map +7 -0
  19. package/dist-cjs/lib/editor/managers/TextManager.js +23 -17
  20. package/dist-cjs/lib/editor/managers/TextManager.js.map +2 -2
  21. package/dist-cjs/lib/editor/shapes/ShapeUtil.js +11 -0
  22. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  23. package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
  24. package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
  25. package/dist-cjs/lib/exports/FontEmbedder.js +7 -2
  26. package/dist-cjs/lib/exports/FontEmbedder.js.map +2 -2
  27. package/dist-cjs/lib/exports/StyleEmbedder.js +1 -1
  28. package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
  29. package/dist-cjs/lib/exports/exportToSvg.js +3 -2
  30. package/dist-cjs/lib/exports/exportToSvg.js.map +2 -2
  31. package/dist-cjs/lib/exports/getSvgJsx.js +18 -1
  32. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  33. package/dist-cjs/lib/exports/parseCss.js +1 -0
  34. package/dist-cjs/lib/exports/parseCss.js.map +2 -2
  35. package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -2
  36. package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
  37. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js +1 -1
  38. package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
  39. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js +48 -0
  40. package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +7 -0
  41. package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
  42. package/dist-cjs/lib/hooks/useViewportHeight.js +56 -0
  43. package/dist-cjs/lib/hooks/useViewportHeight.js.map +7 -0
  44. package/dist-cjs/lib/options.js +2 -1
  45. package/dist-cjs/lib/options.js.map +2 -2
  46. package/dist-cjs/lib/utils/dom.js +1 -1
  47. package/dist-cjs/lib/utils/dom.js.map +2 -2
  48. package/dist-cjs/lib/utils/richText.js +46 -0
  49. package/dist-cjs/lib/utils/richText.js.map +7 -0
  50. package/dist-cjs/version.js +3 -3
  51. package/dist-cjs/version.js.map +1 -1
  52. package/dist-esm/index.d.mts +232 -3
  53. package/dist-esm/index.mjs +13 -1
  54. package/dist-esm/index.mjs.map +2 -2
  55. package/dist-esm/lib/TldrawEditor.mjs +33 -7
  56. package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
  57. package/dist-esm/lib/components/LiveCollaborators.mjs +5 -0
  58. package/dist-esm/lib/components/LiveCollaborators.mjs.map +2 -2
  59. package/dist-esm/lib/components/Shape.mjs +8 -1
  60. package/dist-esm/lib/components/Shape.mjs.map +2 -2
  61. package/dist-esm/lib/components/default-components/DefaultBrush.mjs.map +2 -2
  62. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +2 -2
  63. package/dist-esm/lib/components/default-components/DefaultCursor.mjs.map +2 -2
  64. package/dist-esm/lib/components/default-components/DefaultScribble.mjs.map +2 -2
  65. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  66. package/dist-esm/lib/editor/Editor.mjs +71 -8
  67. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  68. package/dist-esm/lib/editor/managers/FontManager.mjs +152 -0
  69. package/dist-esm/lib/editor/managers/FontManager.mjs.map +7 -0
  70. package/dist-esm/lib/editor/managers/TextManager.mjs +23 -17
  71. package/dist-esm/lib/editor/managers/TextManager.mjs.map +2 -2
  72. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +11 -0
  73. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  74. package/dist-esm/lib/exports/FontEmbedder.mjs +7 -2
  75. package/dist-esm/lib/exports/FontEmbedder.mjs.map +2 -2
  76. package/dist-esm/lib/exports/StyleEmbedder.mjs +1 -1
  77. package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
  78. package/dist-esm/lib/exports/exportToSvg.mjs +3 -2
  79. package/dist-esm/lib/exports/exportToSvg.mjs.map +2 -2
  80. package/dist-esm/lib/exports/getSvgJsx.mjs +19 -2
  81. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  82. package/dist-esm/lib/exports/parseCss.mjs +1 -0
  83. package/dist-esm/lib/exports/parseCss.mjs.map +2 -2
  84. package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -2
  85. package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
  86. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs +1 -1
  87. package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
  88. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs +28 -0
  89. package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +7 -0
  90. package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
  91. package/dist-esm/lib/hooks/useViewportHeight.mjs +36 -0
  92. package/dist-esm/lib/hooks/useViewportHeight.mjs.map +7 -0
  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/dom.mjs +1 -1
  96. package/dist-esm/lib/utils/dom.mjs.map +2 -2
  97. package/dist-esm/lib/utils/richText.mjs +26 -0
  98. package/dist-esm/lib/utils/richText.mjs.map +7 -0
  99. package/dist-esm/version.mjs +3 -3
  100. package/dist-esm/version.mjs.map +1 -1
  101. package/editor.css +127 -13
  102. package/package.json +10 -7
  103. package/src/index.ts +15 -0
  104. package/src/lib/TldrawEditor.tsx +52 -4
  105. package/src/lib/components/LiveCollaborators.tsx +5 -0
  106. package/src/lib/components/Shape.tsx +9 -1
  107. package/src/lib/components/default-components/DefaultBrush.tsx +1 -0
  108. package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -0
  109. package/src/lib/components/default-components/DefaultCursor.tsx +1 -0
  110. package/src/lib/components/default-components/DefaultScribble.tsx +1 -0
  111. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +1 -0
  112. package/src/lib/editor/Editor.ts +91 -6
  113. package/src/lib/editor/managers/FontManager.ts +251 -0
  114. package/src/lib/editor/managers/TextManager.ts +42 -17
  115. package/src/lib/editor/shapes/ShapeUtil.ts +13 -0
  116. package/src/lib/editor/types/emit-types.ts +1 -0
  117. package/src/lib/editor/types/external-content.ts +1 -0
  118. package/src/lib/exports/FontEmbedder.ts +13 -1
  119. package/src/lib/exports/StyleEmbedder.ts +1 -1
  120. package/src/lib/exports/exportToSvg.tsx +4 -3
  121. package/src/lib/exports/getSvgJsx.tsx +22 -2
  122. package/src/lib/exports/parseCss.ts +1 -0
  123. package/src/lib/hooks/useCanvasEvents.ts +2 -1
  124. package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +1 -0
  125. package/src/lib/hooks/usePassThroughMouseOverEvents.ts +29 -0
  126. package/src/lib/hooks/usePassThroughWheelEvents.ts +0 -1
  127. package/src/lib/hooks/useViewportHeight.ts +37 -0
  128. package/src/lib/options.ts +7 -0
  129. package/src/lib/utils/dom.ts +1 -1
  130. package/src/lib/utils/richText.ts +72 -0
  131. package/src/version.ts +3 -3
@@ -1,4 +1,12 @@
1
- import { EMPTY_ARRAY, atom, computed, react, transact, unsafe__withoutCapture } from '@tldraw/state'
1
+ import {
2
+ Atom,
3
+ EMPTY_ARRAY,
4
+ atom,
5
+ computed,
6
+ react,
7
+ transact,
8
+ unsafe__withoutCapture,
9
+ } from '@tldraw/state'
2
10
  import {
3
11
  ComputedCache,
4
12
  RecordType,
@@ -34,6 +42,7 @@ import {
34
42
  TLImageAsset,
35
43
  TLInstance,
36
44
  TLInstancePageState,
45
+ TLNoteShape,
37
46
  TLPOINTER_ID,
38
47
  TLPage,
39
48
  TLPageId,
@@ -130,6 +139,7 @@ import {
130
139
  import { getIncrementedName } from '../utils/getIncrementedName'
131
140
  import { isAccelKey } from '../utils/keyboard'
132
141
  import { getReorderingShapesChanges } from '../utils/reorderShapes'
142
+ import { TLTextOptions, TiptapEditor } from '../utils/richText'
133
143
  import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation'
134
144
  import { BindingOnDeleteOptions, BindingUtil } from './bindings/BindingUtil'
135
145
  import { bindingsIndex } from './derivations/bindingsIndex'
@@ -139,6 +149,7 @@ import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage
139
149
  import { ClickManager } from './managers/ClickManager'
140
150
  import { EdgeScrollManager } from './managers/EdgeScrollManager'
141
151
  import { FocusManager } from './managers/FocusManager'
152
+ import { FontManager } from './managers/FontManager'
142
153
  import { HistoryManager } from './managers/HistoryManager'
143
154
  import { ScribbleManager } from './managers/ScribbleManager'
144
155
  import { SnapManager } from './managers/SnapManager/SnapManager'
@@ -225,8 +236,10 @@ export interface TLEditorOptions {
225
236
  * Options for the editor's camera.
226
237
  */
227
238
  cameraOptions?: Partial<TLCameraOptions>
239
+ textOptions?: TLTextOptions
228
240
  options?: Partial<TldrawOptions>
229
241
  licenseKey?: string
242
+ fontAssetUrls?: { [key: string]: string | undefined }
230
243
  /**
231
244
  * A predicate that should return true if the given shape should be hidden.
232
245
  * @param shape - The shape to check.
@@ -255,6 +268,7 @@ export interface TLRenderingShape {
255
268
 
256
269
  /** @public */
257
270
  export class Editor extends EventEmitter<TLEventMap> {
271
+ readonly id = uniqueId()
258
272
  constructor({
259
273
  store,
260
274
  user,
@@ -263,11 +277,13 @@ export class Editor extends EventEmitter<TLEventMap> {
263
277
  tools,
264
278
  getContainer,
265
279
  cameraOptions,
280
+ textOptions,
266
281
  initialState,
267
282
  autoFocus,
268
283
  inferDarkMode,
269
284
  options,
270
285
  isShapeHidden,
286
+ fontAssetUrls,
271
287
  }: TLEditorOptions) {
272
288
  super()
273
289
 
@@ -291,12 +307,16 @@ export class Editor extends EventEmitter<TLEventMap> {
291
307
 
292
308
  this._cameraOptions.set({ ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions })
293
309
 
310
+ this._textOptions = atom('text options', textOptions ?? null)
311
+
294
312
  this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false)
295
313
  this.disposables.add(() => this.user.dispose())
296
314
 
297
315
  this.getContainer = getContainer
298
316
 
299
317
  this.textMeasure = new TextManager(this)
318
+ this.fonts = new FontManager(this, fontAssetUrls)
319
+
300
320
  this._tickManager = new TickManager(this)
301
321
 
302
322
  class NewRoot extends RootState {
@@ -835,6 +855,13 @@ export class Editor extends EventEmitter<TLEventMap> {
835
855
  */
836
856
  readonly textMeasure: TextManager
837
857
 
858
+ /**
859
+ * A utility for managing the set of fonts that should be rendered in the document.
860
+ *
861
+ * @public
862
+ */
863
+ readonly fonts: FontManager
864
+
838
865
  /**
839
866
  * A manager for the editor's environment.
840
867
  *
@@ -2023,6 +2050,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2023
2050
  */
2024
2051
  setEditingShape(shape: TLShapeId | TLShape | null): this {
2025
2052
  const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
2053
+ this.setRichTextEditor(null)
2026
2054
  if (id !== this.getEditingShapeId()) {
2027
2055
  if (id) {
2028
2056
  const shape = this.getShape(id)
@@ -2041,6 +2069,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2041
2069
  this.run(
2042
2070
  () => {
2043
2071
  this._updateCurrentPageState({ editingShapeId: null })
2072
+ this._currentRichTextEditor.set(null)
2044
2073
  },
2045
2074
  { history: 'ignore' }
2046
2075
  )
@@ -2048,6 +2077,42 @@ export class Editor extends EventEmitter<TLEventMap> {
2048
2077
  return this
2049
2078
  }
2050
2079
 
2080
+ // Rich text editor
2081
+
2082
+ private _currentRichTextEditor = atom('rich text editor', null as TiptapEditor | null)
2083
+
2084
+ /**
2085
+ * The current editing shape's text editor.
2086
+ *
2087
+ * @public
2088
+ */
2089
+ @computed getRichTextEditor(): TiptapEditor | null {
2090
+ return this._currentRichTextEditor.get()
2091
+ }
2092
+
2093
+ /**
2094
+ * Set the current editing shape's rich text editor.
2095
+ *
2096
+ * @example
2097
+ * ```ts
2098
+ * editor.setRichTextEditor(richTextEditorView)
2099
+ * ```
2100
+ *
2101
+ * @param textEditor - The text editor to set as the current editing shape's text editor.
2102
+ *
2103
+ * @public
2104
+ */
2105
+ setRichTextEditor(textEditor: TiptapEditor | null) {
2106
+ // If the new editor is different from the current one, destroy the current one
2107
+ const current = this._currentRichTextEditor.__unsafe__getWithoutCapture()
2108
+ if (current !== textEditor) {
2109
+ current?.destroy()
2110
+ }
2111
+
2112
+ this._currentRichTextEditor.set(textEditor)
2113
+ return this
2114
+ }
2115
+
2051
2116
  // Hovered
2052
2117
 
2053
2118
  /**
@@ -2104,6 +2169,7 @@ export class Editor extends EventEmitter<TLEventMap> {
2104
2169
  @computed getHintingShapeIds() {
2105
2170
  return this.getCurrentPageState().hintingShapeIds
2106
2171
  }
2172
+
2107
2173
  /**
2108
2174
  * The editor's current hinting shapes.
2109
2175
  *
@@ -2252,6 +2318,21 @@ export class Editor extends EventEmitter<TLEventMap> {
2252
2318
  return this
2253
2319
  }
2254
2320
 
2321
+ private _textOptions: Atom<TLTextOptions | null>
2322
+
2323
+ /**
2324
+ * Get the current text options.
2325
+ *
2326
+ * @example
2327
+ * ```ts
2328
+ * editor.getTextOptions()
2329
+ * ```
2330
+ *
2331
+ * @public */
2332
+ getTextOptions() {
2333
+ return assertExists(this._textOptions.get(), 'Cannot use text without setting textOptions')
2334
+ }
2335
+
2255
2336
  /* --------------------- Camera --------------------- */
2256
2337
 
2257
2338
  /** @internal */
@@ -4226,7 +4307,10 @@ export class Editor extends EventEmitter<TLEventMap> {
4226
4307
  if (!this._shapeGeometryCaches[context]) {
4227
4308
  this._shapeGeometryCaches[context] = this.store.createComputedCache(
4228
4309
  'bounds',
4229
- (shape) => this.getShapeUtil(shape).getGeometry(shape, opts),
4310
+ (shape) => {
4311
+ this.fonts.trackFontsForShape(shape)
4312
+ return this.getShapeUtil(shape).getGeometry(shape, opts)
4313
+ },
4230
4314
  { areRecordsEqual: (a, b) => a.props === b.props }
4231
4315
  )
4232
4316
  }
@@ -4764,9 +4848,10 @@ export class Editor extends EventEmitter<TLEventMap> {
4764
4848
  // Check labels first
4765
4849
  if (
4766
4850
  this.isShapeOfType<TLFrameShape>(shape, 'frame') ||
4767
- ((this.isShapeOfType<TLArrowShape>(shape, 'arrow') ||
4851
+ (this.isShapeOfType<TLArrowShape>(shape, 'arrow') && shape.props.text.trim()) ||
4852
+ ((this.isShapeOfType<TLNoteShape>(shape, 'note') ||
4768
4853
  (this.isShapeOfType<TLGeoShape>(shape, 'geo') && shape.props.fill === 'none')) &&
4769
- shape.props.text.trim())
4854
+ this.getShapeUtil(shape).getText(shape)?.trim())
4770
4855
  ) {
4771
4856
  for (const childGeometry of (geometry as Group2d).children) {
4772
4857
  if (childGeometry.isLabel && childGeometry.isPointInBounds(pointInShapeSpace)) {
@@ -7290,7 +7375,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7290
7375
  * @example
7291
7376
  * ```ts
7292
7377
  * editor.createShape(myShape)
7293
- * editor.createShape({ id: 'box1', type: 'text', props: { text: "ok" } })
7378
+ * editor.createShape({ id: 'box1', type: 'text', props: { richText: toRichText("ok") } })
7294
7379
  * ```
7295
7380
  *
7296
7381
  * @param shape - The shape (or shape partial) to create.
@@ -7308,7 +7393,7 @@ export class Editor extends EventEmitter<TLEventMap> {
7308
7393
  * @example
7309
7394
  * ```ts
7310
7395
  * editor.createShapes([myShape])
7311
- * editor.createShapes([{ id: 'box1', type: 'text', props: { text: "ok" } }])
7396
+ * editor.createShapes([{ id: 'box1', type: 'text', props: { richText: toRichText("ok") } }])
7312
7397
  * ```
7313
7398
  *
7314
7399
  * @param shapes - The shapes (or shape partials) to create.
@@ -0,0 +1,251 @@
1
+ import { computed, EMPTY_ARRAY, transact } from '@tldraw/state'
2
+ import { AtomMap } from '@tldraw/store'
3
+ import { TLShape, TLShapeId } from '@tldraw/tlschema'
4
+ import {
5
+ areArraysShallowEqual,
6
+ compact,
7
+ FileHelpers,
8
+ mapObjectMapValues,
9
+ objectMapEntries,
10
+ } from '@tldraw/utils'
11
+ import { Editor } from '../Editor'
12
+
13
+ /**
14
+ * Represents the `src` property of a {@link TLFontFace}.
15
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/src | `src`} for details of the properties here.
16
+ * @public
17
+ */
18
+ export interface TLFontFaceSource {
19
+ /**
20
+ * A URL from which to load the font. If the value here is a key in
21
+ * {@link tldraw#TLEditorAssetUrls.fonts}, the value from there will be used instead.
22
+ */
23
+ url: string
24
+ format?: string
25
+ tech?: string
26
+ }
27
+
28
+ /**
29
+ * A font face that can be used in the editor. The properties of this are largely the same as the
30
+ * ones in the
31
+ * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face | css `@font-face` rule}.
32
+ * @public
33
+ */
34
+ export interface TLFontFace {
35
+ /**
36
+ * How this font can be referred to in CSS.
37
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-family | `font-family`}.
38
+ */
39
+ readonly family: string
40
+ /**
41
+ * The source of the font. This
42
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/src | `src`}.
43
+ */
44
+ readonly src: TLFontFaceSource
45
+ /**
46
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/ascent-override | `ascent-override`}.
47
+ */
48
+ readonly ascentOverride?: string
49
+ /**
50
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/descent-override | `descent-override`}.
51
+ */
52
+ readonly descentOverride?: string
53
+ /**
54
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-stretch | `font-stretch`}.
55
+ */
56
+ readonly stretch?: string
57
+ /**
58
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-style | `font-style`}.
59
+ */
60
+ readonly style?: string
61
+ /**
62
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-weight | `font-weight`}.
63
+ */
64
+ readonly weight?: string
65
+ /**
66
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-feature-settings | `font-feature-settings`}.
67
+ */
68
+ readonly featureSettings?: string
69
+ /**
70
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/line-gap-override | `line-gap-override`}.
71
+ */
72
+ readonly lineGapOverride?: string
73
+ /**
74
+ * See {@link https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range | `unicode-range`}.
75
+ */
76
+ readonly unicodeRange?: string
77
+ }
78
+
79
+ interface FontState {
80
+ readonly state: 'loading' | 'ready' | 'error'
81
+ readonly instance: FontFace
82
+ readonly loadingPromise: Promise<void>
83
+ }
84
+
85
+ /** @public */
86
+ export class FontManager {
87
+ constructor(
88
+ private readonly editor: Editor,
89
+ private readonly assetUrls?: { [key: string]: string | undefined }
90
+ ) {
91
+ this.shapeFontFacesCache = editor.store.createComputedCache(
92
+ 'shape font faces',
93
+ (shape: TLShape) => {
94
+ const shapeUtil = this.editor.getShapeUtil(shape)
95
+ return shapeUtil.getFontFaces(shape)
96
+ },
97
+ { areResultsEqual: areArraysShallowEqual }
98
+ )
99
+
100
+ this.shapeFontLoadStateCache = editor.store.createCache<(FontState | null)[], TLShape>(
101
+ (id: TLShapeId) => {
102
+ const fontFacesComputed = computed('font faces', () => this.getShapeFontFaces(id))
103
+ return computed(
104
+ 'font load state',
105
+ () => {
106
+ const states = fontFacesComputed.get().map((face) => this.getFontState(face))
107
+ return states
108
+ },
109
+ { isEqual: areArraysShallowEqual }
110
+ )
111
+ }
112
+ )
113
+ }
114
+
115
+ private readonly shapeFontFacesCache
116
+ private readonly shapeFontLoadStateCache
117
+
118
+ getShapeFontFaces(shape: TLShape | TLShapeId): TLFontFace[] {
119
+ const shapeId = typeof shape === 'string' ? shape : shape.id
120
+ return this.shapeFontFacesCache.get(shapeId) ?? EMPTY_ARRAY
121
+ }
122
+
123
+ trackFontsForShape(shape: TLShape | TLShapeId) {
124
+ const shapeId = typeof shape === 'string' ? shape : shape.id
125
+ this.shapeFontLoadStateCache.get(shapeId)
126
+ }
127
+
128
+ async loadRequiredFontsForCurrentPage(limit = Infinity) {
129
+ const neededFonts = new Set<TLFontFace>()
130
+ for (const shapeId of this.editor.getCurrentPageShapeIds()) {
131
+ for (const font of this.getShapeFontFaces(this.editor.getShape(shapeId)!)) {
132
+ neededFonts.add(font)
133
+ }
134
+ }
135
+
136
+ if (neededFonts.size > limit) {
137
+ return
138
+ }
139
+
140
+ const promises = Array.from(neededFonts, (font) => this.ensureFontIsLoaded(font))
141
+ await Promise.all(promises)
142
+ }
143
+
144
+ private readonly fontStates = new AtomMap<TLFontFace, FontState>('font states')
145
+ private getFontState(font: TLFontFace): FontState | null {
146
+ return this.fontStates.get(font) ?? null
147
+ }
148
+
149
+ ensureFontIsLoaded(font: TLFontFace): Promise<void> {
150
+ const existingState = this.getFontState(font)
151
+ if (existingState) return existingState.loadingPromise
152
+
153
+ const instance = this.findOrCreateFontFace(font)
154
+ const state: FontState = {
155
+ state: 'loading',
156
+ instance,
157
+ loadingPromise: instance
158
+ .load()
159
+ .then(() => {
160
+ document.fonts.add(instance)
161
+ this.fontStates.update(font, (s) => ({ ...s, state: 'ready' }))
162
+ })
163
+ .catch((err) => {
164
+ console.error(err)
165
+ this.fontStates.update(font, (s) => ({ ...s, state: 'error' }))
166
+ }),
167
+ }
168
+
169
+ this.fontStates.set(font, state)
170
+ return state.loadingPromise
171
+ }
172
+
173
+ private fontsToLoad = new Set<TLFontFace>()
174
+ requestFonts(fonts: TLFontFace[]) {
175
+ if (!this.fontsToLoad.size) {
176
+ queueMicrotask(() => {
177
+ if (this.editor.isDisposed) return
178
+ const toLoad = this.fontsToLoad
179
+ this.fontsToLoad = new Set()
180
+ transact(() => {
181
+ for (const font of toLoad) {
182
+ this.ensureFontIsLoaded(font)
183
+ }
184
+ })
185
+ })
186
+ }
187
+ for (const font of fonts) {
188
+ this.fontsToLoad.add(font)
189
+ }
190
+ }
191
+
192
+ private findOrCreateFontFace(font: TLFontFace) {
193
+ for (const existing of document.fonts) {
194
+ if (
195
+ existing.family === font.family &&
196
+ objectMapEntries(defaultFontFaceDescriptors).every(
197
+ ([key, defaultValue]) => existing[key] === (font[key] ?? defaultValue)
198
+ )
199
+ ) {
200
+ return existing
201
+ }
202
+ }
203
+
204
+ const url = this.assetUrls?.[font.src.url] ?? font.src.url
205
+ const instance = new FontFace(font.family, `url(${JSON.stringify(url)})`, {
206
+ ...mapObjectMapValues(defaultFontFaceDescriptors, (key) => font[key]),
207
+ display: 'swap',
208
+ })
209
+
210
+ document.fonts.add(instance)
211
+
212
+ return instance
213
+ }
214
+
215
+ async toEmbeddedCssDeclaration(font: TLFontFace) {
216
+ const url = this.assetUrls?.[font.src.url] ?? font.src.url
217
+ const dataUrl = await FileHelpers.urlToDataUrl(url)
218
+
219
+ const src = compact([
220
+ `url("${dataUrl}")`,
221
+ font.src.format ? `format(${font.src.format})` : null,
222
+ font.src.tech ? `tech(${font.src.tech})` : null,
223
+ ]).join(' ')
224
+ return compact([
225
+ `@font-face {`,
226
+ ` font-family: "${font.family}";`,
227
+ font.ascentOverride ? ` ascent-override: ${font.ascentOverride};` : null,
228
+ font.descentOverride ? ` descent-override: ${font.descentOverride};` : null,
229
+ font.stretch ? ` font-stretch: ${font.stretch};` : null,
230
+ font.style ? ` font-style: ${font.style};` : null,
231
+ font.weight ? ` font-weight: ${font.weight};` : null,
232
+ font.featureSettings ? ` font-feature-settings: ${font.featureSettings};` : null,
233
+ font.lineGapOverride ? ` line-gap-override: ${font.lineGapOverride};` : null,
234
+ font.unicodeRange ? ` unicode-range: ${font.unicodeRange};` : null,
235
+ ` src: ${src};`,
236
+ `}`,
237
+ ]).join('\n')
238
+ }
239
+ }
240
+
241
+ // From https://drafts.csswg.org/css-font-loading/#fontface-interface
242
+ const defaultFontFaceDescriptors = {
243
+ style: 'normal',
244
+ weight: 'normal',
245
+ stretch: 'normal',
246
+ unicodeRange: 'U+0-10FFFF',
247
+ featureSettings: 'normal',
248
+ ascentOverride: 'normal',
249
+ descentOverride: 'normal',
250
+ lineGapOverride: 'normal',
251
+ }
@@ -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'
@@ -170,6 +172,17 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
170
172
  */
171
173
  abstract indicator(shape: Shape): any
172
174
 
175
+ /**
176
+ * Get the font faces that should be rendered in the document in order for this shape to render
177
+ * correctly.
178
+ *
179
+ * @param shape - The shape.
180
+ * @public
181
+ */
182
+ getFontFaces(shape: Shape): TLFontFace[] {
183
+ return EMPTY_ARRAY
184
+ }
185
+
173
186
  /**
174
187
  * Whether the shape can be snapped to by another shape.
175
188
  *
@@ -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