@tldraw/editor 3.9.0 → 3.10.0-canary.12c0cb0549ca
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 +243 -5
- 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 +69 -14
- 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/usePeerIds.js.map +1 -1
- package/dist-cjs/lib/hooks/usePresence.js.map +1 -1
- 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/browserCanvasMaxSize.js +5 -0
- package/dist-cjs/lib/utils/browserCanvasMaxSize.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 +243 -5
- 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 +72 -10
- 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/usePeerIds.mjs.map +1 -1
- package/dist-esm/lib/hooks/usePresence.mjs.map +1 -1
- 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/browserCanvasMaxSize.mjs +5 -0
- package/dist-esm/lib/utils/browserCanvasMaxSize.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 +92 -8
- 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/usePeerIds.ts +1 -1
- package/src/lib/hooks/usePresence.ts +2 -2
- package/src/lib/hooks/useViewportHeight.ts +37 -0
- package/src/lib/options.ts +7 -0
- package/src/lib/utils/browserCanvasMaxSize.ts +5 -3
- package/src/lib/utils/dom.ts +1 -1
- package/src/lib/utils/richText.ts +72 -0
- package/src/version.ts +3 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { react } from '@tldraw/state'
|
|
1
2
|
import { useQuickReactor, useStateTracking } from '@tldraw/state-react'
|
|
2
3
|
import { TLShape, TLShapeId } from '@tldraw/tlschema'
|
|
3
|
-
import { memo, useCallback, useRef } from 'react'
|
|
4
|
+
import { memo, useCallback, useEffect, useRef } from 'react'
|
|
4
5
|
import { ShapeUtil } from '../editor/shapes/ShapeUtil'
|
|
5
6
|
import { useEditor } from '../hooks/useEditor'
|
|
6
7
|
import { useEditorComponents } from '../hooks/useEditorComponents'
|
|
@@ -41,6 +42,13 @@ export const Shape = memo(function Shape({
|
|
|
41
42
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
42
43
|
const bgContainerRef = useRef<HTMLDivElement>(null)
|
|
43
44
|
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
return react('load fonts', () => {
|
|
47
|
+
const fonts = editor.fonts.getShapeFontFaces(shape)
|
|
48
|
+
editor.fonts.requestFonts(fonts)
|
|
49
|
+
})
|
|
50
|
+
}, [editor, shape])
|
|
51
|
+
|
|
44
52
|
const memoizedStuffRef = useRef({
|
|
45
53
|
transform: '',
|
|
46
54
|
clipPath: 'none',
|
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,
|
|
@@ -83,7 +92,6 @@ import {
|
|
|
83
92
|
structuredClone,
|
|
84
93
|
uniqueId,
|
|
85
94
|
} from '@tldraw/utils'
|
|
86
|
-
import { Number } from 'core-js'
|
|
87
95
|
import EventEmitter from 'eventemitter3'
|
|
88
96
|
import {
|
|
89
97
|
TLEditorSnapshot,
|
|
@@ -130,6 +138,7 @@ import {
|
|
|
130
138
|
import { getIncrementedName } from '../utils/getIncrementedName'
|
|
131
139
|
import { isAccelKey } from '../utils/keyboard'
|
|
132
140
|
import { getReorderingShapesChanges } from '../utils/reorderShapes'
|
|
141
|
+
import { TLTextOptions, TiptapEditor } from '../utils/richText'
|
|
133
142
|
import { applyRotationToSnapshotShapes, getRotationSnapshot } from '../utils/rotation'
|
|
134
143
|
import { BindingOnDeleteOptions, BindingUtil } from './bindings/BindingUtil'
|
|
135
144
|
import { bindingsIndex } from './derivations/bindingsIndex'
|
|
@@ -139,6 +148,7 @@ import { deriveShapeIdsInCurrentPage } from './derivations/shapeIdsInCurrentPage
|
|
|
139
148
|
import { ClickManager } from './managers/ClickManager'
|
|
140
149
|
import { EdgeScrollManager } from './managers/EdgeScrollManager'
|
|
141
150
|
import { FocusManager } from './managers/FocusManager'
|
|
151
|
+
import { FontManager } from './managers/FontManager'
|
|
142
152
|
import { HistoryManager } from './managers/HistoryManager'
|
|
143
153
|
import { ScribbleManager } from './managers/ScribbleManager'
|
|
144
154
|
import { SnapManager } from './managers/SnapManager/SnapManager'
|
|
@@ -225,8 +235,10 @@ export interface TLEditorOptions {
|
|
|
225
235
|
* Options for the editor's camera.
|
|
226
236
|
*/
|
|
227
237
|
cameraOptions?: Partial<TLCameraOptions>
|
|
238
|
+
textOptions?: TLTextOptions
|
|
228
239
|
options?: Partial<TldrawOptions>
|
|
229
240
|
licenseKey?: string
|
|
241
|
+
fontAssetUrls?: { [key: string]: string | undefined }
|
|
230
242
|
/**
|
|
231
243
|
* A predicate that should return true if the given shape should be hidden.
|
|
232
244
|
* @param shape - The shape to check.
|
|
@@ -255,6 +267,7 @@ export interface TLRenderingShape {
|
|
|
255
267
|
|
|
256
268
|
/** @public */
|
|
257
269
|
export class Editor extends EventEmitter<TLEventMap> {
|
|
270
|
+
readonly id = uniqueId()
|
|
258
271
|
constructor({
|
|
259
272
|
store,
|
|
260
273
|
user,
|
|
@@ -263,11 +276,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
263
276
|
tools,
|
|
264
277
|
getContainer,
|
|
265
278
|
cameraOptions,
|
|
279
|
+
textOptions,
|
|
266
280
|
initialState,
|
|
267
281
|
autoFocus,
|
|
268
282
|
inferDarkMode,
|
|
269
283
|
options,
|
|
270
284
|
isShapeHidden,
|
|
285
|
+
fontAssetUrls,
|
|
271
286
|
}: TLEditorOptions) {
|
|
272
287
|
super()
|
|
273
288
|
|
|
@@ -291,12 +306,16 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
291
306
|
|
|
292
307
|
this._cameraOptions.set({ ...DEFAULT_CAMERA_OPTIONS, ...cameraOptions })
|
|
293
308
|
|
|
309
|
+
this._textOptions = atom('text options', textOptions ?? null)
|
|
310
|
+
|
|
294
311
|
this.user = new UserPreferencesManager(user ?? createTLUser(), inferDarkMode ?? false)
|
|
295
312
|
this.disposables.add(() => this.user.dispose())
|
|
296
313
|
|
|
297
314
|
this.getContainer = getContainer
|
|
298
315
|
|
|
299
316
|
this.textMeasure = new TextManager(this)
|
|
317
|
+
this.fonts = new FontManager(this, fontAssetUrls)
|
|
318
|
+
|
|
300
319
|
this._tickManager = new TickManager(this)
|
|
301
320
|
|
|
302
321
|
class NewRoot extends RootState {
|
|
@@ -835,6 +854,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
835
854
|
*/
|
|
836
855
|
readonly textMeasure: TextManager
|
|
837
856
|
|
|
857
|
+
/**
|
|
858
|
+
* A utility for managing the set of fonts that should be rendered in the document.
|
|
859
|
+
*
|
|
860
|
+
* @public
|
|
861
|
+
*/
|
|
862
|
+
readonly fonts: FontManager
|
|
863
|
+
|
|
838
864
|
/**
|
|
839
865
|
* A manager for the editor's environment.
|
|
840
866
|
*
|
|
@@ -2023,6 +2049,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2023
2049
|
*/
|
|
2024
2050
|
setEditingShape(shape: TLShapeId | TLShape | null): this {
|
|
2025
2051
|
const id = typeof shape === 'string' ? shape : (shape?.id ?? null)
|
|
2052
|
+
this.setRichTextEditor(null)
|
|
2026
2053
|
if (id !== this.getEditingShapeId()) {
|
|
2027
2054
|
if (id) {
|
|
2028
2055
|
const shape = this.getShape(id)
|
|
@@ -2041,6 +2068,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2041
2068
|
this.run(
|
|
2042
2069
|
() => {
|
|
2043
2070
|
this._updateCurrentPageState({ editingShapeId: null })
|
|
2071
|
+
this._currentRichTextEditor.set(null)
|
|
2044
2072
|
},
|
|
2045
2073
|
{ history: 'ignore' }
|
|
2046
2074
|
)
|
|
@@ -2048,6 +2076,42 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2048
2076
|
return this
|
|
2049
2077
|
}
|
|
2050
2078
|
|
|
2079
|
+
// Rich text editor
|
|
2080
|
+
|
|
2081
|
+
private _currentRichTextEditor = atom('rich text editor', null as TiptapEditor | null)
|
|
2082
|
+
|
|
2083
|
+
/**
|
|
2084
|
+
* The current editing shape's text editor.
|
|
2085
|
+
*
|
|
2086
|
+
* @public
|
|
2087
|
+
*/
|
|
2088
|
+
@computed getRichTextEditor(): TiptapEditor | null {
|
|
2089
|
+
return this._currentRichTextEditor.get()
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
/**
|
|
2093
|
+
* Set the current editing shape's rich text editor.
|
|
2094
|
+
*
|
|
2095
|
+
* @example
|
|
2096
|
+
* ```ts
|
|
2097
|
+
* editor.setRichTextEditor(richTextEditorView)
|
|
2098
|
+
* ```
|
|
2099
|
+
*
|
|
2100
|
+
* @param textEditor - The text editor to set as the current editing shape's text editor.
|
|
2101
|
+
*
|
|
2102
|
+
* @public
|
|
2103
|
+
*/
|
|
2104
|
+
setRichTextEditor(textEditor: TiptapEditor | null) {
|
|
2105
|
+
// If the new editor is different from the current one, destroy the current one
|
|
2106
|
+
const current = this._currentRichTextEditor.__unsafe__getWithoutCapture()
|
|
2107
|
+
if (current !== textEditor) {
|
|
2108
|
+
current?.destroy()
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
this._currentRichTextEditor.set(textEditor)
|
|
2112
|
+
return this
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2051
2115
|
// Hovered
|
|
2052
2116
|
|
|
2053
2117
|
/**
|
|
@@ -2104,6 +2168,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2104
2168
|
@computed getHintingShapeIds() {
|
|
2105
2169
|
return this.getCurrentPageState().hintingShapeIds
|
|
2106
2170
|
}
|
|
2171
|
+
|
|
2107
2172
|
/**
|
|
2108
2173
|
* The editor's current hinting shapes.
|
|
2109
2174
|
*
|
|
@@ -2252,6 +2317,21 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2252
2317
|
return this
|
|
2253
2318
|
}
|
|
2254
2319
|
|
|
2320
|
+
private _textOptions: Atom<TLTextOptions | null>
|
|
2321
|
+
|
|
2322
|
+
/**
|
|
2323
|
+
* Get the current text options.
|
|
2324
|
+
*
|
|
2325
|
+
* @example
|
|
2326
|
+
* ```ts
|
|
2327
|
+
* editor.getTextOptions()
|
|
2328
|
+
* ```
|
|
2329
|
+
*
|
|
2330
|
+
* @public */
|
|
2331
|
+
getTextOptions() {
|
|
2332
|
+
return assertExists(this._textOptions.get(), 'Cannot use text without setting textOptions')
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2255
2335
|
/* --------------------- Camera --------------------- */
|
|
2256
2336
|
|
|
2257
2337
|
/** @internal */
|
|
@@ -4226,7 +4306,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4226
4306
|
if (!this._shapeGeometryCaches[context]) {
|
|
4227
4307
|
this._shapeGeometryCaches[context] = this.store.createComputedCache(
|
|
4228
4308
|
'bounds',
|
|
4229
|
-
(shape) =>
|
|
4309
|
+
(shape) => {
|
|
4310
|
+
this.fonts.trackFontsForShape(shape)
|
|
4311
|
+
return this.getShapeUtil(shape).getGeometry(shape, opts)
|
|
4312
|
+
},
|
|
4230
4313
|
{ areRecordsEqual: (a, b) => a.props === b.props }
|
|
4231
4314
|
)
|
|
4232
4315
|
}
|
|
@@ -4764,9 +4847,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
4764
4847
|
// Check labels first
|
|
4765
4848
|
if (
|
|
4766
4849
|
this.isShapeOfType<TLFrameShape>(shape, 'frame') ||
|
|
4767
|
-
(
|
|
4850
|
+
(this.isShapeOfType<TLArrowShape>(shape, 'arrow') && shape.props.text.trim()) ||
|
|
4851
|
+
((this.isShapeOfType<TLNoteShape>(shape, 'note') ||
|
|
4768
4852
|
(this.isShapeOfType<TLGeoShape>(shape, 'geo') && shape.props.fill === 'none')) &&
|
|
4769
|
-
shape.
|
|
4853
|
+
this.getShapeUtil(shape).getText(shape)?.trim())
|
|
4770
4854
|
) {
|
|
4771
4855
|
for (const childGeometry of (geometry as Group2d).children) {
|
|
4772
4856
|
if (childGeometry.isLabel && childGeometry.isPointInBounds(pointInShapeSpace)) {
|
|
@@ -5258,7 +5342,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5258
5342
|
|
|
5259
5343
|
const invertedParentTransform = parentTransform.clone().invert()
|
|
5260
5344
|
|
|
5261
|
-
const shapesToReparent = compact(ids.map((id) => this.getShape(id)))
|
|
5345
|
+
const shapesToReparent = compact(ids.map((id) => this.getShape(id))).sort(sortByIndex)
|
|
5262
5346
|
|
|
5263
5347
|
// Ignore locked shapes so that we can reparent locked shapes, for example
|
|
5264
5348
|
// when a locked shape's parent is deleted.
|
|
@@ -7290,7 +7374,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7290
7374
|
* @example
|
|
7291
7375
|
* ```ts
|
|
7292
7376
|
* editor.createShape(myShape)
|
|
7293
|
-
* editor.createShape({ id: 'box1', type: 'text', props: {
|
|
7377
|
+
* editor.createShape({ id: 'box1', type: 'text', props: { richText: toRichText("ok") } })
|
|
7294
7378
|
* ```
|
|
7295
7379
|
*
|
|
7296
7380
|
* @param shape - The shape (or shape partial) to create.
|
|
@@ -7308,7 +7392,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7308
7392
|
* @example
|
|
7309
7393
|
* ```ts
|
|
7310
7394
|
* editor.createShapes([myShape])
|
|
7311
|
-
* editor.createShapes([{ id: 'box1', type: 'text', props: {
|
|
7395
|
+
* editor.createShapes([{ id: 'box1', type: 'text', props: { richText: toRichText("ok") } }])
|
|
7312
7396
|
* ```
|
|
7313
7397
|
*
|
|
7314
7398
|
* @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
|
*
|