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