@tldraw/editor 3.11.0-canary.f529c521e249 → 3.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/CHANGELOG.md +114 -2
  2. package/dist-cjs/index.d.ts +23 -9
  3. package/dist-cjs/index.js +1 -1
  4. package/dist-cjs/index.js.map +2 -2
  5. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +4 -4
  6. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  7. package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js +34 -26
  8. package/dist-cjs/lib/components/default-components/DefaultShapeIndicators.js.map +2 -2
  9. package/dist-cjs/lib/config/TLUserPreferences.js +1 -1
  10. package/dist-cjs/lib/config/TLUserPreferences.js.map +1 -1
  11. package/dist-cjs/lib/config/createTLStore.js +2 -1
  12. package/dist-cjs/lib/config/createTLStore.js.map +2 -2
  13. package/dist-cjs/lib/constants.js +1 -1
  14. package/dist-cjs/lib/constants.js.map +2 -2
  15. package/dist-cjs/lib/editor/Editor.js +25 -26
  16. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  17. package/dist-cjs/lib/editor/managers/FocusManager.js +15 -0
  18. package/dist-cjs/lib/editor/managers/FocusManager.js.map +2 -2
  19. package/dist-cjs/lib/editor/managers/UserPreferencesManager.js +1 -1
  20. package/dist-cjs/lib/editor/managers/UserPreferencesManager.js.map +2 -2
  21. package/dist-cjs/lib/exports/StyleEmbedder.js +32 -3
  22. package/dist-cjs/lib/exports/StyleEmbedder.js.map +2 -2
  23. package/dist-cjs/lib/exports/cssRules.js +127 -0
  24. package/dist-cjs/lib/exports/cssRules.js.map +7 -0
  25. package/dist-cjs/lib/exports/exportToSvg.js.map +1 -1
  26. package/dist-cjs/lib/exports/getSvgJsx.js +1 -1
  27. package/dist-cjs/lib/exports/getSvgJsx.js.map +2 -2
  28. package/dist-cjs/lib/exports/parseCss.js +0 -44
  29. package/dist-cjs/lib/exports/parseCss.js.map +2 -2
  30. package/dist-cjs/lib/hooks/useGestureEvents.js +12 -6
  31. package/dist-cjs/lib/hooks/useGestureEvents.js.map +2 -2
  32. package/dist-cjs/lib/hooks/useLocalStore.js +3 -0
  33. package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
  34. package/dist-cjs/lib/license/Watermark.js +7 -1
  35. package/dist-cjs/lib/license/Watermark.js.map +2 -2
  36. package/dist-cjs/lib/options.js +2 -1
  37. package/dist-cjs/lib/options.js.map +2 -2
  38. package/dist-cjs/lib/utils/sync/LocalIndexedDb.js +8 -0
  39. package/dist-cjs/lib/utils/sync/LocalIndexedDb.js.map +2 -2
  40. package/dist-cjs/version.js +3 -3
  41. package/dist-cjs/version.js.map +1 -1
  42. package/dist-esm/index.d.mts +23 -9
  43. package/dist-esm/index.mjs +4 -2
  44. package/dist-esm/index.mjs.map +2 -2
  45. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +4 -4
  46. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  47. package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs +34 -26
  48. package/dist-esm/lib/components/default-components/DefaultShapeIndicators.mjs.map +2 -2
  49. package/dist-esm/lib/config/TLUserPreferences.mjs +1 -1
  50. package/dist-esm/lib/config/TLUserPreferences.mjs.map +1 -1
  51. package/dist-esm/lib/config/createTLStore.mjs +2 -1
  52. package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
  53. package/dist-esm/lib/constants.mjs +1 -1
  54. package/dist-esm/lib/constants.mjs.map +2 -2
  55. package/dist-esm/lib/editor/Editor.mjs +25 -26
  56. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  57. package/dist-esm/lib/editor/managers/FocusManager.mjs +15 -0
  58. package/dist-esm/lib/editor/managers/FocusManager.mjs.map +2 -2
  59. package/dist-esm/lib/editor/managers/UserPreferencesManager.mjs +1 -1
  60. package/dist-esm/lib/editor/managers/UserPreferencesManager.mjs.map +2 -2
  61. package/dist-esm/lib/exports/StyleEmbedder.mjs +34 -5
  62. package/dist-esm/lib/exports/StyleEmbedder.mjs.map +2 -2
  63. package/dist-esm/lib/exports/cssRules.mjs +107 -0
  64. package/dist-esm/lib/exports/cssRules.mjs.map +7 -0
  65. package/dist-esm/lib/exports/exportToSvg.mjs.map +1 -1
  66. package/dist-esm/lib/exports/getSvgJsx.mjs +1 -1
  67. package/dist-esm/lib/exports/getSvgJsx.mjs.map +2 -2
  68. package/dist-esm/lib/exports/parseCss.mjs +0 -44
  69. package/dist-esm/lib/exports/parseCss.mjs.map +2 -2
  70. package/dist-esm/lib/hooks/useGestureEvents.mjs +12 -6
  71. package/dist-esm/lib/hooks/useGestureEvents.mjs.map +2 -2
  72. package/dist-esm/lib/hooks/useLocalStore.mjs +3 -0
  73. package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
  74. package/dist-esm/lib/license/Watermark.mjs +7 -1
  75. package/dist-esm/lib/license/Watermark.mjs.map +2 -2
  76. package/dist-esm/lib/options.mjs +2 -1
  77. package/dist-esm/lib/options.mjs.map +2 -2
  78. package/dist-esm/lib/utils/sync/LocalIndexedDb.mjs +8 -0
  79. package/dist-esm/lib/utils/sync/LocalIndexedDb.mjs.map +2 -2
  80. package/dist-esm/version.mjs +3 -3
  81. package/dist-esm/version.mjs.map +1 -1
  82. package/editor.css +11 -1
  83. package/package.json +10 -7
  84. package/src/index.ts +4 -1
  85. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +4 -4
  86. package/src/lib/components/default-components/DefaultShapeIndicators.tsx +52 -31
  87. package/src/lib/config/TLUserPreferences.ts +1 -1
  88. package/src/lib/config/createTLStore.ts +1 -0
  89. package/src/lib/constants.ts +1 -1
  90. package/src/lib/editor/Editor.ts +27 -29
  91. package/src/lib/editor/managers/FocusManager.ts +18 -0
  92. package/src/lib/editor/managers/UserPreferencesManager.ts +1 -1
  93. package/src/lib/exports/StyleEmbedder.ts +38 -9
  94. package/src/lib/exports/cssRules.ts +126 -0
  95. package/src/lib/exports/exportToSvg.tsx +1 -1
  96. package/src/lib/exports/getSvgJsx.tsx +1 -1
  97. package/src/lib/exports/parseCss.ts +0 -45
  98. package/src/lib/hooks/useGestureEvents.ts +12 -6
  99. package/src/lib/hooks/useLocalStore.ts +3 -0
  100. package/src/lib/license/Watermark.tsx +7 -1
  101. package/src/lib/options.ts +6 -0
  102. package/src/lib/utils/sync/LocalIndexedDb.ts +9 -0
  103. package/src/version.ts +3 -3
@@ -9,15 +9,15 @@ import { useEditorComponents } from '../../hooks/useEditorComponents'
9
9
  import { OptionalErrorBoundary } from '../ErrorBoundary'
10
10
 
11
11
  // need an extra layer of indirection here to allow hooks to be used inside the indicator render
12
- const EvenInnererIndicator = ({ shape, util }: { shape: TLShape; util: ShapeUtil<any> }) => {
12
+ const EvenInnererIndicator = memo(({ shape, util }: { shape: TLShape; util: ShapeUtil<any> }) => {
13
13
  return useStateTracking('Indicator: ' + shape.type, () =>
14
14
  // always fetch the latest shape from the store even if the props/meta have not changed, to avoid
15
15
  // calling the render method with stale data.
16
16
  util.indicator(util.editor.store.unsafeGetWithoutCapture(shape.id) as TLShape)
17
17
  )
18
- }
18
+ })
19
19
 
20
- const InnerIndicator = ({ editor, id }: { editor: Editor; id: TLShapeId }) => {
20
+ const InnerIndicator = memo(({ editor, id }: { editor: Editor; id: TLShapeId }) => {
21
21
  const shape = useValue('shape for indicator', () => editor.store.get(id), [editor, id])
22
22
 
23
23
  const { ShapeIndicatorErrorFallback } = useEditorComponents()
@@ -34,7 +34,7 @@ const InnerIndicator = ({ editor, id }: { editor: Editor; id: TLShapeId }) => {
34
34
  <EvenInnererIndicator key={shape.id} shape={shape} util={editor.getShapeUtil(shape)} />
35
35
  </OptionalErrorBoundary>
36
36
  )
37
- }
37
+ })
38
38
 
39
39
  /** @public */
40
40
  export interface TLShapeIndicatorProps {
@@ -4,10 +4,24 @@ import { memo, useRef } from 'react'
4
4
  import { useEditor } from '../../hooks/useEditor'
5
5
  import { useEditorComponents } from '../../hooks/useEditorComponents'
6
6
 
7
+ /** @public */
8
+ export interface TLShapeIndicatorsProps {
9
+ /** Whether to hide all of the indicators */
10
+ hideAll?: boolean
11
+ /** Whether to show all of the indicators */
12
+ showAll?: boolean
13
+ }
14
+
7
15
  /** @public @react */
8
- export const DefaultShapeIndicators = memo(function DefaultShapeIndicators() {
16
+ export const DefaultShapeIndicators = memo(function DefaultShapeIndicators({
17
+ hideAll,
18
+ showAll,
19
+ }: TLShapeIndicatorsProps) {
9
20
  const editor = useEditor()
10
21
 
22
+ if (hideAll && showAll)
23
+ throw Error('You cannot set both hideAll and showAll props to true, cmon now')
24
+
11
25
  const rPreviousSelectedShapeIds = useRef<Set<TLShapeId>>(new Set())
12
26
 
13
27
  const idsToDisplay = useValue(
@@ -16,34 +30,38 @@ export const DefaultShapeIndicators = memo(function DefaultShapeIndicators() {
16
30
  const prev = rPreviousSelectedShapeIds.current
17
31
  const next = new Set<TLShapeId>()
18
32
 
19
- if (
20
- // We only show indicators when in the following states...
21
- editor.isInAny(
22
- 'select.idle',
23
- 'select.brushing',
24
- 'select.scribble_brushing',
25
- 'select.editing_shape',
26
- 'select.pointing_shape',
27
- 'select.pointing_selection',
28
- 'select.pointing_handle'
29
- ) &&
30
- // ...but we hide indicators when we've just changed a style (so that the user can see the change)
31
- !editor.getInstanceState().isChangingStyle
32
- ) {
33
- // We always want to show indicators for the selected shapes, if any
34
- const selected = editor.getSelectedShapeIds()
35
- for (const id of selected) {
36
- next.add(id)
37
- }
33
+ const instanceState = editor.getInstanceState()
38
34
 
39
- // If we're idle or editing a shape, we want to also show an indicator for the hovered shape, if any
40
- if (editor.isInAny('select.idle', 'select.editing_shape')) {
41
- const instanceState = editor.getInstanceState()
42
- if (instanceState.isHoveringCanvas && !instanceState.isCoarsePointer) {
43
- const hovered = editor.getHoveredShapeId()
44
- if (hovered) next.add(hovered)
45
- }
46
- }
35
+ const isChangingStyle = instanceState.isChangingStyle
36
+
37
+ // todo: this is tldraw specific and is duplicated at the tldraw layer. What should we do here instead?
38
+
39
+ const isIdleOrEditing = editor.isInAny('select.idle', 'select.editing_shape')
40
+
41
+ const isInSelectState = editor.isInAny(
42
+ 'select.brushing',
43
+ 'select.scribble_brushing',
44
+ 'select.pointing_shape',
45
+ 'select.pointing_selection',
46
+ 'select.pointing_handle'
47
+ )
48
+
49
+ // We hide all indicators if we're changing style or in certain interactions
50
+ // todo: move this to some kind of Tool.hideIndicators property
51
+ if (isChangingStyle || !(isIdleOrEditing || isInSelectState)) {
52
+ rPreviousSelectedShapeIds.current = next
53
+ return next
54
+ }
55
+
56
+ // We always want to show indicators for the selected shapes, if any
57
+ for (const id of editor.getSelectedShapeIds()) {
58
+ next.add(id)
59
+ }
60
+
61
+ // If we're idle or editing a shape, we want to also show an indicator for the hovered shape, if any
62
+ if (isIdleOrEditing && instanceState.isHoveringCanvas && !instanceState.isCoarsePointer) {
63
+ const hovered = editor.getHoveredShapeId()
64
+ if (hovered) next.add(hovered)
47
65
  }
48
66
 
49
67
  // Ok, has anything changed?
@@ -54,7 +72,7 @@ export const DefaultShapeIndicators = memo(function DefaultShapeIndicators() {
54
72
  return next
55
73
  }
56
74
 
57
- // If any of the new ids are not in the previous set, then the selection has changed
75
+ // Set difference check
58
76
  for (const id of next) {
59
77
  if (!prev.has(id)) {
60
78
  rPreviousSelectedShapeIds.current = next
@@ -62,7 +80,6 @@ export const DefaultShapeIndicators = memo(function DefaultShapeIndicators() {
62
80
  }
63
81
  }
64
82
 
65
- // If nothing has changed, then return the previous value
66
83
  return prev
67
84
  },
68
85
  [editor]
@@ -75,6 +92,10 @@ export const DefaultShapeIndicators = memo(function DefaultShapeIndicators() {
75
92
  if (!ShapeIndicator) return null
76
93
 
77
94
  return renderingShapes.map(({ id }) => (
78
- <ShapeIndicator key={id + '_indicator'} shapeId={id} hidden={!idsToDisplay.has(id)} />
95
+ <ShapeIndicator
96
+ key={id + '_indicator'}
97
+ shapeId={id}
98
+ hidden={!showAll && (hideAll || !idsToDisplay.has(id))}
99
+ />
79
100
  ))
80
101
  })
@@ -132,7 +132,7 @@ export function userPrefersReducedMotion() {
132
132
 
133
133
  /** @public */
134
134
  export const defaultUserPreferences = Object.freeze({
135
- name: 'New User',
135
+ name: '',
136
136
  locale: getDefaultTranslationLocale(),
137
137
  color: getRandomColor(),
138
138
 
@@ -119,6 +119,7 @@ export function createTLStore({
119
119
  assets: {
120
120
  upload: assets.upload,
121
121
  resolve: assets.resolve ?? defaultAssetResolve,
122
+ remove: assets.remove ?? (() => Promise.resolve()),
122
123
  },
123
124
  onMount: (editor) => {
124
125
  assert(editor instanceof Editor)
@@ -7,7 +7,7 @@ export const DEFAULT_CAMERA_OPTIONS: TLCameraOptions = {
7
7
  wheelBehavior: 'pan',
8
8
  panSpeed: 1,
9
9
  zoomSpeed: 1,
10
- zoomSteps: [0.1, 0.25, 0.5, 1, 2, 4, 8],
10
+ zoomSteps: [0.05, 0.1, 0.25, 0.5, 1, 2, 4, 8],
11
11
  }
12
12
 
13
13
  /** @internal */
@@ -2102,12 +2102,6 @@ export class Editor extends EventEmitter<TLEventMap> {
2102
2102
  * @public
2103
2103
  */
2104
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
2105
  this._currentRichTextEditor.set(textEditor)
2112
2106
  return this
2113
2107
  }
@@ -4218,7 +4212,13 @@ export class Editor extends EventEmitter<TLEventMap> {
4218
4212
  : (assets as TLAsset[]).map((a) => a.id)
4219
4213
  if (ids.length <= 0) return this
4220
4214
 
4221
- this.run(() => this.store.remove(ids), { history: 'ignore' })
4215
+ this.run(
4216
+ () => {
4217
+ this.store.props.assets.remove?.(ids)
4218
+ this.store.remove(ids)
4219
+ },
4220
+ { history: 'ignore' }
4221
+ )
4222
4222
  return this
4223
4223
  }
4224
4224
 
@@ -6328,21 +6328,22 @@ export class Editor extends EventEmitter<TLEventMap> {
6328
6328
  *
6329
6329
  * @example
6330
6330
  * ```ts
6331
- * editor.stackShapes([box1, box2], 'horizontal', 32)
6332
- * editor.stackShapes(editor.getSelectedShapeIds(), 'horizontal', 32)
6331
+ * editor.stackShapes([box1, box2], 'horizontal')
6332
+ * editor.stackShapes(editor.getSelectedShapeIds(), 'horizontal')
6333
6333
  * ```
6334
6334
  *
6335
6335
  * @param shapes - The shapes (or shape ids) to stack.
6336
6336
  * @param operation - Whether to stack horizontally or vertically.
6337
- * @param gap - The gap to leave between shapes.
6337
+ * @param gap - The gap to leave between shapes. By default, uses the editor's `adjacentShapeMargin` option.
6338
6338
  *
6339
6339
  * @public
6340
6340
  */
6341
6341
  stackShapes(
6342
6342
  shapes: TLShapeId[] | TLShape[],
6343
6343
  operation: 'horizontal' | 'vertical',
6344
- gap: number
6344
+ gap?: number
6345
6345
  ): this {
6346
+ const _gap = gap ?? this.options.adjacentShapeMargin
6346
6347
  const ids =
6347
6348
  typeof shapes[0] === 'string'
6348
6349
  ? (shapes as TLShapeId[])
@@ -6400,7 +6401,7 @@ export class Editor extends EventEmitter<TLEventMap> {
6400
6401
  }
6401
6402
 
6402
6403
  const len = shapeClustersToStack.length
6403
- if ((gap === 0 && len < 3) || len < 2) return this
6404
+ if ((_gap === 0 && len < 3) || len < 2) return this
6404
6405
 
6405
6406
  let val: 'x' | 'y'
6406
6407
  let min: 'minX' | 'minY'
@@ -6421,7 +6422,7 @@ export class Editor extends EventEmitter<TLEventMap> {
6421
6422
 
6422
6423
  let shapeGap: number = 0
6423
6424
 
6424
- if (gap === 0) {
6425
+ if (_gap === 0) {
6425
6426
  // note: this is not used in the current tldraw.com; there we use a specified stack
6426
6427
 
6427
6428
  const gaps: Record<number, number> = {}
@@ -6461,7 +6462,7 @@ export class Editor extends EventEmitter<TLEventMap> {
6461
6462
  }
6462
6463
  } else {
6463
6464
  // If a gap was provided, then use that instead.
6464
- shapeGap = gap
6465
+ shapeGap = _gap
6465
6466
  }
6466
6467
 
6467
6468
  const changes: TLShapePartial[] = []
@@ -6500,17 +6501,19 @@ export class Editor extends EventEmitter<TLEventMap> {
6500
6501
  *
6501
6502
  * @example
6502
6503
  * ```ts
6503
- * editor.packShapes([box1, box2], 32)
6504
+ * editor.packShapes([box1, box2])
6504
6505
  * editor.packShapes(editor.getSelectedShapeIds(), 32)
6505
6506
  * ```
6506
6507
  *
6507
6508
  *
6508
6509
  * @param shapes - The shapes (or shape ids) to pack.
6509
- * @param gap - The padding to apply to the packed shapes. Defaults to 16.
6510
+ * @param gap - The padding to apply to the packed shapes. Defaults to the editor's `adjacentShapeMargin` option.
6510
6511
  */
6511
- packShapes(shapes: TLShapeId[] | TLShape[], gap: number): this {
6512
+ packShapes(shapes: TLShapeId[] | TLShape[], _gap?: number): this {
6512
6513
  if (this.getIsReadonly()) return this
6513
6514
 
6515
+ const gap = _gap ?? this.options.adjacentShapeMargin
6516
+
6514
6517
  const ids =
6515
6518
  typeof shapes[0] === 'string'
6516
6519
  ? (shapes as TLShapeId[])
@@ -9846,12 +9849,12 @@ export class Editor extends EventEmitter<TLEventMap> {
9846
9849
 
9847
9850
  const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera())
9848
9851
 
9849
- const { panSpeed, zoomSpeed } = cameraOptions
9852
+ const { panSpeed } = cameraOptions
9850
9853
  this._setCamera(
9851
9854
  new Vec(
9852
- cx + (dx * panSpeed) / cz - x / cz + x / (z * zoomSpeed),
9853
- cy + (dy * panSpeed) / cz - y / cz + y / (z * zoomSpeed),
9854
- z * zoomSpeed
9855
+ cx + (dx * panSpeed) / cz - x / cz + x / z,
9856
+ cy + (dy * panSpeed) / cz - y / cz + y / z,
9857
+ z
9855
9858
  ),
9856
9859
  { immediate: true }
9857
9860
  )
@@ -9926,14 +9929,9 @@ export class Editor extends EventEmitter<TLEventMap> {
9926
9929
  }
9927
9930
 
9928
9931
  const zoom = cz + (delta ?? 0) * zoomSpeed * cz
9929
- this._setCamera(
9930
- new Vec(
9931
- cx + (x / zoom - x) - (x / cz - x),
9932
- cy + (y / zoom - y) - (y / cz - y),
9933
- zoom
9934
- ),
9935
- { immediate: true }
9936
- )
9932
+ this._setCamera(new Vec(cx + x / zoom - x / cz, cy + y / zoom - y / cz, zoom), {
9933
+ immediate: true,
9934
+ })
9937
9935
  this.maybeTrackPerformance('Zooming')
9938
9936
  return
9939
9937
  }
@@ -30,6 +30,9 @@ export class FocusManager {
30
30
  editor.updateInstanceState({ isFocused: !!autoFocus })
31
31
  }
32
32
  this.updateContainerClass()
33
+
34
+ document.body.addEventListener('keydown', this.handleKeyDown.bind(this))
35
+ document.body.addEventListener('mousedown', this.handleMouseDown.bind(this))
33
36
  }
34
37
 
35
38
  /**
@@ -50,6 +53,19 @@ export class FocusManager {
50
53
  } else {
51
54
  container.classList.remove('tl-container__focused')
52
55
  }
56
+ container.classList.add('tl-container__no-focus-ring')
57
+ }
58
+
59
+ private handleKeyDown(keyEvent: KeyboardEvent) {
60
+ const container = this.editor.getContainer()
61
+ if (keyEvent.key === 'Tab') {
62
+ container.classList.remove('tl-container__no-focus-ring')
63
+ }
64
+ }
65
+
66
+ private handleMouseDown() {
67
+ const container = this.editor.getContainer()
68
+ container.classList.add('tl-container__no-focus-ring')
53
69
  }
54
70
 
55
71
  focus() {
@@ -62,6 +78,8 @@ export class FocusManager {
62
78
  }
63
79
 
64
80
  dispose() {
81
+ document.body.removeEventListener('keydown', this.handleKeyDown.bind(this))
82
+ document.body.removeEventListener('mousedown', this.handleMouseDown.bind(this))
65
83
  this.disposeSideEffectListener?.()
66
84
  }
67
85
  }
@@ -80,7 +80,7 @@ export class UserPreferencesManager {
80
80
  }
81
81
 
82
82
  @computed getName() {
83
- return this.user.userPreferences.get().name ?? defaultUserPreferences.name
83
+ return this.user.userPreferences.get().name?.trim() ?? defaultUserPreferences.name
84
84
  }
85
85
 
86
86
  @computed getLocale() {
@@ -1,5 +1,6 @@
1
- import { assertExists, objectMapValues, uniqueId } from '@tldraw/utils'
1
+ import { assertExists, 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,10 +8,8 @@ import {
7
8
  getRenderedChildren,
8
9
  } from './domUtils'
9
10
  import { resourceToDataUrl } from './fetchCache'
10
- import { isPropertyInherited, parseCssValueUrls, shouldIncludeCssProperty } from './parseCss'
11
+ import { parseCssValueUrls, shouldIncludeCssProperty } from './parseCss'
11
12
 
12
- type Styles = { [K in string]?: string }
13
- type ReadonlyStyles = { readonly [K in string]?: string }
14
13
  const NO_STYLES = {} as const
15
14
 
16
15
  interface ElementStyleInfo {
@@ -53,9 +52,20 @@ export class StyleEmbedder {
53
52
  ? getDefaultStylesForTagName(element.tagName.toLowerCase())
54
53
  : NO_STYLES
55
54
 
56
- const parentStyles = shouldSkipInheritedParentStyles
57
- ? (this.styles.get(element.parentElement as Element)?.self ?? NO_STYLES)
58
- : NO_STYLES
55
+ const parentStyles = Object.assign({}, NO_STYLES) as Styles
56
+ if (shouldSkipInheritedParentStyles) {
57
+ let el = element.parentElement
58
+ // Keep going up the tree to find all the relevant styles
59
+ while (el) {
60
+ const currentStyles = this.styles.get(el)?.self
61
+ for (const style in currentStyles) {
62
+ if (!parentStyles[style]) {
63
+ parentStyles[style] = currentStyles[style]
64
+ }
65
+ }
66
+ el = el.parentElement
67
+ }
68
+ }
59
69
 
60
70
  const info: ElementStyleInfo = {
61
71
  self: styleFromElement(element, { defaultStyles, parentStyles }),
@@ -223,13 +233,22 @@ function styleFromComputedStyleMap(
223
233
  { defaultStyles, parentStyles }: ReadStyleOpts
224
234
  ) {
225
235
  const styles: Record<string, string> = {}
236
+ const currentColor = style.get('color')?.toString() || ''
237
+ const ruleOptions = {
238
+ currentColor,
239
+ parentStyles,
240
+ defaultStyles,
241
+ getStyle: (property: string) => style.get(property)?.toString() ?? '',
242
+ }
226
243
  for (const property of style.keys()) {
227
244
  if (!shouldIncludeCssProperty(property)) continue
228
245
 
229
246
  const value = style.get(property)!.toString()
230
247
 
231
248
  if (defaultStyles[property] === value) continue
232
- if (parentStyles[property] === value && isPropertyInherited(property)) continue
249
+
250
+ const rule = getOwnProperty(cssRules, property)
251
+ if (rule && rule(value, property, ruleOptions)) continue
233
252
 
234
253
  styles[property] = value
235
254
  }
@@ -242,13 +261,23 @@ function styleFromComputedStyle(
242
261
  { defaultStyles, parentStyles }: ReadStyleOpts
243
262
  ) {
244
263
  const styles: Record<string, string> = {}
264
+ const currentColor = style.color
265
+ const ruleOptions = {
266
+ currentColor,
267
+ parentStyles,
268
+ defaultStyles,
269
+ getStyle: (property: string) => style.getPropertyValue(property),
270
+ }
271
+
245
272
  for (const property in style) {
246
273
  if (!shouldIncludeCssProperty(property)) continue
247
274
 
248
275
  const value = style.getPropertyValue(property)
249
276
 
250
277
  if (defaultStyles[property] === value) continue
251
- if (parentStyles[property] === value && isPropertyInherited(property)) continue
278
+
279
+ const rule = getOwnProperty(cssRules, property)
280
+ if (rule && rule(value, property, ruleOptions)) continue
252
281
 
253
282
  styles[property] = value
254
283
  }
@@ -0,0 +1,126 @@
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
+ getStyle(property: string): string
9
+ parentStyles: ReadonlyStyles
10
+ defaultStyles: ReadonlyStyles
11
+ currentColor: string
12
+ }
13
+ ) => boolean
14
+
15
+ const isCoveredByCurrentColor: CanSkipRule = (value, property, { currentColor }) => {
16
+ return value === 'currentColor' || value === currentColor
17
+ }
18
+
19
+ const isInherited: CanSkipRule = (value, property, { parentStyles }) => {
20
+ return parentStyles[property] === value
21
+ }
22
+
23
+ // see comment below about why we exclude border styles
24
+ const isExcludedBorder =
25
+ (borderDirection: string): CanSkipRule =>
26
+ (value, property, { getStyle }) => {
27
+ const borderWidth = getStyle(`border-${borderDirection}-width`)
28
+ const borderStyle = getStyle(`border-${borderDirection}-style`)
29
+
30
+ if (borderWidth === '0px') return true
31
+ if (borderStyle === 'none') return true
32
+ return false
33
+ }
34
+
35
+ export const cssRules = {
36
+ // currentColor properties:
37
+ 'border-block-end-color': isCoveredByCurrentColor,
38
+ 'border-block-start-color': isCoveredByCurrentColor,
39
+ 'border-bottom-color': isCoveredByCurrentColor,
40
+ 'border-inline-end-color': isCoveredByCurrentColor,
41
+ 'border-inline-start-color': isCoveredByCurrentColor,
42
+ 'border-left-color': isCoveredByCurrentColor,
43
+ 'border-right-color': isCoveredByCurrentColor,
44
+ 'border-top-color': isCoveredByCurrentColor,
45
+ 'caret-color': isCoveredByCurrentColor,
46
+ 'column-rule-color': isCoveredByCurrentColor,
47
+ 'outline-color': isCoveredByCurrentColor,
48
+ 'text-decoration': (value, property, { currentColor }) => {
49
+ return value === 'none solid currentColor' || value === 'none solid ' + currentColor
50
+ },
51
+ 'text-decoration-color': isCoveredByCurrentColor,
52
+ 'text-emphasis-color': isCoveredByCurrentColor,
53
+
54
+ // inherited properties:
55
+ 'border-collapse': isInherited,
56
+ 'border-spacing': isInherited,
57
+ 'caption-side': isInherited,
58
+ // N.B. We shouldn't inherit 'color' because there's some UA styling, e.g. `mark` elements
59
+ // 'color': isInherited,
60
+ cursor: isInherited,
61
+ direction: isInherited,
62
+ 'empty-cells': isInherited,
63
+ 'font-family': isInherited,
64
+ 'font-size': isInherited,
65
+ 'font-style': isInherited,
66
+ 'font-variant': isInherited,
67
+ 'font-weight': isInherited,
68
+ 'font-size-adjust': isInherited,
69
+ 'font-stretch': isInherited,
70
+ font: isInherited,
71
+ 'letter-spacing': isInherited,
72
+ 'line-height': isInherited,
73
+ 'list-style-image': isInherited,
74
+ 'list-style-position': isInherited,
75
+ 'list-style-type': isInherited,
76
+ 'list-style': isInherited,
77
+ orphans: isInherited,
78
+ 'overflow-wrap': isInherited,
79
+ quotes: isInherited,
80
+ 'stroke-linecap': isInherited,
81
+ 'stroke-linejoin': isInherited,
82
+ 'tab-size': isInherited,
83
+ 'text-align': isInherited,
84
+ 'text-align-last': isInherited,
85
+ 'text-indent': isInherited,
86
+ 'text-justify': isInherited,
87
+ 'text-shadow': isInherited,
88
+ 'text-transform': isInherited,
89
+ visibility: isInherited,
90
+ 'white-space': isInherited,
91
+ 'white-space-collapse': isInherited,
92
+ widows: isInherited,
93
+ 'word-break': isInherited,
94
+ 'word-spacing': isInherited,
95
+ 'word-wrap': isInherited,
96
+
97
+ // special border cases - we have a weird case (tailwind seems to trigger this) where all
98
+ // border-styles sometimes get set to 'solid', but the border-width is 0 so they don't render.
99
+ // but in SVGs, **sometimes**, the border-width defaults (i think from a UA style-sheet? but
100
+ // honestly can't tell) to 1.5px so the border displays. we work around this by only including
101
+ // border styles at all if both the border-width and border-style are set to something that
102
+ // would show a border.
103
+ 'border-top': isExcludedBorder('top'),
104
+ 'border-right': isExcludedBorder('right'),
105
+ 'border-bottom': isExcludedBorder('bottom'),
106
+ 'border-left': isExcludedBorder('left'),
107
+ 'border-block-end': isExcludedBorder('block-end'),
108
+ 'border-block-start': isExcludedBorder('block-start'),
109
+ 'border-inline-end': isExcludedBorder('inline-end'),
110
+ 'border-inline-start': isExcludedBorder('inline-start'),
111
+ 'border-top-style': isExcludedBorder('top'),
112
+ 'border-right-style': isExcludedBorder('right'),
113
+ 'border-bottom-style': isExcludedBorder('bottom'),
114
+ 'border-left-style': isExcludedBorder('left'),
115
+ 'border-block-end-style': isExcludedBorder('block-end'),
116
+ 'border-block-start-style': isExcludedBorder('block-start'),
117
+ 'border-inline-end-style': isExcludedBorder('inline-end'),
118
+ 'border-inline-start-style': isExcludedBorder('inline-start'),
119
+ 'border-top-width': isExcludedBorder('top'),
120
+ 'border-right-width': isExcludedBorder('right'),
121
+ 'border-bottom-width': isExcludedBorder('bottom'),
122
+ 'border-left-width': isExcludedBorder('left'),
123
+ 'border-block-end-width': isExcludedBorder('block-end'),
124
+ 'border-block-start-width': isExcludedBorder('block-start'),
125
+ 'border-inline-end-width': isExcludedBorder('inline-end'),
126
+ } satisfies Record<string, CanSkipRule>
@@ -62,7 +62,7 @@ export async function exportToSvg(
62
62
  const svg = renderTarget.firstElementChild
63
63
  assert(svg instanceof SVGSVGElement, 'Expected an SVG element')
64
64
 
65
- // And apply any changes to <foreignObject> elements that we need to make. Whilst we're in
65
+ // And apply any changes to <foreignObject> elements that we need to make. while we're in
66
66
  // the document, these elements work exactly as we'd expect from other dom elements - they
67
67
  // can load external resources, and any stylesheets in the document apply to them as we
68
68
  // would expect them to. But when we pull the SVG into its own file or draw it to a canvas
@@ -354,7 +354,7 @@ function SvgExport({
354
354
  key: uniqueId(),
355
355
  getElement: async () => {
356
356
  const declaration = await editor.fonts.toEmbeddedCssDeclaration(font)
357
- return <style>{declaration}</style>
357
+ return <style nonce={editor.options.nonce}>{declaration}</style>
358
358
  },
359
359
  })
360
360
  }
@@ -110,48 +110,3 @@ export function parseCssValueUrls(value: string) {
110
110
  url: m[1] || m[2] || m[3],
111
111
  }))
112
112
  }
113
-
114
- const inheritedProperties = new Set([
115
- 'border-collapse',
116
- 'border-spacing',
117
- 'caption-side',
118
- 'color',
119
- 'cursor',
120
- 'direction',
121
- 'empty-cells',
122
- 'font-family',
123
- 'font-size',
124
- 'font-style',
125
- 'font-variant',
126
- 'font-weight',
127
- 'font-size-adjust',
128
- 'font-stretch',
129
- 'font',
130
- 'letter-spacing',
131
- 'line-height',
132
- 'list-style-image',
133
- 'list-style-position',
134
- 'list-style-type',
135
- 'list-style',
136
- 'orphans',
137
- 'overflow-wrap',
138
- 'quotes',
139
- 'tab-size',
140
- 'text-align',
141
- 'text-align-last',
142
- 'text-decoration-color',
143
- 'text-indent',
144
- 'text-justify',
145
- 'text-shadow',
146
- 'text-transform',
147
- 'visibility',
148
- 'white-space',
149
- 'widows',
150
- 'word-break',
151
- 'word-spacing',
152
- 'word-wrap',
153
- ])
154
-
155
- export function isPropertyInherited(property: string) {
156
- return inheritedProperties.has(property)
157
- }