@tldraw/editor 3.14.0-canary.4c533b76dc35 → 3.14.0-canary.4deeaa9df15f
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 +52 -44
- package/dist-cjs/index.js +1 -3
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/config/TLSessionStateSnapshot.js +1 -12
- package/dist-cjs/lib/config/TLSessionStateSnapshot.js.map +3 -3
- package/dist-cjs/lib/editor/Editor.js +40 -20
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/bindings/BindingUtil.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +1 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +72 -42
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +1 -1
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +1 -1
- package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
- package/dist-cjs/lib/hooks/useCanvasEvents.js +1 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/primitives/Box.js +0 -6
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- package/dist-cjs/lib/utils/areShapesContentEqual.js +1 -1
- package/dist-cjs/lib/utils/areShapesContentEqual.js.map +2 -2
- package/dist-cjs/lib/utils/richText.js +7 -2
- package/dist-cjs/lib/utils/richText.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +52 -44
- package/dist-esm/index.mjs +1 -3
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/config/TLSessionStateSnapshot.mjs +1 -1
- package/dist-esm/lib/config/TLSessionStateSnapshot.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +40 -20
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/bindings/BindingUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +1 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +72 -42
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +1 -1
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +1 -1
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +1 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/primitives/Box.mjs +0 -6
- package/dist-esm/lib/primitives/Box.mjs.map +2 -2
- package/dist-esm/lib/utils/areShapesContentEqual.mjs +1 -1
- package/dist-esm/lib/utils/areShapesContentEqual.mjs.map +2 -2
- package/dist-esm/lib/utils/richText.mjs +8 -3
- package/dist-esm/lib/utils/richText.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +433 -482
- package/package.json +8 -9
- package/src/index.ts +1 -1
- package/src/lib/config/TLSessionStateSnapshot.ts +1 -1
- package/src/lib/editor/Editor.test.ts +11 -11
- package/src/lib/editor/Editor.ts +42 -17
- package/src/lib/editor/bindings/BindingUtil.ts +6 -0
- package/src/lib/editor/managers/FontManager/FontManager.ts +1 -2
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +1 -5
- package/src/lib/editor/managers/TextManager/TextManager.ts +116 -86
- package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +1 -1
- package/src/lib/editor/types/external-content.ts +1 -1
- package/src/lib/hooks/useCanvasEvents.ts +0 -1
- package/src/lib/primitives/Box.ts +0 -8
- package/src/lib/utils/areShapesContentEqual.ts +1 -2
- package/src/lib/utils/richText.ts +9 -3
- package/src/version.ts +3 -3
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.14.0-canary.
|
|
4
|
+
"version": "3.14.0-canary.4deeaa9df15f",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -48,20 +48,19 @@
|
|
|
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.14.0-canary.
|
|
52
|
-
"@tldraw/state-react": "3.14.0-canary.
|
|
53
|
-
"@tldraw/store": "3.14.0-canary.
|
|
54
|
-
"@tldraw/tlschema": "3.14.0-canary.
|
|
55
|
-
"@tldraw/utils": "3.14.0-canary.
|
|
56
|
-
"@tldraw/validate": "3.14.0-canary.
|
|
51
|
+
"@tldraw/state": "3.14.0-canary.4deeaa9df15f",
|
|
52
|
+
"@tldraw/state-react": "3.14.0-canary.4deeaa9df15f",
|
|
53
|
+
"@tldraw/store": "3.14.0-canary.4deeaa9df15f",
|
|
54
|
+
"@tldraw/tlschema": "3.14.0-canary.4deeaa9df15f",
|
|
55
|
+
"@tldraw/utils": "3.14.0-canary.4deeaa9df15f",
|
|
56
|
+
"@tldraw/validate": "3.14.0-canary.4deeaa9df15f",
|
|
57
57
|
"@types/core-js": "^2.5.8",
|
|
58
58
|
"@use-gesture/react": "^10.3.1",
|
|
59
59
|
"classnames": "^2.5.1",
|
|
60
60
|
"core-js": "^3.40.0",
|
|
61
61
|
"eventemitter3": "^4.0.7",
|
|
62
62
|
"idb": "^7.1.1",
|
|
63
|
-
"is-plain-object": "^5.0.0"
|
|
64
|
-
"lodash.isequal": "^4.5.0"
|
|
63
|
+
"is-plain-object": "^5.0.0"
|
|
65
64
|
},
|
|
66
65
|
"peerDependencies": {
|
|
67
66
|
"react": "^18.2.0 || ^19.0.0",
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,6 @@ import 'core-js/stable/array/flat-map.js'
|
|
|
4
4
|
import 'core-js/stable/array/flat.js'
|
|
5
5
|
import 'core-js/stable/string/at.js'
|
|
6
6
|
import 'core-js/stable/string/replace-all.js'
|
|
7
|
-
export { areShapesContentEqual } from './lib/utils/areShapesContentEqual'
|
|
8
7
|
|
|
9
8
|
// eslint-disable-next-line local/no-export-star
|
|
10
9
|
export * from '@tldraw/state'
|
|
@@ -175,6 +174,7 @@ export {
|
|
|
175
174
|
} from './lib/editor/managers/SnapManager/SnapManager'
|
|
176
175
|
export {
|
|
177
176
|
TextManager,
|
|
177
|
+
type TLMeasureTextOpts,
|
|
178
178
|
type TLMeasureTextSpanOpts,
|
|
179
179
|
} from './lib/editor/managers/TextManager/TextManager'
|
|
180
180
|
export { UserPreferencesManager } from './lib/editor/managers/UserPreferencesManager/UserPreferencesManager'
|
|
@@ -14,12 +14,12 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
deleteFromSessionStorage,
|
|
16
16
|
getFromSessionStorage,
|
|
17
|
+
isEqual,
|
|
17
18
|
setInSessionStorage,
|
|
18
19
|
structuredClone,
|
|
19
20
|
uniqueId,
|
|
20
21
|
} from '@tldraw/utils'
|
|
21
22
|
import { T } from '@tldraw/validate'
|
|
22
|
-
import isEqual from 'lodash.isequal'
|
|
23
23
|
import { tlenv } from '../globals/environment'
|
|
24
24
|
|
|
25
25
|
const tabIdKey = 'TLDRAW_TAB_ID_v2' as const
|
|
@@ -233,7 +233,7 @@ describe('getShapesAtPoint', () => {
|
|
|
233
233
|
})
|
|
234
234
|
})
|
|
235
235
|
|
|
236
|
-
it('returns shapes at a point in
|
|
236
|
+
it('returns shapes at a point in reverse z-index order', () => {
|
|
237
237
|
// Point at (50, 50) should hit shape3's edge (since it's at 50,50 with size 100x100)
|
|
238
238
|
// This point is exactly at the top-left corner of shape3
|
|
239
239
|
const shapes = editor.getShapesAtPoint({ x: 50, y: 50 })
|
|
@@ -313,12 +313,12 @@ describe('getShapesAtPoint', () => {
|
|
|
313
313
|
const shapes = editor.getShapesAtPoint({ x: 100, y: 0 })
|
|
314
314
|
const shapeIds = shapes.map((s) => s.id)
|
|
315
315
|
|
|
316
|
-
// Both shapes should be detected at this overlapping point
|
|
317
|
-
expect(shapeIds).toEqual([ids.
|
|
316
|
+
// Both shapes should be detected at this overlapping point (reversed order - top-most first)
|
|
317
|
+
expect(shapeIds).toEqual([ids.shape2, ids.shape1])
|
|
318
318
|
expect(shapes).toHaveLength(2)
|
|
319
319
|
})
|
|
320
320
|
|
|
321
|
-
it('maintains shape order
|
|
321
|
+
it('maintains reverse shape order and responds to z-index changes', () => {
|
|
322
322
|
// Create filled shape that overlaps with shape2
|
|
323
323
|
editor.createShape({
|
|
324
324
|
id: ids.shape5,
|
|
@@ -333,14 +333,14 @@ describe('getShapesAtPoint', () => {
|
|
|
333
333
|
const shapes = editor.getShapesAtPoint({ x: 120, y: 120 }, { hitInside: true })
|
|
334
334
|
const shapeIds = shapes.map((s) => s.id)
|
|
335
335
|
|
|
336
|
-
// All shapes that contain this point should be returned in z-index order
|
|
337
|
-
expect(shapeIds).toEqual([ids.
|
|
336
|
+
// All shapes that contain this point should be returned in reverse z-index order (top-most first)
|
|
337
|
+
expect(shapeIds).toEqual([ids.shape5, ids.shape4, ids.shape3, ids.shape2, ids.shape1])
|
|
338
338
|
|
|
339
|
-
// After bringing shape2 to front, order should change
|
|
339
|
+
// After bringing shape2 to front, order should change (shape2 becomes top-most)
|
|
340
340
|
editor.bringToFront([ids.shape2])
|
|
341
341
|
const shapes2 = editor.getShapesAtPoint({ x: 120, y: 120 }, { hitInside: true })
|
|
342
342
|
const shapeIds2 = shapes2.map((s) => s.id)
|
|
343
|
-
expect(shapeIds2).toEqual([ids.
|
|
343
|
+
expect(shapeIds2).toEqual([ids.shape2, ids.shape5, ids.shape4, ids.shape3, ids.shape1])
|
|
344
344
|
})
|
|
345
345
|
|
|
346
346
|
it('combines hitInside and margin options', () => {
|
|
@@ -361,7 +361,7 @@ describe('getShapesAtPoint', () => {
|
|
|
361
361
|
isShapeHiddenSpy.mockRestore()
|
|
362
362
|
})
|
|
363
363
|
|
|
364
|
-
it('returns multiple shapes at same point in z-index order', () => {
|
|
364
|
+
it('returns multiple shapes at same point in reverse z-index order', () => {
|
|
365
365
|
// Create two shapes at exactly the same position (away from existing shapes)
|
|
366
366
|
editor.createShape({
|
|
367
367
|
id: ids.overlap1,
|
|
@@ -383,8 +383,8 @@ describe('getShapesAtPoint', () => {
|
|
|
383
383
|
const shapes = editor.getShapesAtPoint({ x: 600, y: 600 })
|
|
384
384
|
const shapeIds = shapes.map((s) => s.id)
|
|
385
385
|
|
|
386
|
-
// Should return both shapes in z-index order
|
|
387
|
-
expect(shapeIds).toEqual([ids.
|
|
386
|
+
// Should return both shapes in reverse z-index order (top-most first)
|
|
387
|
+
expect(shapeIds).toEqual([ids.overlap2, ids.overlap1])
|
|
388
388
|
expect(shapes).toHaveLength(2)
|
|
389
389
|
})
|
|
390
390
|
|
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -348,6 +348,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
348
348
|
this.getContainer = getContainer
|
|
349
349
|
|
|
350
350
|
this.textMeasure = new TextManager(this)
|
|
351
|
+
this.disposables.add(() => this.textMeasure.dispose())
|
|
352
|
+
|
|
351
353
|
this.fonts = new FontManager(this, fontAssetUrls)
|
|
352
354
|
|
|
353
355
|
this._tickManager = new TickManager(this)
|
|
@@ -506,14 +508,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
506
508
|
shape: {
|
|
507
509
|
afterChange: (shapeBefore, shapeAfter) => {
|
|
508
510
|
for (const binding of this.getBindingsInvolvingShape(shapeAfter)) {
|
|
509
|
-
if (areShapesContentEqual(shapeBefore, shapeAfter)) continue
|
|
510
|
-
|
|
511
511
|
invalidBindingTypes.add(binding.type)
|
|
512
512
|
if (binding.fromId === shapeAfter.id) {
|
|
513
513
|
this.getBindingUtil(binding).onAfterChangeFromShape?.({
|
|
514
514
|
binding,
|
|
515
515
|
shapeBefore,
|
|
516
516
|
shapeAfter,
|
|
517
|
+
reason: 'self',
|
|
517
518
|
})
|
|
518
519
|
}
|
|
519
520
|
if (binding.toId === shapeAfter.id) {
|
|
@@ -521,6 +522,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
521
522
|
binding,
|
|
522
523
|
shapeBefore,
|
|
523
524
|
shapeAfter,
|
|
525
|
+
reason: 'self',
|
|
524
526
|
})
|
|
525
527
|
}
|
|
526
528
|
}
|
|
@@ -539,6 +541,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
539
541
|
binding,
|
|
540
542
|
shapeBefore: descendantShape,
|
|
541
543
|
shapeAfter: descendantShape,
|
|
544
|
+
reason: 'ancestry',
|
|
542
545
|
})
|
|
543
546
|
}
|
|
544
547
|
if (binding.toId === descendantShape.id) {
|
|
@@ -546,6 +549,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
546
549
|
binding,
|
|
547
550
|
shapeBefore: descendantShape,
|
|
548
551
|
shapeAfter: descendantShape,
|
|
552
|
+
reason: 'ancestry',
|
|
549
553
|
})
|
|
550
554
|
}
|
|
551
555
|
}
|
|
@@ -2118,6 +2122,20 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2118
2122
|
return this.getShapesPageBounds(this.getSelectedShapeIds())
|
|
2119
2123
|
}
|
|
2120
2124
|
|
|
2125
|
+
/**
|
|
2126
|
+
* The bounds of the selection bounding box in the current page space.
|
|
2127
|
+
*
|
|
2128
|
+
* @readonly
|
|
2129
|
+
* @public
|
|
2130
|
+
*/
|
|
2131
|
+
@computed getSelectionScreenBounds(): Box | undefined {
|
|
2132
|
+
const bounds = this.getSelectionPageBounds()
|
|
2133
|
+
if (!bounds) return undefined
|
|
2134
|
+
const { x, y } = this.pageToScreen(bounds.point)
|
|
2135
|
+
const zoom = this.getZoomLevel()
|
|
2136
|
+
return new Box(x, y, bounds.width * zoom, bounds.height * zoom)
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2121
2139
|
/**
|
|
2122
2140
|
* @internal
|
|
2123
2141
|
*/
|
|
@@ -5035,28 +5053,33 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5035
5053
|
*
|
|
5036
5054
|
* @public
|
|
5037
5055
|
*/
|
|
5038
|
-
isShapeOrAncestorLocked(shape?: TLShape): boolean
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
if (shape.isLocked) return true
|
|
5044
|
-
return this.isShapeOrAncestorLocked(this.getShapeParent(shape))
|
|
5056
|
+
isShapeOrAncestorLocked(shape?: TLShape | TLShapeId): boolean {
|
|
5057
|
+
const _shape = shape && this.getShape(shape)
|
|
5058
|
+
if (_shape === undefined) return false
|
|
5059
|
+
if (_shape.isLocked) return true
|
|
5060
|
+
return this.isShapeOrAncestorLocked(this.getShapeParent(_shape))
|
|
5045
5061
|
}
|
|
5046
5062
|
|
|
5063
|
+
/**
|
|
5064
|
+
* Get shapes that are outside of the viewport.
|
|
5065
|
+
*
|
|
5066
|
+
* @public
|
|
5067
|
+
*/
|
|
5047
5068
|
@computed
|
|
5048
|
-
|
|
5049
|
-
return
|
|
5069
|
+
getNotVisibleShapes() {
|
|
5070
|
+
return this._notVisibleShapes.get()
|
|
5050
5071
|
}
|
|
5051
5072
|
|
|
5073
|
+
private _notVisibleShapes = notVisibleShapes(this)
|
|
5074
|
+
|
|
5052
5075
|
/**
|
|
5053
|
-
* Get culled shapes.
|
|
5076
|
+
* Get culled shapes (those that should not render), taking into account which shapes are selected or editing.
|
|
5054
5077
|
*
|
|
5055
5078
|
* @public
|
|
5056
5079
|
*/
|
|
5057
5080
|
@computed
|
|
5058
5081
|
getCulledShapes() {
|
|
5059
|
-
const notVisibleShapes = this.
|
|
5082
|
+
const notVisibleShapes = this.getNotVisibleShapes()
|
|
5060
5083
|
const selectedShapeIds = this.getSelectedShapeIds()
|
|
5061
5084
|
const editingId = this.getEditingShapeId()
|
|
5062
5085
|
const culledShapes = new Set<TLShapeId>(notVisibleShapes)
|
|
@@ -5305,21 +5328,23 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5305
5328
|
* @example
|
|
5306
5329
|
* ```ts
|
|
5307
5330
|
* editor.getShapesAtPoint({ x: 100, y: 100 })
|
|
5308
|
-
* editor.getShapesAtPoint({ x: 100, y: 100 }, { hitInside: true,
|
|
5331
|
+
* editor.getShapesAtPoint({ x: 100, y: 100 }, { hitInside: true, margin: 8 })
|
|
5309
5332
|
* ```
|
|
5310
5333
|
*
|
|
5311
5334
|
* @param point - The page point to test.
|
|
5312
5335
|
* @param opts - The options for the hit point testing.
|
|
5313
5336
|
*
|
|
5337
|
+
* @returns An array of shapes at the given point, sorted in reverse order of their absolute z-index (top-most shape first).
|
|
5338
|
+
*
|
|
5314
5339
|
* @public
|
|
5315
5340
|
*/
|
|
5316
5341
|
getShapesAtPoint(
|
|
5317
5342
|
point: VecLike,
|
|
5318
5343
|
opts = {} as { margin?: number; hitInside?: boolean }
|
|
5319
5344
|
): TLShape[] {
|
|
5320
|
-
return this.getCurrentPageShapesSorted()
|
|
5321
|
-
(shape) => !this.isShapeHidden(shape) && this.isPointInShape(shape, point, opts)
|
|
5322
|
-
|
|
5345
|
+
return this.getCurrentPageShapesSorted()
|
|
5346
|
+
.filter((shape) => !this.isShapeHidden(shape) && this.isPointInShape(shape, point, opts))
|
|
5347
|
+
.reverse()
|
|
5323
5348
|
}
|
|
5324
5349
|
|
|
5325
5350
|
/**
|
|
@@ -62,6 +62,12 @@ export interface BindingOnShapeChangeOptions<Binding extends TLUnknownBinding> {
|
|
|
62
62
|
shapeBefore: TLShape
|
|
63
63
|
/** The shape record after the change is made. */
|
|
64
64
|
shapeAfter: TLShape
|
|
65
|
+
/**
|
|
66
|
+
* Why did this shape change?
|
|
67
|
+
* - 'self': the shape itself changed
|
|
68
|
+
* - 'ancestry': the ancestry of the shape changed, but the shape itself may not have done
|
|
69
|
+
*/
|
|
70
|
+
reason: 'self' | 'ancestry'
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
/**
|
|
@@ -96,8 +96,7 @@ export class FontManager {
|
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
areResultsEqual: areArraysShallowEqual,
|
|
99
|
-
|
|
100
|
-
areRecordsEqual: (a, b) => a.props.richText === b.props.richText,
|
|
99
|
+
areRecordsEqual: (a, b) => a.props === b.props && a.meta === b.meta,
|
|
101
100
|
}
|
|
102
101
|
)
|
|
103
102
|
|
|
@@ -99,7 +99,7 @@ describe('TextManager', () => {
|
|
|
99
99
|
})
|
|
100
100
|
|
|
101
101
|
it('should handle empty text', () => {
|
|
102
|
-
const result = textManager.measureText('', defaultOpts)
|
|
102
|
+
const result = textManager.measureText('', { ...defaultOpts, measureScrollWidth: true })
|
|
103
103
|
expect(result).toHaveProperty('x', 0)
|
|
104
104
|
expect(result).toHaveProperty('y', 0)
|
|
105
105
|
expect(result).toHaveProperty('w')
|
|
@@ -128,7 +128,6 @@ describe('TextManager', () => {
|
|
|
128
128
|
y: 0,
|
|
129
129
|
w: expect.any(Number),
|
|
130
130
|
h: expect.any(Number),
|
|
131
|
-
scrollWidth: expect.any(Number),
|
|
132
131
|
})
|
|
133
132
|
})
|
|
134
133
|
|
|
@@ -141,7 +140,6 @@ describe('TextManager', () => {
|
|
|
141
140
|
y: 0,
|
|
142
141
|
w: expect.any(Number),
|
|
143
142
|
h: expect.any(Number),
|
|
144
|
-
scrollWidth: expect.any(Number),
|
|
145
143
|
})
|
|
146
144
|
})
|
|
147
145
|
|
|
@@ -154,7 +152,6 @@ describe('TextManager', () => {
|
|
|
154
152
|
y: 0,
|
|
155
153
|
w: expect.any(Number),
|
|
156
154
|
h: expect.any(Number),
|
|
157
|
-
scrollWidth: expect.any(Number),
|
|
158
155
|
})
|
|
159
156
|
})
|
|
160
157
|
|
|
@@ -173,7 +170,6 @@ describe('TextManager', () => {
|
|
|
173
170
|
y: 0,
|
|
174
171
|
w: expect.any(Number),
|
|
175
172
|
h: expect.any(Number),
|
|
176
|
-
scrollWidth: expect.any(Number),
|
|
177
173
|
})
|
|
178
174
|
})
|
|
179
175
|
})
|
|
@@ -20,6 +20,27 @@ const textAlignmentsForLtr = {
|
|
|
20
20
|
'end-legacy': 'right',
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** @public */
|
|
24
|
+
export interface TLMeasureTextOpts {
|
|
25
|
+
fontStyle: string
|
|
26
|
+
fontWeight: string
|
|
27
|
+
fontFamily: string
|
|
28
|
+
fontSize: number
|
|
29
|
+
lineHeight: number
|
|
30
|
+
/**
|
|
31
|
+
* When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth
|
|
32
|
+
* is null, the text will be measured without wrapping, but explicit line breaks and
|
|
33
|
+
* space are preserved.
|
|
34
|
+
*/
|
|
35
|
+
maxWidth: null | number
|
|
36
|
+
minWidth?: null | number
|
|
37
|
+
// todo: make this a number so that it is consistent with other TLMeasureTextSpanOpts
|
|
38
|
+
padding: string
|
|
39
|
+
otherStyles?: Record<string, string>
|
|
40
|
+
disableOverflowWrapBreaking?: boolean
|
|
41
|
+
measureScrollWidth?: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
23
44
|
/** @public */
|
|
24
45
|
export interface TLMeasureTextSpanOpts {
|
|
25
46
|
overflow: 'wrap' | 'truncate-ellipsis' | 'truncate-clip'
|
|
@@ -33,96 +54,98 @@ export interface TLMeasureTextSpanOpts {
|
|
|
33
54
|
lineHeight: number
|
|
34
55
|
textAlign: TLDefaultHorizontalAlignStyle
|
|
35
56
|
otherStyles?: Record<string, string>
|
|
57
|
+
measureScrollWidth?: boolean
|
|
36
58
|
}
|
|
37
59
|
|
|
38
60
|
const spaceCharacterRegex = /\s/
|
|
39
61
|
|
|
40
62
|
/** @public */
|
|
41
63
|
export class TextManager {
|
|
42
|
-
private
|
|
64
|
+
private elm: HTMLDivElement
|
|
65
|
+
private defaultStyles: Record<string, string | null>
|
|
43
66
|
|
|
44
67
|
constructor(public editor: Editor) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
68
|
+
const elm = document.createElement('div')
|
|
69
|
+
elm.classList.add('tl-text')
|
|
70
|
+
elm.classList.add('tl-text-measure')
|
|
71
|
+
elm.setAttribute('dir', 'auto')
|
|
72
|
+
elm.tabIndex = -1
|
|
73
|
+
this.editor.getContainer().appendChild(elm)
|
|
74
|
+
|
|
75
|
+
// we need to save the default styles so that we can restore them when we're done
|
|
76
|
+
// these must be the css names, not the js names for the styles
|
|
77
|
+
this.defaultStyles = {
|
|
78
|
+
'word-break': 'auto',
|
|
79
|
+
width: null,
|
|
80
|
+
height: null,
|
|
81
|
+
'max-width': null,
|
|
82
|
+
'min-width': null,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.elm = elm
|
|
49
86
|
}
|
|
50
87
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth
|
|
61
|
-
* is null, the text will be measured without wrapping, but explicit line breaks and
|
|
62
|
-
* space are preserved.
|
|
63
|
-
*/
|
|
64
|
-
maxWidth: null | number
|
|
65
|
-
minWidth?: null | number
|
|
66
|
-
padding: string
|
|
67
|
-
disableOverflowWrapBreaking?: boolean
|
|
88
|
+
dispose() {
|
|
89
|
+
return this.elm.remove()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private resetElmStyles() {
|
|
93
|
+
const { elm, defaultStyles } = this
|
|
94
|
+
for (const key in defaultStyles) {
|
|
95
|
+
elm.style.setProperty(key, defaultStyles[key])
|
|
68
96
|
}
|
|
69
|
-
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
measureText(textToMeasure: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
|
|
70
100
|
const div = document.createElement('div')
|
|
71
101
|
div.textContent = normalizeTextForDom(textToMeasure)
|
|
72
102
|
return this.measureHtml(div.innerHTML, opts)
|
|
73
103
|
}
|
|
74
104
|
|
|
75
|
-
measureHtml(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
* is null, the text will be measured without wrapping, but explicit line breaks and
|
|
86
|
-
* space are preserved.
|
|
87
|
-
*/
|
|
88
|
-
maxWidth: null | number
|
|
89
|
-
minWidth?: null | number
|
|
90
|
-
otherStyles?: Record<string, string>
|
|
91
|
-
padding: string
|
|
92
|
-
disableOverflowWrapBreaking?: boolean
|
|
105
|
+
measureHtml(html: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
|
|
106
|
+
const { elm } = this
|
|
107
|
+
|
|
108
|
+
if (opts.otherStyles) {
|
|
109
|
+
for (const key in opts.otherStyles) {
|
|
110
|
+
if (!this.defaultStyles[key]) {
|
|
111
|
+
// we need to save the original style so that we can restore it when we're done
|
|
112
|
+
this.defaultStyles[key] = elm.style.getPropertyValue(key)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
93
115
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
|
|
117
|
+
elm.innerHTML = html
|
|
118
|
+
|
|
119
|
+
// Apply the default styles to the element (for all styles here or that were ever seen in opts.otherStyles)
|
|
120
|
+
this.resetElmStyles()
|
|
121
|
+
|
|
122
|
+
elm.style.setProperty('font-family', opts.fontFamily)
|
|
123
|
+
elm.style.setProperty('font-style', opts.fontStyle)
|
|
124
|
+
elm.style.setProperty('font-weight', opts.fontWeight)
|
|
125
|
+
elm.style.setProperty('font-size', opts.fontSize + 'px')
|
|
126
|
+
elm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px')
|
|
127
|
+
elm.style.setProperty('padding', opts.padding)
|
|
128
|
+
|
|
129
|
+
if (opts.maxWidth) {
|
|
130
|
+
elm.style.setProperty('max-width', opts.maxWidth + 'px')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (opts.minWidth) {
|
|
134
|
+
elm.style.setProperty('min-width', opts.minWidth + 'px')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (opts.disableOverflowWrapBreaking) {
|
|
138
|
+
elm.style.setProperty('overflow-wrap', 'normal')
|
|
139
|
+
}
|
|
140
|
+
|
|
117
141
|
if (opts.otherStyles) {
|
|
118
142
|
for (const [key, value] of Object.entries(opts.otherStyles)) {
|
|
119
|
-
|
|
143
|
+
elm.style.setProperty(key, value)
|
|
120
144
|
}
|
|
121
145
|
}
|
|
122
146
|
|
|
123
|
-
const scrollWidth =
|
|
124
|
-
const rect =
|
|
125
|
-
wrapperElm.remove()
|
|
147
|
+
const scrollWidth = opts.measureScrollWidth ? elm.scrollWidth : 0
|
|
148
|
+
const rect = elm.getBoundingClientRect()
|
|
126
149
|
|
|
127
150
|
return {
|
|
128
151
|
x: 0,
|
|
@@ -247,27 +270,29 @@ export class TextManager {
|
|
|
247
270
|
): { text: string; box: BoxModel }[] {
|
|
248
271
|
if (textToMeasure === '') return []
|
|
249
272
|
|
|
250
|
-
const elm = this
|
|
251
|
-
|
|
273
|
+
const { elm } = this
|
|
274
|
+
|
|
275
|
+
if (opts.otherStyles) {
|
|
276
|
+
for (const key in opts.otherStyles) {
|
|
277
|
+
if (!this.defaultStyles[key]) {
|
|
278
|
+
// we need to save the original style so that we can restore it when we're done
|
|
279
|
+
this.defaultStyles[key] = elm.style.getPropertyValue(key)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
this.resetElmStyles()
|
|
285
|
+
|
|
286
|
+
elm.style.setProperty('font-family', opts.fontFamily)
|
|
287
|
+
elm.style.setProperty('font-style', opts.fontStyle)
|
|
288
|
+
elm.style.setProperty('font-weight', opts.fontWeight)
|
|
289
|
+
elm.style.setProperty('font-size', opts.fontSize + 'px')
|
|
290
|
+
elm.style.setProperty('line-height', opts.lineHeight * opts.fontSize + 'px')
|
|
252
291
|
|
|
253
292
|
const elementWidth = Math.ceil(opts.width - opts.padding * 2)
|
|
254
|
-
elm.setAttribute('dir', 'auto')
|
|
255
|
-
// N.B. This property, while discouraged ("intended for Document Type Definition (DTD) designers")
|
|
256
|
-
// is necessary for ensuring correct mixed RTL/LTR behavior when exporting SVGs.
|
|
257
|
-
elm.style.setProperty('unicode-bidi', 'plaintext')
|
|
258
293
|
elm.style.setProperty('width', `${elementWidth}px`)
|
|
259
294
|
elm.style.setProperty('height', 'min-content')
|
|
260
|
-
elm.style.setProperty('font-size', `${opts.fontSize}px`)
|
|
261
|
-
elm.style.setProperty('font-family', opts.fontFamily)
|
|
262
|
-
elm.style.setProperty('font-weight', opts.fontWeight)
|
|
263
|
-
elm.style.setProperty('line-height', `${opts.lineHeight * opts.fontSize}px`)
|
|
264
295
|
elm.style.setProperty('text-align', textAlignmentsForLtr[opts.textAlign])
|
|
265
|
-
elm.style.setProperty('font-style', opts.fontStyle)
|
|
266
|
-
if (opts.otherStyles) {
|
|
267
|
-
for (const [key, value] of Object.entries(opts.otherStyles)) {
|
|
268
|
-
elm.style.setProperty(key, value)
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
296
|
|
|
272
297
|
const shouldTruncateToFirstLine =
|
|
273
298
|
opts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'
|
|
@@ -277,6 +302,12 @@ export class TextManager {
|
|
|
277
302
|
elm.style.setProperty('word-break', 'break-all')
|
|
278
303
|
}
|
|
279
304
|
|
|
305
|
+
if (opts.otherStyles) {
|
|
306
|
+
for (const [key, value] of Object.entries(opts.otherStyles)) {
|
|
307
|
+
elm.style.setProperty(key, value)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
280
311
|
const normalizedText = normalizeTextForDom(textToMeasure)
|
|
281
312
|
|
|
282
313
|
// Render the text into the measurement element:
|
|
@@ -313,11 +344,10 @@ export class TextManager {
|
|
|
313
344
|
h: lastSpan.box.h,
|
|
314
345
|
},
|
|
315
346
|
})
|
|
347
|
+
|
|
316
348
|
return truncatedSpans
|
|
317
349
|
}
|
|
318
350
|
|
|
319
|
-
elm.remove()
|
|
320
|
-
|
|
321
351
|
return spans
|
|
322
352
|
}
|
|
323
353
|
}
|
|
@@ -591,14 +591,6 @@ export class Box {
|
|
|
591
591
|
return b.x === a.x && b.y === a.y && b.w === a.w && b.h === a.h
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
-
prettyMuchEquals(other: Box | BoxModel) {
|
|
595
|
-
return this.clone().toFixed().equals(Box.From(other).toFixed())
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
static PrettyMuchEquals(a: Box | BoxModel, b: Box | BoxModel) {
|
|
599
|
-
return b.x === a.x && b.y === a.y && b.w === a.w && b.h === a.h
|
|
600
|
-
}
|
|
601
|
-
|
|
602
594
|
zeroFix() {
|
|
603
595
|
this.w = Math.max(1, this.w)
|
|
604
596
|
this.h = Math.max(1, this.h)
|