@tldraw/editor 3.9.0 → 3.10.0-canary.3bf31007c5a7
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 +232 -3
- package/dist-cjs/index.js +9 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js +32 -6
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/LiveCollaborators.js +5 -0
- package/dist-cjs/lib/components/LiveCollaborators.js.map +2 -2
- package/dist-cjs/lib/components/Shape.js +7 -0
- package/dist-cjs/lib/components/Shape.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultBrush.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCursor.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultScribble.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +63 -7
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FontManager.js +166 -0
- package/dist-cjs/lib/editor/managers/FontManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/TextManager.js +23 -17
- package/dist-cjs/lib/editor/managers/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +11 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/types/emit-types.js.map +1 -1
- package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
- package/dist-cjs/lib/exports/FontEmbedder.js +7 -2
- package/dist-cjs/lib/exports/FontEmbedder.js.map +2 -2
- package/dist-cjs/lib/exports/StyleEmbedder.js +1 -1
- package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
- package/dist-cjs/lib/exports/exportToSvg.js +3 -2
- package/dist-cjs/lib/exports/exportToSvg.js.map +2 -2
- package/dist-cjs/lib/exports/getSvgJsx.js +18 -1
- package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
- package/dist-cjs/lib/exports/parseCss.js +1 -0
- package/dist-cjs/lib/exports/parseCss.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js +1 -1
- package/dist-cjs/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js +48 -0
- package/dist-cjs/lib/hooks/usePassThroughMouseOverEvents.js.map +7 -0
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useViewportHeight.js +56 -0
- package/dist-cjs/lib/hooks/useViewportHeight.js.map +7 -0
- package/dist-cjs/lib/options.js +2 -1
- package/dist-cjs/lib/options.js.map +2 -2
- package/dist-cjs/lib/utils/dom.js +1 -1
- package/dist-cjs/lib/utils/dom.js.map +2 -2
- package/dist-cjs/lib/utils/richText.js +46 -0
- package/dist-cjs/lib/utils/richText.js.map +7 -0
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +232 -3
- package/dist-esm/index.mjs +13 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs +33 -7
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/LiveCollaborators.mjs +5 -0
- package/dist-esm/lib/components/LiveCollaborators.mjs.map +2 -2
- package/dist-esm/lib/components/Shape.mjs +8 -1
- package/dist-esm/lib/components/Shape.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultBrush.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCursor.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultScribble.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +71 -8
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FontManager.mjs +152 -0
- package/dist-esm/lib/editor/managers/FontManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/TextManager.mjs +23 -17
- package/dist-esm/lib/editor/managers/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +11 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/exports/FontEmbedder.mjs +7 -2
- package/dist-esm/lib/exports/FontEmbedder.mjs.map +2 -2
- package/dist-esm/lib/exports/StyleEmbedder.mjs +1 -1
- package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
- package/dist-esm/lib/exports/exportToSvg.mjs +3 -2
- package/dist-esm/lib/exports/exportToSvg.mjs.map +2 -2
- package/dist-esm/lib/exports/getSvgJsx.mjs +19 -2
- package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
- package/dist-esm/lib/exports/parseCss.mjs +1 -0
- package/dist-esm/lib/exports/parseCss.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs +1 -1
- package/dist-esm/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs +28 -0
- package/dist-esm/lib/hooks/usePassThroughMouseOverEvents.mjs.map +7 -0
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useViewportHeight.mjs +36 -0
- package/dist-esm/lib/hooks/useViewportHeight.mjs.map +7 -0
- package/dist-esm/lib/options.mjs +2 -1
- package/dist-esm/lib/options.mjs.map +2 -2
- package/dist-esm/lib/utils/dom.mjs +1 -1
- package/dist-esm/lib/utils/dom.mjs.map +2 -2
- package/dist-esm/lib/utils/richText.mjs +26 -0
- package/dist-esm/lib/utils/richText.mjs.map +7 -0
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +127 -13
- package/package.json +10 -7
- package/src/index.ts +15 -0
- package/src/lib/TldrawEditor.tsx +52 -4
- package/src/lib/components/LiveCollaborators.tsx +5 -0
- package/src/lib/components/Shape.tsx +9 -1
- package/src/lib/components/default-components/DefaultBrush.tsx +1 -0
- package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -0
- package/src/lib/components/default-components/DefaultCursor.tsx +1 -0
- package/src/lib/components/default-components/DefaultScribble.tsx +1 -0
- package/src/lib/components/default-components/DefaultShapeIndicator.tsx +1 -0
- package/src/lib/editor/Editor.ts +91 -6
- package/src/lib/editor/managers/FontManager.ts +251 -0
- package/src/lib/editor/managers/TextManager.ts +42 -17
- package/src/lib/editor/shapes/ShapeUtil.ts +13 -0
- package/src/lib/editor/types/emit-types.ts +1 -0
- package/src/lib/editor/types/external-content.ts +1 -0
- package/src/lib/exports/FontEmbedder.ts +13 -1
- package/src/lib/exports/StyleEmbedder.ts +1 -1
- package/src/lib/exports/exportToSvg.tsx +4 -3
- package/src/lib/exports/getSvgJsx.tsx +22 -2
- package/src/lib/exports/parseCss.ts +1 -0
- package/src/lib/hooks/useCanvasEvents.ts +2 -1
- package/src/lib/hooks/useFixSafariDoubleTapZoomPencilEvents.ts +1 -0
- package/src/lib/hooks/usePassThroughMouseOverEvents.ts +29 -0
- package/src/lib/hooks/usePassThroughWheelEvents.ts +0 -1
- package/src/lib/hooks/useViewportHeight.ts +37 -0
- package/src/lib/options.ts +7 -0
- package/src/lib/utils/dom.ts +1 -1
- package/src/lib/utils/richText.ts +72 -0
- package/src/version.ts +3 -3
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
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) =>
|
|
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
|
-
(
|
|
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.
|
|
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: {
|
|
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: {
|
|
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
|
|
71
|
-
this.editor.getContainer().appendChild(
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
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
|
*
|
|
@@ -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
|
-
|
|
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 =
|
|
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-
|
|
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-
|
|
89
|
+
...svg.querySelectorAll('foreignObject.tl-export-embed-styles > *'),
|
|
89
90
|
]
|
|
90
91
|
if (!foreignObjectChildren.length) return
|
|
91
92
|
|