@tldraw/editor 3.14.0-canary.f907ed7d9ee5 → 3.14.0-canary.faba3f64c07f
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 +133 -50
- package/dist-cjs/index.js +4 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +60 -22
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +73 -42
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js +0 -10
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/tools/StateNode.js +3 -3
- package/dist-cjs/lib/editor/tools/StateNode.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/hooks/useCanvasEvents.js +1 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js +6 -2
- package/dist-cjs/lib/primitives/geometry/Geometry2d.js.map +2 -2
- package/dist-cjs/lib/utils/reparenting.js +232 -0
- package/dist-cjs/lib/utils/reparenting.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 +133 -50
- package/dist-esm/index.mjs +4 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +60 -22
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +73 -42
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs +0 -10
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/StateNode.mjs +3 -3
- package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +1 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs +6 -2
- package/dist-esm/lib/primitives/geometry/Geometry2d.mjs.map +2 -2
- package/dist-esm/lib/utils/reparenting.mjs +216 -0
- package/dist-esm/lib/utils/reparenting.mjs.map +7 -0
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +442 -492
- package/package.json +7 -7
- package/src/index.ts +7 -0
- package/src/lib/editor/Editor.ts +73 -30
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +1 -5
- package/src/lib/editor/managers/TextManager/TextManager.ts +118 -86
- package/src/lib/editor/shapes/ShapeUtil.ts +47 -15
- package/src/lib/editor/tools/StateNode.ts +3 -3
- package/src/lib/editor/types/emit-types.ts +4 -0
- package/src/lib/editor/types/external-content.ts +11 -2
- package/src/lib/hooks/useCanvasEvents.ts +0 -1
- package/src/lib/primitives/geometry/Geometry2d.ts +7 -2
- package/src/lib/utils/reparenting.ts +383 -0
- 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.faba3f64c07f",
|
|
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.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.faba3f64c07f",
|
|
52
|
+
"@tldraw/state-react": "3.14.0-canary.faba3f64c07f",
|
|
53
|
+
"@tldraw/store": "3.14.0-canary.faba3f64c07f",
|
|
54
|
+
"@tldraw/tlschema": "3.14.0-canary.faba3f64c07f",
|
|
55
|
+
"@tldraw/utils": "3.14.0-canary.faba3f64c07f",
|
|
56
|
+
"@tldraw/validate": "3.14.0-canary.faba3f64c07f",
|
|
57
57
|
"@types/core-js": "^2.5.8",
|
|
58
58
|
"@use-gesture/react": "^10.3.1",
|
|
59
59
|
"classnames": "^2.5.1",
|
package/src/index.ts
CHANGED
|
@@ -174,6 +174,7 @@ export {
|
|
|
174
174
|
} from './lib/editor/managers/SnapManager/SnapManager'
|
|
175
175
|
export {
|
|
176
176
|
TextManager,
|
|
177
|
+
type TLMeasureTextOpts,
|
|
177
178
|
type TLMeasureTextSpanOpts,
|
|
178
179
|
} from './lib/editor/managers/TextManager/TextManager'
|
|
179
180
|
export { UserPreferencesManager } from './lib/editor/managers/UserPreferencesManager/UserPreferencesManager'
|
|
@@ -181,6 +182,10 @@ export { BaseBoxShapeUtil, type TLBaseBoxShape } from './lib/editor/shapes/BaseB
|
|
|
181
182
|
export {
|
|
182
183
|
ShapeUtil,
|
|
183
184
|
type TLCropInfo,
|
|
185
|
+
type TLDragShapesInInfo,
|
|
186
|
+
type TLDragShapesOutInfo,
|
|
187
|
+
type TLDragShapesOverInfo,
|
|
188
|
+
type TLDropShapesOverInfo,
|
|
184
189
|
type TLGeometryOpts,
|
|
185
190
|
type TLHandleDragInfo,
|
|
186
191
|
type TLResizeInfo,
|
|
@@ -252,6 +257,7 @@ export {
|
|
|
252
257
|
type TLExternalContent,
|
|
253
258
|
type TLExternalContentSource,
|
|
254
259
|
type TLFileExternalAsset,
|
|
260
|
+
type TLFileReplaceExternalContent,
|
|
255
261
|
type TLFilesExternalContent,
|
|
256
262
|
type TLSvgTextExternalContent,
|
|
257
263
|
type TLTextExternalContent,
|
|
@@ -444,6 +450,7 @@ export { hardResetEditor } from './lib/utils/hardResetEditor'
|
|
|
444
450
|
export { isAccelKey } from './lib/utils/keyboard'
|
|
445
451
|
export { normalizeWheel } from './lib/utils/normalizeWheel'
|
|
446
452
|
export { refreshPage } from './lib/utils/refreshPage'
|
|
453
|
+
export { getDroppedShapesToNewParents, kickoutOccludedShapes } from './lib/utils/reparenting'
|
|
447
454
|
export {
|
|
448
455
|
getFontsFromRichText,
|
|
449
456
|
type RichTextFontVisitor,
|
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)
|
|
@@ -2120,6 +2122,20 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2120
2122
|
return this.getShapesPageBounds(this.getSelectedShapeIds())
|
|
2121
2123
|
}
|
|
2122
2124
|
|
|
2125
|
+
/**
|
|
2126
|
+
* The bounds of the selection bounding box in the current page space.
|
|
2127
|
+
*
|
|
2128
|
+
* @readonly
|
|
2129
|
+
* @public
|
|
2130
|
+
*/
|
|
2131
|
+
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
|
+
|
|
2123
2139
|
/**
|
|
2124
2140
|
* @internal
|
|
2125
2141
|
*/
|
|
@@ -5512,7 +5528,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5512
5528
|
if (!id) return undefined
|
|
5513
5529
|
const freshShape = this.getShape(id)
|
|
5514
5530
|
if (freshShape === undefined || !isShapeId(freshShape.parentId)) return undefined
|
|
5515
|
-
return this.
|
|
5531
|
+
return this.getShape(freshShape.parentId)
|
|
5516
5532
|
}
|
|
5517
5533
|
|
|
5518
5534
|
/**
|
|
@@ -5695,6 +5711,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5695
5711
|
const newPoint = invertedParentTransform.applyToPoint(pagePoint)
|
|
5696
5712
|
const newRotation = pageTransform.rotation() - parentPageRotation
|
|
5697
5713
|
|
|
5714
|
+
if (shape.id === parentId) {
|
|
5715
|
+
throw Error('Attempted to reparent a shape to itself!')
|
|
5716
|
+
}
|
|
5717
|
+
|
|
5698
5718
|
changes.push({
|
|
5699
5719
|
id: shape.id,
|
|
5700
5720
|
type: shape.type,
|
|
@@ -5798,6 +5818,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5798
5818
|
return shapeIds
|
|
5799
5819
|
}
|
|
5800
5820
|
|
|
5821
|
+
/** @deprecated Use {@link Editor.getDraggingOverShape} instead */
|
|
5822
|
+
getDroppingOverShape(point: Vec, droppingShapes: TLShape[]): TLShape | undefined {
|
|
5823
|
+
return this.getDraggingOverShape(point, droppingShapes)
|
|
5824
|
+
}
|
|
5825
|
+
|
|
5801
5826
|
/**
|
|
5802
5827
|
* Get the shape that some shapes should be dropped on at a given point.
|
|
5803
5828
|
*
|
|
@@ -5808,35 +5833,33 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5808
5833
|
*
|
|
5809
5834
|
* @public
|
|
5810
5835
|
*/
|
|
5811
|
-
|
|
5812
|
-
//
|
|
5813
|
-
const
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
if (
|
|
5818
|
-
// ignore hidden shapes
|
|
5819
|
-
this.isShapeHidden(shape) ||
|
|
5820
|
-
// don't allow dropping on selected shapes
|
|
5821
|
-
this.getSelectedShapeIds().includes(shape.id) ||
|
|
5822
|
-
// only allow shapes that can receive children
|
|
5823
|
-
!this.getShapeUtil(shape).canDropShapes(shape, droppingShapes) ||
|
|
5824
|
-
// don't allow dropping a shape on itself or one of it's children
|
|
5825
|
-
droppingShapes.find((s) => s.id === shape.id || this.hasAncestor(shape, s.id))
|
|
5826
|
-
) {
|
|
5827
|
-
continue
|
|
5828
|
-
}
|
|
5836
|
+
getDraggingOverShape(point: Vec, droppingShapes: TLShape[]): TLShape | undefined {
|
|
5837
|
+
// get fresh moving shapes
|
|
5838
|
+
const draggingShapes = compact(droppingShapes.map((s) => this.getShape(s))).filter(
|
|
5839
|
+
(s) => !s.isLocked && !this.isShapeHidden(s)
|
|
5840
|
+
)
|
|
5829
5841
|
|
|
5830
|
-
|
|
5831
|
-
|
|
5832
|
-
|
|
5842
|
+
const maybeDraggingOverShapes = this.getShapesAtPoint(point, {
|
|
5843
|
+
hitInside: true,
|
|
5844
|
+
margin: 0,
|
|
5845
|
+
}).filter(
|
|
5846
|
+
(s) =>
|
|
5847
|
+
!droppingShapes.includes(s) &&
|
|
5848
|
+
!s.isLocked &&
|
|
5849
|
+
!this.isShapeHidden(s) &&
|
|
5850
|
+
!draggingShapes.includes(s)
|
|
5851
|
+
)
|
|
5833
5852
|
|
|
5853
|
+
for (const maybeDraggingOverShape of maybeDraggingOverShapes) {
|
|
5854
|
+
const shapeUtil = this.getShapeUtil(maybeDraggingOverShape)
|
|
5855
|
+
// Any shape that can handle any dragging interactions is a valid target
|
|
5834
5856
|
if (
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5857
|
+
shapeUtil.onDragShapesOver ||
|
|
5858
|
+
shapeUtil.onDragShapesIn ||
|
|
5859
|
+
shapeUtil.onDragShapesOut ||
|
|
5860
|
+
shapeUtil.onDropShapesOver
|
|
5838
5861
|
) {
|
|
5839
|
-
return
|
|
5862
|
+
return maybeDraggingOverShape
|
|
5840
5863
|
}
|
|
5841
5864
|
}
|
|
5842
5865
|
}
|
|
@@ -6195,11 +6218,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6195
6218
|
*/
|
|
6196
6219
|
duplicateShapes(shapes: TLShapeId[] | TLShape[], offset?: VecLike): this {
|
|
6197
6220
|
this.run(() => {
|
|
6198
|
-
const
|
|
6221
|
+
const _ids =
|
|
6199
6222
|
typeof shapes[0] === 'string'
|
|
6200
6223
|
? (shapes as TLShapeId[])
|
|
6201
6224
|
: (shapes as TLShape[]).map((s) => s.id)
|
|
6202
6225
|
|
|
6226
|
+
const ids = this._shouldIgnoreShapeLock ? _ids : this._getUnlockedShapeIds(_ids)
|
|
6203
6227
|
if (ids.length <= 0) return this
|
|
6204
6228
|
|
|
6205
6229
|
const initialIds = new Set(ids)
|
|
@@ -7789,9 +7813,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7789
7813
|
|
|
7790
7814
|
for (let i = currentPageShapesSorted.length - 1; i >= 0; i--) {
|
|
7791
7815
|
const parent = currentPageShapesSorted[i]
|
|
7816
|
+
const util = this.getShapeUtil(parent)
|
|
7792
7817
|
if (
|
|
7818
|
+
util.canReceiveNewChildrenOfType(parent, partial.type) &&
|
|
7793
7819
|
!this.isShapeHidden(parent) &&
|
|
7794
|
-
this.getShapeUtil(parent).canReceiveNewChildrenOfType(parent, partial.type) &&
|
|
7795
7820
|
this.isPointInShape(
|
|
7796
7821
|
parent,
|
|
7797
7822
|
// If no parent is provided, then we can treat the
|
|
@@ -7920,6 +7945,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7920
7945
|
}
|
|
7921
7946
|
})
|
|
7922
7947
|
|
|
7948
|
+
this.emit('created-shapes', shapeRecordsToCreate)
|
|
7949
|
+
this.emit('edit')
|
|
7923
7950
|
this.store.put(shapeRecordsToCreate)
|
|
7924
7951
|
})
|
|
7925
7952
|
|
|
@@ -8314,6 +8341,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8314
8341
|
updates.push(updated)
|
|
8315
8342
|
}
|
|
8316
8343
|
|
|
8344
|
+
this.emit('edited-shapes', updates)
|
|
8345
|
+
this.emit('edit')
|
|
8317
8346
|
this.store.put(updates)
|
|
8318
8347
|
})
|
|
8319
8348
|
}
|
|
@@ -8363,6 +8392,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8363
8392
|
})
|
|
8364
8393
|
}
|
|
8365
8394
|
|
|
8395
|
+
this.emit('deleted-shapes', [...allShapeIdsToDelete])
|
|
8396
|
+
this.emit('edit')
|
|
8366
8397
|
return this.run(() => this.store.remove([...allShapeIdsToDelete]))
|
|
8367
8398
|
}
|
|
8368
8399
|
|
|
@@ -8811,6 +8842,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8811
8842
|
} = {
|
|
8812
8843
|
text: null,
|
|
8813
8844
|
files: null,
|
|
8845
|
+
'file-replace': null,
|
|
8814
8846
|
embed: null,
|
|
8815
8847
|
'svg-text': null,
|
|
8816
8848
|
url: null,
|
|
@@ -8860,6 +8892,15 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
8860
8892
|
return this.externalContentHandlers[info.type]?.(info as any)
|
|
8861
8893
|
}
|
|
8862
8894
|
|
|
8895
|
+
/**
|
|
8896
|
+
* Handle replacing external content.
|
|
8897
|
+
*
|
|
8898
|
+
* @param info - Info about the external content.
|
|
8899
|
+
*/
|
|
8900
|
+
async replaceExternalContent<E>(info: TLExternalContent<E>): Promise<void> {
|
|
8901
|
+
return this.externalContentHandlers[info.type]?.(info as any)
|
|
8902
|
+
}
|
|
8903
|
+
|
|
8863
8904
|
/**
|
|
8864
8905
|
* Get content that can be exported for the given shape ids.
|
|
8865
8906
|
*
|
|
@@ -9479,6 +9520,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9479
9520
|
previousPagePoint,
|
|
9480
9521
|
currentScreenPoint,
|
|
9481
9522
|
currentPagePoint,
|
|
9523
|
+
originScreenPoint,
|
|
9524
|
+
originPagePoint,
|
|
9482
9525
|
} = this.inputs
|
|
9483
9526
|
|
|
9484
9527
|
const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
|
|
@@ -9507,8 +9550,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
9507
9550
|
// Reset velocity on pointer down, or when a pinch starts or ends
|
|
9508
9551
|
if (info.name === 'pointer_down' || this.inputs.isPinching) {
|
|
9509
9552
|
pointerVelocity.set(0, 0)
|
|
9510
|
-
|
|
9511
|
-
|
|
9553
|
+
originScreenPoint.setTo(currentScreenPoint)
|
|
9554
|
+
originPagePoint.setTo(currentPagePoint)
|
|
9512
9555
|
}
|
|
9513
9556
|
|
|
9514
9557
|
// todo: We only have to do this if there are multiple users in the document
|
|
@@ -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,28 @@ 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
|
+
/** This must be a number, e.g. 1.35, not a pixel value. */
|
|
30
|
+
lineHeight: number
|
|
31
|
+
/**
|
|
32
|
+
* When maxWidth is a number, the text will be wrapped to that maxWidth. When maxWidth
|
|
33
|
+
* is null, the text will be measured without wrapping, but explicit line breaks and
|
|
34
|
+
* space are preserved.
|
|
35
|
+
*/
|
|
36
|
+
maxWidth: null | number
|
|
37
|
+
minWidth?: null | number
|
|
38
|
+
// todo: make this a number so that it is consistent with other TLMeasureTextSpanOpts
|
|
39
|
+
padding: string
|
|
40
|
+
otherStyles?: Record<string, string>
|
|
41
|
+
disableOverflowWrapBreaking?: boolean
|
|
42
|
+
measureScrollWidth?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
23
45
|
/** @public */
|
|
24
46
|
export interface TLMeasureTextSpanOpts {
|
|
25
47
|
overflow: 'wrap' | 'truncate-ellipsis' | 'truncate-clip'
|
|
@@ -33,96 +55,99 @@ export interface TLMeasureTextSpanOpts {
|
|
|
33
55
|
lineHeight: number
|
|
34
56
|
textAlign: TLDefaultHorizontalAlignStyle
|
|
35
57
|
otherStyles?: Record<string, string>
|
|
58
|
+
measureScrollWidth?: boolean
|
|
36
59
|
}
|
|
37
60
|
|
|
38
61
|
const spaceCharacterRegex = /\s/
|
|
39
62
|
|
|
40
63
|
/** @public */
|
|
41
64
|
export class TextManager {
|
|
42
|
-
private
|
|
65
|
+
private elm: HTMLDivElement
|
|
66
|
+
private defaultStyles: Record<string, string | null>
|
|
43
67
|
|
|
44
68
|
constructor(public editor: Editor) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
69
|
+
const elm = document.createElement('div')
|
|
70
|
+
elm.classList.add('tl-text')
|
|
71
|
+
elm.classList.add('tl-text-measure')
|
|
72
|
+
elm.setAttribute('dir', 'auto')
|
|
73
|
+
elm.tabIndex = -1
|
|
74
|
+
this.editor.getContainer().appendChild(elm)
|
|
75
|
+
|
|
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,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.elm = elm
|
|
49
88
|
}
|
|
50
89
|
|
|
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
|
|
90
|
+
dispose() {
|
|
91
|
+
return this.elm.remove()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private resetElmStyles() {
|
|
95
|
+
const { elm, defaultStyles } = this
|
|
96
|
+
for (const key in defaultStyles) {
|
|
97
|
+
elm.style.setProperty(key, defaultStyles[key])
|
|
68
98
|
}
|
|
69
|
-
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
measureText(textToMeasure: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
|
|
70
102
|
const div = document.createElement('div')
|
|
71
103
|
div.textContent = normalizeTextForDom(textToMeasure)
|
|
72
104
|
return this.measureHtml(div.innerHTML, opts)
|
|
73
105
|
}
|
|
74
106
|
|
|
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
|
|
107
|
+
measureHtml(html: string, opts: TLMeasureTextOpts): BoxModel & { scrollWidth: number } {
|
|
108
|
+
const { elm } = this
|
|
109
|
+
|
|
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
|
+
}
|
|
93
117
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
|
|
119
|
+
elm.innerHTML = html
|
|
120
|
+
|
|
121
|
+
// Apply the default styles to the element (for all styles here or that were ever seen in opts.otherStyles)
|
|
122
|
+
this.resetElmStyles()
|
|
123
|
+
|
|
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)
|
|
130
|
+
|
|
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
|
+
|
|
117
143
|
if (opts.otherStyles) {
|
|
118
144
|
for (const [key, value] of Object.entries(opts.otherStyles)) {
|
|
119
|
-
|
|
145
|
+
elm.style.setProperty(key, value)
|
|
120
146
|
}
|
|
121
147
|
}
|
|
122
148
|
|
|
123
|
-
const scrollWidth =
|
|
124
|
-
const rect =
|
|
125
|
-
wrapperElm.remove()
|
|
149
|
+
const scrollWidth = opts.measureScrollWidth ? elm.scrollWidth : 0
|
|
150
|
+
const rect = elm.getBoundingClientRect()
|
|
126
151
|
|
|
127
152
|
return {
|
|
128
153
|
x: 0,
|
|
@@ -247,27 +272,29 @@ export class TextManager {
|
|
|
247
272
|
): { text: string; box: BoxModel }[] {
|
|
248
273
|
if (textToMeasure === '') return []
|
|
249
274
|
|
|
250
|
-
const elm = this
|
|
251
|
-
|
|
275
|
+
const { elm } = this
|
|
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())
|
|
252
293
|
|
|
253
294
|
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
295
|
elm.style.setProperty('width', `${elementWidth}px`)
|
|
259
296
|
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
297
|
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
298
|
|
|
272
299
|
const shouldTruncateToFirstLine =
|
|
273
300
|
opts.overflow === 'truncate-ellipsis' || opts.overflow === 'truncate-clip'
|
|
@@ -277,6 +304,12 @@ export class TextManager {
|
|
|
277
304
|
elm.style.setProperty('word-break', 'break-all')
|
|
278
305
|
}
|
|
279
306
|
|
|
307
|
+
if (opts.otherStyles) {
|
|
308
|
+
for (const [key, value] of Object.entries(opts.otherStyles)) {
|
|
309
|
+
elm.style.setProperty(key, value)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
280
313
|
const normalizedText = normalizeTextForDom(textToMeasure)
|
|
281
314
|
|
|
282
315
|
// Render the text into the measurement element:
|
|
@@ -313,11 +346,10 @@ export class TextManager {
|
|
|
313
346
|
h: lastSpan.box.h,
|
|
314
347
|
},
|
|
315
348
|
})
|
|
349
|
+
|
|
316
350
|
return truncatedSpans
|
|
317
351
|
}
|
|
318
352
|
|
|
319
|
-
elm.remove()
|
|
320
|
-
|
|
321
353
|
return spans
|
|
322
354
|
}
|
|
323
355
|
}
|