@tldraw/editor 3.15.0-canary.14c6b9d1aa1e → 3.15.0-canary.1fc6c3fa6d1c

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.
@@ -1,5 +1,4 @@
1
1
  import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
2
- import { objectMapKeys } from '@tldraw/utils'
3
2
  import { Editor } from '../../Editor'
4
3
 
5
4
  const fixNewLines = /\r?\n|\r/g
@@ -61,18 +60,10 @@ export interface TLMeasureTextSpanOpts {
61
60
 
62
61
  const spaceCharacterRegex = /\s/
63
62
 
64
- const initialDefaultStyles = Object.freeze({
65
- 'overflow-wrap': 'break-word',
66
- 'word-break': 'auto',
67
- width: null,
68
- height: null,
69
- 'max-width': null,
70
- 'min-width': null,
71
- })
72
-
73
63
  /** @public */
74
64
  export class TextManager {
75
65
  private elm: HTMLDivElement
66
+ private defaultStyles: Record<string, string | null>
76
67
 
77
68
  constructor(public editor: Editor) {
78
69
  const elm = document.createElement('div')
@@ -82,34 +73,31 @@ export class TextManager {
82
73
  elm.tabIndex = -1
83
74
  this.editor.getContainer().appendChild(elm)
84
75
 
85
- this.elm = elm
86
-
87
- for (const key of objectMapKeys(initialDefaultStyles)) {
88
- elm.style.setProperty(key, initialDefaultStyles[key])
76
+ // we need to save the default styles so that we can restore them when we're done
77
+ // these must be the css names, not the js names for the styles
78
+ this.defaultStyles = {
79
+ 'overflow-wrap': 'break-word',
80
+ 'word-break': 'auto',
81
+ width: null,
82
+ height: null,
83
+ 'max-width': null,
84
+ 'min-width': null,
89
85
  }
90
- }
91
86
 
92
- private setElementStyles(styles: Record<string, string | undefined>) {
93
- const stylesToReinstate = {} as any
94
- for (const key of objectMapKeys(styles)) {
95
- if (typeof styles[key] === 'string') {
96
- const oldValue = this.elm.style.getPropertyValue(key)
97
- if (oldValue === styles[key]) continue
98
- stylesToReinstate[key] = oldValue
99
- this.elm.style.setProperty(key, styles[key])
100
- }
101
- }
102
- return () => {
103
- for (const key of objectMapKeys(stylesToReinstate)) {
104
- this.elm.style.setProperty(key, stylesToReinstate[key])
105
- }
106
- }
87
+ this.elm = elm
107
88
  }
108
89
 
109
90
  dispose() {
110
91
  return this.elm.remove()
111
92
  }
112
93
 
94
+ private resetElmStyles() {
95
+ const { elm, defaultStyles } = this
96
+ for (const key in defaultStyles) {
97
+ elm.style.setProperty(key, defaultStyles[key])
98
+ }
99
+ }
100
+
113
101
  measureText(textToMeasure: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
114
102
  const div = document.createElement('div')
115
103
  div.textContent = normalizeTextForDom(textToMeasure)
@@ -119,36 +107,54 @@ export class TextManager {
119
107
  measureHtml(html: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
120
108
  const { elm } = this
121
109
 
122
- const newStyles = {
123
- 'font-family': opts.fontFamily,
124
- 'font-style': opts.fontStyle,
125
- 'font-weight': opts.fontWeight,
126
- 'font-size': opts.fontSize + 'px',
127
- 'line-height': opts.lineHeight.toString(),
128
- padding: opts.padding,
129
- 'max-width': opts.maxWidth ? opts.maxWidth + 'px' : undefined,
130
- 'min-width': opts.minWidth ? opts.minWidth + 'px' : undefined,
131
- 'overflow-wrap': opts.disableOverflowWrapBreaking ? 'normal' : undefined,
132
- ...opts.otherStyles,
110
+ if (opts.otherStyles) {
111
+ for (const key in opts.otherStyles) {
112
+ if (!this.defaultStyles[key]) {
113
+ // we need to save the original style so that we can restore it when we're done
114
+ this.defaultStyles[key] = elm.style.getPropertyValue(key)
115
+ }
116
+ }
133
117
  }
134
118
 
135
- const restoreStyles = this.setElementStyles(newStyles)
119
+ elm.innerHTML = html
136
120
 
137
- try {
138
- elm.innerHTML = html
121
+ // Apply the default styles to the element (for all styles here or that were ever seen in opts.otherStyles)
122
+ this.resetElmStyles()
139
123
 
140
- const scrollWidth = opts.measureScrollWidth ? elm.scrollWidth : 0
141
- const rect = elm.getBoundingClientRect()
124
+ elm.style.setProperty('font-family', opts.fontFamily)
125
+ elm.style.setProperty('font-style', opts.fontStyle)
126
+ elm.style.setProperty('font-weight', opts.fontWeight)
127
+ elm.style.setProperty('font-size', opts.fontSize + 'px')
128
+ elm.style.setProperty('line-height', opts.lineHeight.toString())
129
+ elm.style.setProperty('padding', opts.padding)
142
130
 
143
- return {
144
- x: 0,
145
- y: 0,
146
- w: rect.width,
147
- h: rect.height,
148
- scrollWidth,
131
+ if (opts.maxWidth) {
132
+ elm.style.setProperty('max-width', opts.maxWidth + 'px')
133
+ }
134
+
135
+ if (opts.minWidth) {
136
+ elm.style.setProperty('min-width', opts.minWidth + 'px')
137
+ }
138
+
139
+ if (opts.disableOverflowWrapBreaking) {
140
+ elm.style.setProperty('overflow-wrap', 'normal')
141
+ }
142
+
143
+ if (opts.otherStyles) {
144
+ for (const [key, value] of Object.entries(opts.otherStyles)) {
145
+ elm.style.setProperty(key, value)
149
146
  }
150
- } finally {
151
- restoreStyles()
147
+ }
148
+
149
+ const scrollWidth = opts.measureScrollWidth ? elm.scrollWidth : 0
150
+ const rect = elm.getBoundingClientRect()
151
+
152
+ return {
153
+ x: 0,
154
+ y: 0,
155
+ w: rect.width,
156
+ h: rect.height,
157
+ scrollWidth,
152
158
  }
153
159
  }
154
160
 
@@ -268,68 +274,82 @@ export class TextManager {
268
274
 
269
275
  const { elm } = this
270
276
 
277
+ if (opts.otherStyles) {
278
+ for (const key in opts.otherStyles) {
279
+ if (!this.defaultStyles[key]) {
280
+ // we need to save the original style so that we can restore it when we're done
281
+ this.defaultStyles[key] = elm.style.getPropertyValue(key)
282
+ }
283
+ }
284
+ }
285
+
286
+ this.resetElmStyles()
287
+
288
+ elm.style.setProperty('font-family', opts.fontFamily)
289
+ elm.style.setProperty('font-style', opts.fontStyle)
290
+ elm.style.setProperty('font-weight', opts.fontWeight)
291
+ elm.style.setProperty('font-size', opts.fontSize + 'px')
292
+ elm.style.setProperty('line-height', opts.lineHeight.toString())
293
+
294
+ const elementWidth = Math.ceil(opts.width - opts.padding * 2)
295
+ elm.style.setProperty('width', `${elementWidth}px`)
296
+ elm.style.setProperty('height', 'min-content')
297
+ elm.style.setProperty('text-align', textAlignmentsForLtr[opts.textAlign])
298
+
271
299
  const shouldTruncateToFirstLine =
272
300
  opts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'
273
- const elementWidth = Math.ceil(opts.width - opts.padding * 2)
274
- const newStyles = {
275
- 'font-family': opts.fontFamily,
276
- 'font-style': opts.fontStyle,
277
- 'font-weight': opts.fontWeight,
278
- 'font-size': opts.fontSize + 'px',
279
- 'line-height': opts.lineHeight.toString(),
280
- width: `${elementWidth}px`,
281
- height: 'min-content',
282
- 'text-align': textAlignmentsForLtr[opts.textAlign],
283
- 'overflow-wrap': shouldTruncateToFirstLine ? 'anywhere' : undefined,
284
- 'word-break': shouldTruncateToFirstLine ? 'break-all' : undefined,
285
- ...opts.otherStyles,
301
+
302
+ if (shouldTruncateToFirstLine) {
303
+ elm.style.setProperty('overflow-wrap', 'anywhere')
304
+ elm.style.setProperty('word-break', 'break-all')
286
305
  }
287
- const restoreStyles = this.setElementStyles(newStyles)
288
306
 
289
- try {
290
- const normalizedText = normalizeTextForDom(textToMeasure)
307
+ if (opts.otherStyles) {
308
+ for (const [key, value] of Object.entries(opts.otherStyles)) {
309
+ elm.style.setProperty(key, value)
310
+ }
311
+ }
291
312
 
292
- // Render the text into the measurement element:
293
- elm.textContent = normalizedText
313
+ const normalizedText = normalizeTextForDom(textToMeasure)
294
314
 
295
- // actually measure the text:
296
- const { spans, didTruncate } = this.measureElementTextNodeSpans(elm, {
297
- shouldTruncateToFirstLine,
298
- })
315
+ // Render the text into the measurement element:
316
+ elm.textContent = normalizedText
299
317
 
300
- if (opts.overflow === 'truncate-ellipsis' && didTruncate) {
301
- // we need to measure the ellipsis to know how much space it takes up
302
- elm.textContent = '…'
303
- const ellipsisWidth = Math.ceil(this.measureElementTextNodeSpans(elm).spans[0].box.w)
304
-
305
- // then, we need to subtract that space from the width we have and measure again:
306
- elm.style.setProperty('width', `${elementWidth - ellipsisWidth}px`)
307
- elm.textContent = normalizedText
308
- const truncatedSpans = this.measureElementTextNodeSpans(elm, {
309
- shouldTruncateToFirstLine: true,
310
- }).spans
311
-
312
- // Finally, we add in our ellipsis at the end of the last span. We
313
- // have to do this after measuring, not before, because adding the
314
- // ellipsis changes how whitespace might be getting collapsed by the
315
- // browser.
316
- const lastSpan = truncatedSpans[truncatedSpans.length - 1]!
317
- truncatedSpans.push({
318
- text: '…',
319
- box: {
320
- x: Math.min(lastSpan.box.x + lastSpan.box.w, opts.width - opts.padding - ellipsisWidth),
321
- y: lastSpan.box.y,
322
- w: ellipsisWidth,
323
- h: lastSpan.box.h,
324
- },
325
- })
326
-
327
- return truncatedSpans
328
- }
318
+ // actually measure the text:
319
+ const { spans, didTruncate } = this.measureElementTextNodeSpans(elm, {
320
+ shouldTruncateToFirstLine,
321
+ })
329
322
 
330
- return spans
331
- } finally {
332
- restoreStyles()
323
+ if (opts.overflow === 'truncate-ellipsis' && didTruncate) {
324
+ // we need to measure the ellipsis to know how much space it takes up
325
+ elm.textContent = '…'
326
+ const ellipsisWidth = Math.ceil(this.measureElementTextNodeSpans(elm).spans[0].box.w)
327
+
328
+ // then, we need to subtract that space from the width we have and measure again:
329
+ elm.style.setProperty('width', `${elementWidth - ellipsisWidth}px`)
330
+ elm.textContent = normalizedText
331
+ const truncatedSpans = this.measureElementTextNodeSpans(elm, {
332
+ shouldTruncateToFirstLine: true,
333
+ }).spans
334
+
335
+ // Finally, we add in our ellipsis at the end of the last span. We
336
+ // have to do this after measuring, not before, because adding the
337
+ // ellipsis changes how whitespace might be getting collapsed by the
338
+ // browser.
339
+ const lastSpan = truncatedSpans[truncatedSpans.length - 1]!
340
+ truncatedSpans.push({
341
+ text: '…',
342
+ box: {
343
+ x: Math.min(lastSpan.box.x + lastSpan.box.w, opts.width - opts.padding - ellipsisWidth),
344
+ y: lastSpan.box.y,
345
+ w: ellipsisWidth,
346
+ h: lastSpan.box.h,
347
+ },
348
+ })
349
+
350
+ return truncatedSpans
333
351
  }
352
+
353
+ return spans
334
354
  }
335
355
  }
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.15.0-canary.14c6b9d1aa1e'
4
+ export const version = '3.15.0-canary.1fc6c3fa6d1c'
5
5
  export const publishDates = {
6
6
  major: '2024-09-13T14:36:29.063Z',
7
- minor: '2025-07-10T10:11:22.062Z',
8
- patch: '2025-07-10T10:11:22.062Z',
7
+ minor: '2025-07-09T11:51:13.402Z',
8
+ patch: '2025-07-09T11:51:13.402Z',
9
9
  }