@tldraw/editor 3.12.0-canary.f3ad945f4591 → 3.12.0-internal.624e32507d98

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/editor",
3
3
  "description": "A tiny little drawing app (editor).",
4
- "version": "3.12.0-canary.f3ad945f4591",
4
+ "version": "3.12.0-internal.624e32507d98",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -48,12 +48,12 @@
48
48
  "@tiptap/core": "^2.9.1",
49
49
  "@tiptap/pm": "^2.9.1",
50
50
  "@tiptap/react": "^2.9.1",
51
- "@tldraw/state": "3.12.0-canary.f3ad945f4591",
52
- "@tldraw/state-react": "3.12.0-canary.f3ad945f4591",
53
- "@tldraw/store": "3.12.0-canary.f3ad945f4591",
54
- "@tldraw/tlschema": "3.12.0-canary.f3ad945f4591",
55
- "@tldraw/utils": "3.12.0-canary.f3ad945f4591",
56
- "@tldraw/validate": "3.12.0-canary.f3ad945f4591",
51
+ "@tldraw/state": "3.12.0-internal.624e32507d98",
52
+ "@tldraw/state-react": "3.12.0-internal.624e32507d98",
53
+ "@tldraw/store": "3.12.0-internal.624e32507d98",
54
+ "@tldraw/tlschema": "3.12.0-internal.624e32507d98",
55
+ "@tldraw/utils": "3.12.0-internal.624e32507d98",
56
+ "@tldraw/validate": "3.12.0-internal.624e32507d98",
57
57
  "@types/core-js": "^2.5.8",
58
58
  "@use-gesture/react": "^10.3.1",
59
59
  "classnames": "^2.5.1",
@@ -1,5 +1,6 @@
1
- import { assertExists, objectMapValues, uniqueId } from '@tldraw/utils'
1
+ import { assert, getOwnProperty, objectMapValues, uniqueId } from '@tldraw/utils'
2
2
  import { FontEmbedder } from './FontEmbedder'
3
+ import { ReadonlyStyles, Styles, cssRules } from './cssRules'
3
4
  import {
4
5
  elementStyle,
5
6
  getComputedStyle,
@@ -7,15 +8,8 @@ import {
7
8
  getRenderedChildren,
8
9
  } from './domUtils'
9
10
  import { resourceToDataUrl } from './fetchCache'
10
- import {
11
- isPropertyCoveredByCurrentColor,
12
- isPropertyInherited,
13
- parseCssValueUrls,
14
- shouldIncludeCssProperty,
15
- } from './parseCss'
16
-
17
- type Styles = { [K in string]?: string }
18
- type ReadonlyStyles = { readonly [K in string]?: string }
11
+ import { parseCssValueUrls, shouldIncludeCssProperty } from './parseCss'
12
+
19
13
  const NO_STYLES = {} as const
20
14
 
21
15
  interface ElementStyleInfo {
@@ -29,6 +23,18 @@ export class StyleEmbedder {
29
23
  private readonly styles = new Map<Element, ElementStyleInfo>()
30
24
  readonly fonts = new FontEmbedder()
31
25
 
26
+ async collectDefaultStyles(elements: Element[]) {
27
+ const collected = new Set<string>()
28
+ const promises = []
29
+ for (const element of elements) {
30
+ const tagName = element.tagName.toLowerCase()
31
+ if (collected.has(tagName)) continue
32
+ collected.add(tagName)
33
+ promises.push(populateDefaultStylesForTagName(tagName))
34
+ }
35
+ await Promise.all(promises)
36
+ }
37
+
32
38
  readRootElementStyles(rootElement: Element) {
33
39
  // when reading a root, we always apply _all_ the styles, even if they match the defaults
34
40
  this.readElementStyles(rootElement, {
@@ -239,15 +245,17 @@ function styleFromComputedStyleMap(
239
245
  { defaultStyles, parentStyles }: ReadStyleOpts
240
246
  ) {
241
247
  const styles: Record<string, string> = {}
248
+ const currentColor = style.get('color')?.toString() || ''
249
+ const ruleOptions = { currentColor, parentStyles, defaultStyles, styles: style }
242
250
  for (const property of style.keys()) {
243
251
  if (!shouldIncludeCssProperty(property)) continue
244
252
 
245
253
  const value = style.get(property)!.toString()
246
254
 
247
255
  if (defaultStyles[property] === value) continue
248
- if (parentStyles[property] === value && isPropertyInherited(property)) continue
249
- if (isPropertyCoveredByCurrentColor(style.get('color')?.toString() || '', property, value))
250
- continue
256
+
257
+ const rule = getOwnProperty(cssRules, property)
258
+ if (rule && rule(value, property, ruleOptions)) continue
251
259
 
252
260
  styles[property] = value
253
261
  }
@@ -260,14 +268,18 @@ function styleFromComputedStyle(
260
268
  { defaultStyles, parentStyles }: ReadStyleOpts
261
269
  ) {
262
270
  const styles: Record<string, string> = {}
271
+ const currentColor = style.color
272
+ const ruleOptions = { currentColor, parentStyles, defaultStyles, styles: style }
273
+
263
274
  for (const property in style) {
264
275
  if (!shouldIncludeCssProperty(property)) continue
265
276
 
266
277
  const value = style.getPropertyValue(property)
267
278
 
268
279
  if (defaultStyles[property] === value) continue
269
- if (parentStyles[property] === value && isPropertyInherited(property)) continue
270
- if (isPropertyCoveredByCurrentColor(style.color, property, value)) continue
280
+
281
+ const rule = getOwnProperty(cssRules, property)
282
+ if (rule && rule(value, property, ruleOptions)) continue
271
283
 
272
284
  styles[property] = value
273
285
  }
@@ -285,43 +297,88 @@ function formatCss(style: ReadonlyStyles) {
285
297
  // when we're figuring out the default values for a tag, we need read them from a separate document
286
298
  // so they're not affected by the current document's styles
287
299
  let defaultStyleFrame:
288
- | { iframe: HTMLIFrameElement; foreignObject: SVGForeignObjectElement; document: Document }
300
+ | Promise<{
301
+ iframe: HTMLIFrameElement
302
+ foreignObject: SVGForeignObjectElement
303
+ document: Document
304
+ }>
289
305
  | undefined
290
- const defaultStylesByTagName: Record<string, ReadonlyStyles> = {}
306
+
307
+ const defaultStylesByTagName: Record<
308
+ string,
309
+ | { type: 'resolved'; styles: ReadonlyStyles; promise: Promise<ReadonlyStyles> }
310
+ | { type: 'pending'; promise: Promise<ReadonlyStyles> }
311
+ > = {}
312
+
313
+ const emptyFrameBlob = new Blob(
314
+ ['<svg xmlns="http://www.w3.org/2000/svg"><foreignObject/></svg>'],
315
+ { type: 'image/svg+xml' }
316
+ )
317
+ const emptyFrameUrl = URL.createObjectURL(emptyFrameBlob)
318
+
291
319
  function getDefaultStyleFrame() {
292
320
  if (!defaultStyleFrame) {
293
- const frame = document.createElement('iframe')
294
- frame.style.display = 'none'
295
- document.body.appendChild(frame)
296
- const frameDocument = assertExists(frame.contentDocument, 'frame must have a document')
297
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
298
- const foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject')
299
- svg.appendChild(foreignObject)
300
- frameDocument.body.appendChild(svg)
301
- defaultStyleFrame = { iframe: frame, foreignObject, document: frameDocument }
321
+ defaultStyleFrame = new Promise((resolve) => {
322
+ const frame = document.createElement('iframe')
323
+ Object.assign(frame.style, {
324
+ position: 'absolute',
325
+ top: '-10000px',
326
+ left: '-10000px',
327
+ width: '1px',
328
+ height: '1px',
329
+ opacity: '0',
330
+ pointerEvents: 'none',
331
+ })
332
+
333
+ frame.onload = () => {
334
+ const contentDocument = frame.contentDocument!
335
+ const foreignObject = contentDocument.querySelector('foreignObject')!
336
+ resolve({ iframe: frame, foreignObject, document: contentDocument })
337
+ }
338
+
339
+ frame.src = emptyFrameUrl
340
+ document.body.appendChild(frame)
341
+ })
302
342
  }
303
343
  return defaultStyleFrame
304
344
  }
305
345
 
306
346
  function destroyDefaultStyleFrame() {
307
347
  if (defaultStyleFrame) {
308
- document.body.removeChild(defaultStyleFrame.iframe)
348
+ defaultStyleFrame.then(({ iframe }) => document.body.removeChild(iframe))
309
349
  defaultStyleFrame = undefined
310
350
  }
311
351
  }
312
352
 
313
353
  const defaultStyleReadOptions: ReadStyleOpts = { defaultStyles: NO_STYLES, parentStyles: NO_STYLES }
314
- function getDefaultStylesForTagName(tagName: string) {
315
- let existing = defaultStylesByTagName[tagName]
316
- if (!existing) {
317
- const { foreignObject, document } = getDefaultStyleFrame()
318
- const element = document.createElement(tagName)
354
+ function populateDefaultStylesForTagName(tagName: string) {
355
+ const existing = defaultStylesByTagName[tagName]
356
+ if (existing && existing.type === 'resolved') {
357
+ return existing.promise
358
+ }
359
+
360
+ if (existing && existing.type === 'pending') {
361
+ return existing.promise
362
+ }
363
+
364
+ const promise = getDefaultStyleFrame().then(({ foreignObject, document }) => {
365
+ const element = document.createElementNS('http://www.w3.org/1999/xhtml', tagName)
366
+ element.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')
319
367
  foreignObject.appendChild(element)
320
- existing = element.computedStyleMap
368
+ const styles = element.computedStyleMap
321
369
  ? styleFromComputedStyleMap(element.computedStyleMap(), defaultStyleReadOptions)
322
370
  : styleFromComputedStyle(getComputedStyle(element), defaultStyleReadOptions)
323
371
  foreignObject.removeChild(element)
324
- defaultStylesByTagName[tagName] = existing
325
- }
326
- return existing
372
+ defaultStylesByTagName[tagName] = { type: 'resolved', styles, promise }
373
+ return styles
374
+ })
375
+
376
+ defaultStylesByTagName[tagName] = { type: 'pending', promise }
377
+ return promise
378
+ }
379
+
380
+ function getDefaultStylesForTagName(tagName: string) {
381
+ const existing = defaultStylesByTagName[tagName]
382
+ assert(existing && existing.type === 'resolved', 'default styles must be populated & resolved')
383
+ return existing.styles
327
384
  }
@@ -0,0 +1,132 @@
1
+ export type Styles = { [K in string]?: string }
2
+ export type ReadonlyStyles = { readonly [K in string]?: string }
3
+
4
+ type CanSkipRule = (
5
+ value: string,
6
+ property: string,
7
+ options: {
8
+ styles: StylePropertyMapReadOnly | CSSStyleDeclaration
9
+ parentStyles: ReadonlyStyles
10
+ defaultStyles: ReadonlyStyles
11
+ currentColor: string
12
+ }
13
+ ) => boolean
14
+
15
+ const getStyle = (styles: StylePropertyMapReadOnly | CSSStyleDeclaration, property: string) => {
16
+ if (styles instanceof CSSStyleDeclaration) {
17
+ return styles.getPropertyValue(property)
18
+ }
19
+ return styles.get(property)?.toString()
20
+ }
21
+
22
+ const isCoveredByCurrentColor: CanSkipRule = (value, property, { currentColor }) => {
23
+ return value === 'currentColor' || value === currentColor
24
+ }
25
+
26
+ const isInherited: CanSkipRule = (value, property, { parentStyles }) => {
27
+ return parentStyles[property] === value
28
+ }
29
+
30
+ const isExcludedBorder =
31
+ (borderDirection: string): CanSkipRule =>
32
+ (value, property, { styles }) => {
33
+ const borderWidth = getStyle(styles, `border-${borderDirection}-width`)
34
+ const borderStyle = getStyle(styles, `border-${borderDirection}-style`)
35
+
36
+ if (borderWidth === '0px') return true
37
+ if (borderStyle === 'none') return true
38
+ return false
39
+ }
40
+
41
+ export const cssRules = {
42
+ // currentColor properties:
43
+ 'border-block-end-color': isCoveredByCurrentColor,
44
+ 'border-block-start-color': isCoveredByCurrentColor,
45
+ 'border-bottom-color': isCoveredByCurrentColor,
46
+ 'border-inline-end-color': isCoveredByCurrentColor,
47
+ 'border-inline-start-color': isCoveredByCurrentColor,
48
+ 'border-left-color': isCoveredByCurrentColor,
49
+ 'border-right-color': isCoveredByCurrentColor,
50
+ 'border-top-color': isCoveredByCurrentColor,
51
+ 'caret-color': isCoveredByCurrentColor,
52
+ 'column-rule-color': isCoveredByCurrentColor,
53
+ 'outline-color': isCoveredByCurrentColor,
54
+ 'text-decoration': (value, property, { currentColor }) => {
55
+ return value === 'none solid currentColor' || value === 'none solid ' + currentColor
56
+ },
57
+ 'text-decoration-color': isCoveredByCurrentColor,
58
+ 'text-emphasis-color': isCoveredByCurrentColor,
59
+
60
+ // inherited properties:
61
+ 'border-collapse': isInherited,
62
+ 'border-spacing': isInherited,
63
+ 'caption-side': isInherited,
64
+ // N.B. We shouldn't inherit 'color' because there's some UA styling, e.g. `mark` elements
65
+ // 'color': isInherited,
66
+ cursor: isInherited,
67
+ direction: isInherited,
68
+ 'empty-cells': isInherited,
69
+ 'font-family': isInherited,
70
+ 'font-size': isInherited,
71
+ 'font-style': isInherited,
72
+ 'font-variant': isInherited,
73
+ 'font-weight': isInherited,
74
+ 'font-size-adjust': isInherited,
75
+ 'font-stretch': isInherited,
76
+ font: isInherited,
77
+ 'letter-spacing': isInherited,
78
+ 'line-height': isInherited,
79
+ 'list-style-image': isInherited,
80
+ 'list-style-position': isInherited,
81
+ 'list-style-type': isInherited,
82
+ 'list-style': isInherited,
83
+ orphans: isInherited,
84
+ 'overflow-wrap': isInherited,
85
+ quotes: isInherited,
86
+ 'stroke-linecap': isInherited,
87
+ 'stroke-linejoin': isInherited,
88
+ 'tab-size': isInherited,
89
+ 'text-align': isInherited,
90
+ 'text-align-last': isInherited,
91
+ 'text-indent': isInherited,
92
+ 'text-justify': isInherited,
93
+ 'text-shadow': isInherited,
94
+ 'text-transform': isInherited,
95
+ visibility: isInherited,
96
+ 'white-space': isInherited,
97
+ 'white-space-collapse': isInherited,
98
+ widows: isInherited,
99
+ 'word-break': isInherited,
100
+ 'word-spacing': isInherited,
101
+ 'word-wrap': isInherited,
102
+
103
+ // special border cases - we have a weird case (tailwind seems to trigger this) where all
104
+ // border-styles sometimes get set to 'solid', but the border-width is 0 so they don't render.
105
+ // but in SVGs, **sometimes**, the border-width defaults (i think from a UA style-sheet? but
106
+ // honestly can't tell) to 1.5px so the border displays. we work around this by only including
107
+ // border styles at all if both the border-width and border-style are set to something that
108
+ // would show a border.
109
+ 'border-top': isExcludedBorder('top'),
110
+ 'border-right': isExcludedBorder('right'),
111
+ 'border-bottom': isExcludedBorder('bottom'),
112
+ 'border-left': isExcludedBorder('left'),
113
+ 'border-block-end': isExcludedBorder('block-end'),
114
+ 'border-block-start': isExcludedBorder('block-start'),
115
+ 'border-inline-end': isExcludedBorder('inline-end'),
116
+ 'border-inline-start': isExcludedBorder('inline-start'),
117
+ 'border-top-style': isExcludedBorder('top'),
118
+ 'border-right-style': isExcludedBorder('right'),
119
+ 'border-bottom-style': isExcludedBorder('bottom'),
120
+ 'border-left-style': isExcludedBorder('left'),
121
+ 'border-block-end-style': isExcludedBorder('block-end'),
122
+ 'border-block-start-style': isExcludedBorder('block-start'),
123
+ 'border-inline-end-style': isExcludedBorder('inline-end'),
124
+ 'border-inline-start-style': isExcludedBorder('inline-start'),
125
+ 'border-top-width': isExcludedBorder('top'),
126
+ 'border-right-width': isExcludedBorder('right'),
127
+ 'border-bottom-width': isExcludedBorder('bottom'),
128
+ 'border-left-width': isExcludedBorder('left'),
129
+ 'border-block-end-width': isExcludedBorder('block-end'),
130
+ 'border-block-start-width': isExcludedBorder('block-start'),
131
+ 'border-inline-end-width': isExcludedBorder('inline-end'),
132
+ } satisfies Record<string, CanSkipRule>
@@ -101,11 +101,15 @@ async function applyChangesToForeignObjects(svg: SVGSVGElement) {
101
101
  // urls, and things like videos will be converted to images.
102
102
  await Promise.all(foreignObjectChildren.map((el) => embedMedia(el as HTMLElement)))
103
103
 
104
+ await styleEmbedder.collectDefaultStyles([
105
+ ...svg.querySelectorAll('foreignObject.tl-export-embed-styles *'),
106
+ ])
107
+
104
108
  // read the computed styles of every element (+ it's children & pseudo-elements) in the
105
109
  // document. we do this in a single pass before we start embedding any CSS stuff to avoid
106
110
  // constantly forcing the browser to recompute styles & layout.
107
111
  for (const el of foreignObjectChildren) {
108
- styleEmbedder.readRootElementStyles(el as HTMLElement)
112
+ await styleEmbedder.readRootElementStyles(el as HTMLElement)
109
113
  }
110
114
 
111
115
  // fetch any resources that we need to embed in the CSS, like background images.
@@ -110,82 +110,3 @@ export function parseCssValueUrls(value: string) {
110
110
  url: m[1] || m[2] || m[3],
111
111
  }))
112
112
  }
113
-
114
- const currentColorProperties = new Set([
115
- 'border-block-end-color',
116
- 'border-block-start-color',
117
- 'border-bottom-color',
118
- 'border-inline-end-color',
119
- 'border-inline-start-color',
120
- 'border-left-color',
121
- 'border-right-color',
122
- 'border-top-color',
123
- 'caret-color',
124
- 'column-rule-color',
125
- 'outline-color',
126
- 'text-decoration',
127
- 'text-decoration-color',
128
- 'text-emphasis-color',
129
- ])
130
-
131
- export function isPropertyCoveredByCurrentColor(
132
- currentColor: string,
133
- property: string,
134
- value: string
135
- ) {
136
- if (currentColorProperties.has(property)) {
137
- return (
138
- value === 'currentColor' ||
139
- value === currentColor ||
140
- (property === 'text-decoration' && value === `none solid ${currentColor}`)
141
- )
142
- }
143
- }
144
-
145
- const inheritedProperties = new Set([
146
- 'border-collapse',
147
- 'border-spacing',
148
- 'caption-side',
149
- // N.B. We shouldn't inherit 'color' because there's some UA styling, e.g. `mark` elements
150
- // 'color',
151
- 'cursor',
152
- 'direction',
153
- 'empty-cells',
154
- 'font-family',
155
- 'font-size',
156
- 'font-style',
157
- 'font-variant',
158
- 'font-weight',
159
- 'font-size-adjust',
160
- 'font-stretch',
161
- 'font',
162
- 'letter-spacing',
163
- 'line-height',
164
- 'list-style-image',
165
- 'list-style-position',
166
- 'list-style-type',
167
- 'list-style',
168
- 'orphans',
169
- 'overflow-wrap',
170
- 'quotes',
171
- 'stroke-linecap',
172
- 'stroke-linejoin',
173
- 'tab-size',
174
- 'text-align',
175
- 'text-align-last',
176
- 'text-indent',
177
- 'text-justify',
178
- 'text-shadow',
179
- 'text-transform',
180
- 'visibility',
181
- 'white-space',
182
- 'white-space-collapse',
183
- 'widows',
184
- 'word-break',
185
- 'word-spacing',
186
- 'word-wrap',
187
- ])
188
-
189
- export function isPropertyInherited(property: string) {
190
- return inheritedProperties.has(property)
191
- }
package/src/version.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '3.12.0-canary.f3ad945f4591'
4
+ export const version = '3.12.0-internal.624e32507d98'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-04-01T15:25:53.203Z',
8
- patch: '2025-04-01T15:25:53.203Z',
7
+ minor: '2025-04-01T17:14:18.076Z',
8
+ patch: '2025-04-01T17:14:18.076Z',
9
9
  }