@tldraw/editor 5.3.0 → 5.4.0-canary.0714aa9af9a0
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 +62 -52
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +82 -26
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js +7 -3
- package/dist-cjs/lib/editor/derivations/shapeIdsInCurrentPage.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +6 -3
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js +6 -3
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +2 -2
- package/dist-cjs/lib/hooks/useDocumentEvents.js +3 -2
- package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useViewportHeight.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Group2d.js +63 -21
- package/dist-cjs/lib/primitives/geometry/Group2d.js.map +2 -2
- package/dist-cjs/lib/utils/normalizeWheel.js +2 -0
- package/dist-cjs/lib/utils/normalizeWheel.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 +62 -52
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +82 -26
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs +7 -3
- package/dist-esm/lib/editor/derivations/shapeIdsInCurrentPage.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +7 -4
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs +6 -3
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +2 -2
- package/dist-esm/lib/hooks/useDocumentEvents.mjs +3 -2
- package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useViewportHeight.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Group2d.mjs +63 -21
- package/dist-esm/lib/primitives/geometry/Group2d.mjs.map +2 -2
- package/dist-esm/lib/utils/normalizeWheel.mjs +2 -0
- package/dist-esm/lib/utils/normalizeWheel.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +7 -7
- package/src/index.ts +1 -0
- package/src/lib/editor/Editor.ts +120 -37
- package/src/lib/editor/derivations/shapeIdsInCurrentPage.ts +7 -3
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +42 -0
- package/src/lib/editor/managers/TextManager/TextManager.ts +10 -8
- package/src/lib/editor/managers/TickManager/TickManager.test.ts +13 -0
- package/src/lib/editor/managers/TickManager/TickManager.ts +9 -3
- package/src/lib/hooks/useDocumentEvents.ts +9 -8
- package/src/lib/hooks/useViewportHeight.ts +9 -1
- package/src/lib/primitives/geometry/Group2d.ts +63 -27
- package/src/lib/utils/normalizeWheel.test.ts +39 -0
- package/src/lib/utils/normalizeWheel.ts +6 -0
- package/src/version.ts +3 -3
package/dist-esm/version.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/version.ts"],
|
|
4
|
-
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.
|
|
4
|
+
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.4.0-canary.0714aa9af9a0'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-08-15T20:16:10.391Z',\n\tpatch: '2026-08-15T20:16:10.391Z',\n}\n"],
|
|
5
5
|
"mappings": "AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/editor",
|
|
3
3
|
"description": "tldraw infinite canvas SDK (editor).",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.4.0-canary.0714aa9af9a0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
"@tiptap/core": "^3.12.1",
|
|
50
50
|
"@tiptap/pm": "^3.12.1",
|
|
51
51
|
"@tiptap/react": "^3.12.1",
|
|
52
|
-
"@tldraw/state": "5.
|
|
53
|
-
"@tldraw/state-react": "5.
|
|
54
|
-
"@tldraw/store": "5.
|
|
55
|
-
"@tldraw/tlschema": "5.
|
|
56
|
-
"@tldraw/utils": "5.
|
|
57
|
-
"@tldraw/validate": "5.
|
|
52
|
+
"@tldraw/state": "5.4.0-canary.0714aa9af9a0",
|
|
53
|
+
"@tldraw/state-react": "5.4.0-canary.0714aa9af9a0",
|
|
54
|
+
"@tldraw/store": "5.4.0-canary.0714aa9af9a0",
|
|
55
|
+
"@tldraw/tlschema": "5.4.0-canary.0714aa9af9a0",
|
|
56
|
+
"@tldraw/utils": "5.4.0-canary.0714aa9af9a0",
|
|
57
|
+
"@tldraw/validate": "5.4.0-canary.0714aa9af9a0",
|
|
58
58
|
"classnames": "^2.5.1",
|
|
59
59
|
"eventemitter3": "^4.0.7",
|
|
60
60
|
"idb": "^7.1.1",
|
package/src/index.ts
CHANGED
|
@@ -329,6 +329,7 @@ export {
|
|
|
329
329
|
export { useTransform } from './lib/hooks/useTransform'
|
|
330
330
|
export { isCursorInViewport } from './lib/utils/collaborators'
|
|
331
331
|
export { useTLSchemaFromUtils, useTLStore } from './lib/hooks/useTLStore'
|
|
332
|
+
// oxlint-disable-next-line typescript/no-deprecated
|
|
332
333
|
export { useViewportHeight } from './lib/hooks/useViewportHeight'
|
|
333
334
|
export {
|
|
334
335
|
LicenseManager,
|
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -5862,6 +5862,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5862
5862
|
const margin = this.options.hitTestMargin / this.getZoomLevel()
|
|
5863
5863
|
const sortedShapes = this.getCurrentPageShapesSorted()
|
|
5864
5864
|
|
|
5865
|
+
// iterate from the top (highest z-index) to find the top-most matching shape
|
|
5865
5866
|
for (let i = sortedShapes.length - 1; i >= 0; i--) {
|
|
5866
5867
|
const shape = sortedShapes[i]
|
|
5867
5868
|
if (shape.type === 'group') continue
|
|
@@ -5911,28 +5912,25 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
5911
5912
|
const searchMargin = Math.max(innerMargin, outerMargin, this.getHitTestMargin())
|
|
5912
5913
|
const candidateIds = this._spatialIndex.getShapeIdsAtPoint(point, searchMargin)
|
|
5913
5914
|
|
|
5914
|
-
const shapesToCheck =
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
: this.getCurrentPageShapesSorted()
|
|
5918
|
-
).filter((shape) => {
|
|
5919
|
-
// Frame-like shapes have labels positioned above the shape (outside bounds), so always include them
|
|
5920
|
-
if (!candidateIds.has(shape.id) && !this.isShapeFrameLike(shape)) return false
|
|
5915
|
+
const shapesToCheck = opts.renderingOnly
|
|
5916
|
+
? this.getCurrentPageRenderingShapesSorted()
|
|
5917
|
+
: this.getCurrentPageShapesSorted()
|
|
5921
5918
|
|
|
5919
|
+
for (let i = shapesToCheck.length - 1; i >= 0; i--) {
|
|
5920
|
+
const shape = shapesToCheck[i]
|
|
5921
|
+
// Frame-like shapes have labels positioned above the shape (outside bounds), so always include them
|
|
5922
|
+
if (!candidateIds.has(shape.id) && !this.isShapeFrameLike(shape)) continue
|
|
5922
5923
|
if (
|
|
5923
5924
|
(shape.isLocked && !hitLocked) ||
|
|
5924
5925
|
this.isShapeHidden(shape) ||
|
|
5925
5926
|
this.isShapeOfType(shape, 'group')
|
|
5926
|
-
)
|
|
5927
|
-
|
|
5927
|
+
) {
|
|
5928
|
+
continue
|
|
5929
|
+
}
|
|
5928
5930
|
const pageMask = this.getShapeMask(shape)
|
|
5929
|
-
if (pageMask && !pointInPolygon(point, pageMask))
|
|
5930
|
-
if (filter && !filter(shape))
|
|
5931
|
-
return true
|
|
5932
|
-
})
|
|
5931
|
+
if (pageMask && !pointInPolygon(point, pageMask)) continue
|
|
5932
|
+
if (filter && !filter(shape)) continue
|
|
5933
5933
|
|
|
5934
|
-
for (let i = shapesToCheck.length - 1; i >= 0; i--) {
|
|
5935
|
-
const shape = shapesToCheck[i]
|
|
5936
5934
|
const geometry = this.getShapeGeometry(shape)
|
|
5937
5935
|
const isGroup = geometry instanceof Group2d
|
|
5938
5936
|
|
|
@@ -6114,15 +6112,18 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
6114
6112
|
const margin = opts.margin ?? 0
|
|
6115
6113
|
const candidateIds = this._spatialIndex.getShapeIdsAtPoint(point, margin)
|
|
6116
6114
|
|
|
6117
|
-
// Get all page shapes in z-index order and filter to candidates that pass isPointInShape
|
|
6118
|
-
// Frame-like shapes are always checked because their labels can be outside their bounds
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
.
|
|
6115
|
+
// Get all page shapes in z-index order and filter to candidates that pass isPointInShape.
|
|
6116
|
+
// Frame-like shapes are always checked because their labels can be outside their bounds.
|
|
6117
|
+
// Iterate backwards so the result is pre-sorted in reverse z-index order (top-most first).
|
|
6118
|
+
const sorted = this.getCurrentPageShapesSorted()
|
|
6119
|
+
const result: TLShape[] = []
|
|
6120
|
+
for (let i = sorted.length - 1; i >= 0; i--) {
|
|
6121
|
+
const shape = sorted[i]
|
|
6122
|
+
if (this.isShapeHidden(shape)) continue
|
|
6123
|
+
if (!candidateIds.has(shape.id) && !this.isShapeFrameLike(shape)) continue
|
|
6124
|
+
if (this.isPointInShape(shape, point, opts)) result.push(shape)
|
|
6125
|
+
}
|
|
6126
|
+
return result
|
|
6126
6127
|
}
|
|
6127
6128
|
|
|
6128
6129
|
/**
|
|
@@ -7877,9 +7878,19 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
7877
7878
|
*/
|
|
7878
7879
|
alignShapes(
|
|
7879
7880
|
shapes: TLShapeId[] | TLShape[],
|
|
7880
|
-
operation:
|
|
7881
|
+
operation:
|
|
7882
|
+
| 'left'
|
|
7883
|
+
| 'center-horizontal'
|
|
7884
|
+
| 'right'
|
|
7885
|
+
| 'top'
|
|
7886
|
+
| 'center-vertical'
|
|
7887
|
+
| 'bottom'
|
|
7888
|
+
| 'center'
|
|
7881
7889
|
): this {
|
|
7882
7890
|
if (this.getIsReadonly()) return this
|
|
7891
|
+
if (operation === 'center') {
|
|
7892
|
+
return this.alignShapes(shapes, 'center-horizontal').alignShapes(shapes, 'center-vertical')
|
|
7893
|
+
}
|
|
7883
7894
|
|
|
7884
7895
|
const { clusters: shapeClustersToAlign, allBounds } = this.getShapeClusters(shapes, 'align')
|
|
7885
7896
|
|
|
@@ -10840,9 +10851,15 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10840
10851
|
/** @internal */
|
|
10841
10852
|
private _shiftKeyTimeout = -1 as any
|
|
10842
10853
|
|
|
10843
|
-
/**
|
|
10854
|
+
/**
|
|
10855
|
+
* Release the shift modifier: clear its debounce timer id, drop the atom, and
|
|
10856
|
+
* dispatch a synthetic `key_up` so `inputs.keys` and tool `onKeyUp` handlers update.
|
|
10857
|
+
* Runs both as the 150ms debounce timer callback and when a pointer down flushes it.
|
|
10858
|
+
* @internal
|
|
10859
|
+
*/
|
|
10844
10860
|
@bind
|
|
10845
|
-
|
|
10861
|
+
_releaseShiftKey() {
|
|
10862
|
+
this._shiftKeyTimeout = -1
|
|
10846
10863
|
this.inputs.setShiftKey(false)
|
|
10847
10864
|
this.dispatch({
|
|
10848
10865
|
type: 'keyboard',
|
|
@@ -10860,9 +10877,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10860
10877
|
/** @internal */
|
|
10861
10878
|
private _altKeyTimeout = -1 as any
|
|
10862
10879
|
|
|
10863
|
-
/**
|
|
10880
|
+
/**
|
|
10881
|
+
* Release the alt modifier. See {@link Editor._releaseShiftKey}.
|
|
10882
|
+
* @internal
|
|
10883
|
+
*/
|
|
10864
10884
|
@bind
|
|
10865
|
-
|
|
10885
|
+
_releaseAltKey() {
|
|
10886
|
+
this._altKeyTimeout = -1
|
|
10866
10887
|
this.inputs.setAltKey(false)
|
|
10867
10888
|
this.dispatch({
|
|
10868
10889
|
type: 'keyboard',
|
|
@@ -10880,9 +10901,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10880
10901
|
/** @internal */
|
|
10881
10902
|
private _ctrlKeyTimeout = -1 as any
|
|
10882
10903
|
|
|
10883
|
-
/**
|
|
10904
|
+
/**
|
|
10905
|
+
* Release the ctrl modifier. See {@link Editor._releaseShiftKey}.
|
|
10906
|
+
* @internal
|
|
10907
|
+
*/
|
|
10884
10908
|
@bind
|
|
10885
|
-
|
|
10909
|
+
_releaseCtrlKey() {
|
|
10910
|
+
this._ctrlKeyTimeout = -1
|
|
10886
10911
|
this.inputs.setCtrlKey(false)
|
|
10887
10912
|
this.dispatch({
|
|
10888
10913
|
type: 'keyboard',
|
|
@@ -10900,9 +10925,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10900
10925
|
/** @internal */
|
|
10901
10926
|
private _metaKeyTimeout = -1 as any
|
|
10902
10927
|
|
|
10903
|
-
/**
|
|
10928
|
+
/**
|
|
10929
|
+
* Release the meta modifier. See {@link Editor._releaseShiftKey}.
|
|
10930
|
+
* @internal
|
|
10931
|
+
*/
|
|
10904
10932
|
@bind
|
|
10905
|
-
|
|
10933
|
+
_releaseMetaKey() {
|
|
10934
|
+
this._metaKeyTimeout = -1
|
|
10906
10935
|
this.inputs.setMetaKey(false)
|
|
10907
10936
|
this.dispatch({
|
|
10908
10937
|
type: 'keyboard',
|
|
@@ -10917,6 +10946,56 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10917
10946
|
})
|
|
10918
10947
|
}
|
|
10919
10948
|
|
|
10949
|
+
/**
|
|
10950
|
+
* Flush any modifier still sitting in its 150ms release-debounce window, so a new
|
|
10951
|
+
* interaction (a pointer down) starts with correct modifier state instead of a
|
|
10952
|
+
* just-released key still being counted as held. A pending timer is exactly
|
|
10953
|
+
* "released but still counted as held"; a genuinely-held modifier has no timer
|
|
10954
|
+
* (-1) and is left alone.
|
|
10955
|
+
*
|
|
10956
|
+
* Each modifier is released through the same path its timer would have taken
|
|
10957
|
+
* (`_releaseShiftKey` and friends), which dispatches the synthetic `key_up` so
|
|
10958
|
+
* stale codes (e.g. `ShiftLeft`) leave `inputs.keys` and tool `onKeyUp` handlers run.
|
|
10959
|
+
* We clear every pending modifier atom and timer *first*, then dispatch: a synthetic
|
|
10960
|
+
* `key_up` reports all currently-held modifiers, so releasing them one at a time
|
|
10961
|
+
* would make each event re-confirm the not-yet-released ones and cancel their flush.
|
|
10962
|
+
* @internal
|
|
10963
|
+
*/
|
|
10964
|
+
private _releaseDebouncedModifiers() {
|
|
10965
|
+
const releaseShift = this._shiftKeyTimeout !== -1
|
|
10966
|
+
const releaseAlt = this._altKeyTimeout !== -1
|
|
10967
|
+
const releaseCtrl = this._ctrlKeyTimeout !== -1
|
|
10968
|
+
const releaseMeta = this._metaKeyTimeout !== -1
|
|
10969
|
+
|
|
10970
|
+
if (releaseShift) {
|
|
10971
|
+
clearTimeout(this._shiftKeyTimeout)
|
|
10972
|
+
this._shiftKeyTimeout = -1
|
|
10973
|
+
this.inputs.setShiftKey(false)
|
|
10974
|
+
}
|
|
10975
|
+
if (releaseAlt) {
|
|
10976
|
+
clearTimeout(this._altKeyTimeout)
|
|
10977
|
+
this._altKeyTimeout = -1
|
|
10978
|
+
this.inputs.setAltKey(false)
|
|
10979
|
+
}
|
|
10980
|
+
if (releaseCtrl) {
|
|
10981
|
+
clearTimeout(this._ctrlKeyTimeout)
|
|
10982
|
+
this._ctrlKeyTimeout = -1
|
|
10983
|
+
this.inputs.setCtrlKey(false)
|
|
10984
|
+
}
|
|
10985
|
+
if (releaseMeta) {
|
|
10986
|
+
clearTimeout(this._metaKeyTimeout)
|
|
10987
|
+
this._metaKeyTimeout = -1
|
|
10988
|
+
this.inputs.setMetaKey(false)
|
|
10989
|
+
}
|
|
10990
|
+
|
|
10991
|
+
// Dispatch the synthetic key_up for each flushed modifier (same path its 150ms
|
|
10992
|
+
// timer would run): clears stale codes from inputs.keys and fires tool onKeyUp.
|
|
10993
|
+
if (releaseShift) this._releaseShiftKey()
|
|
10994
|
+
if (releaseAlt) this._releaseAltKey()
|
|
10995
|
+
if (releaseCtrl) this._releaseCtrlKey()
|
|
10996
|
+
if (releaseMeta) this._releaseMetaKey()
|
|
10997
|
+
}
|
|
10998
|
+
|
|
10920
10999
|
/** @internal */
|
|
10921
11000
|
private _restoreToolId = 'select'
|
|
10922
11001
|
|
|
@@ -11055,7 +11134,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11055
11134
|
this._shiftKeyTimeout = -1
|
|
11056
11135
|
inputs.setShiftKey(true)
|
|
11057
11136
|
} else if (!info.shiftKey && inputs.getShiftKey() && this._shiftKeyTimeout === -1) {
|
|
11058
|
-
this._shiftKeyTimeout = this.timers.setTimeout(this.
|
|
11137
|
+
this._shiftKeyTimeout = this.timers.setTimeout(this._releaseShiftKey, 150)
|
|
11059
11138
|
}
|
|
11060
11139
|
|
|
11061
11140
|
if (info.altKey) {
|
|
@@ -11063,7 +11142,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11063
11142
|
this._altKeyTimeout = -1
|
|
11064
11143
|
inputs.setAltKey(true)
|
|
11065
11144
|
} else if (!info.altKey && inputs.getAltKey() && this._altKeyTimeout === -1) {
|
|
11066
|
-
this._altKeyTimeout = this.timers.setTimeout(this.
|
|
11145
|
+
this._altKeyTimeout = this.timers.setTimeout(this._releaseAltKey, 150)
|
|
11067
11146
|
}
|
|
11068
11147
|
|
|
11069
11148
|
if (info.ctrlKey) {
|
|
@@ -11071,7 +11150,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11071
11150
|
this._ctrlKeyTimeout = -1
|
|
11072
11151
|
inputs.setCtrlKey(true)
|
|
11073
11152
|
} else if (!info.ctrlKey && inputs.getCtrlKey() && this._ctrlKeyTimeout === -1) {
|
|
11074
|
-
this._ctrlKeyTimeout = this.timers.setTimeout(this.
|
|
11153
|
+
this._ctrlKeyTimeout = this.timers.setTimeout(this._releaseCtrlKey, 150)
|
|
11075
11154
|
}
|
|
11076
11155
|
|
|
11077
11156
|
if (info.metaKey && info.name !== 'key_up') {
|
|
@@ -11081,7 +11160,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11081
11160
|
this._metaKeyTimeout = -1
|
|
11082
11161
|
inputs.setMetaKey(true)
|
|
11083
11162
|
} else if (!info.metaKey && inputs.getMetaKey() && this._metaKeyTimeout === -1) {
|
|
11084
|
-
this._metaKeyTimeout = this.timers.setTimeout(this.
|
|
11163
|
+
this._metaKeyTimeout = this.timers.setTimeout(this._releaseMetaKey, 150)
|
|
11085
11164
|
}
|
|
11086
11165
|
|
|
11087
11166
|
if (!inputs.getIsPointing()) {
|
|
@@ -11289,6 +11368,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11289
11368
|
// If we're in pen mode and the input is not a pen type, then stop here
|
|
11290
11369
|
if (isPenMode && !isPen) return
|
|
11291
11370
|
|
|
11371
|
+
// A pointer down starts a new interaction, so flush any modifier that's
|
|
11372
|
+
// still lingering in its release-debounce window: treat it as released now.
|
|
11373
|
+
this._releaseDebouncedModifiers()
|
|
11374
|
+
|
|
11292
11375
|
if (!this.inputs.getIsPanning()) {
|
|
11293
11376
|
// Start a long press timeout
|
|
11294
11377
|
this._longPressTimeout = this.timers.setTimeout(() => {
|
|
@@ -39,9 +39,13 @@ export function deriveShapeIdsInCurrentPage(store: TLStore, getCurrentPageId: ()
|
|
|
39
39
|
function fromScratch() {
|
|
40
40
|
const currentPageId = getCurrentPageId()
|
|
41
41
|
lastPageId = currentPageId
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const result = new Set<TLShapeId>()
|
|
43
|
+
for (const id of shapesIndex.get()) {
|
|
44
|
+
if (isShapeInPage(store, currentPageId, store.get(id)!)) {
|
|
45
|
+
result.add(id)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return result
|
|
45
49
|
}
|
|
46
50
|
return computed<Set<TLShapeId>>('_shapeIdsInCurrentPage', (prevValue, lastComputedEpoch) => {
|
|
47
51
|
if (isUninitialized(prevValue)) {
|
|
@@ -420,6 +420,48 @@ describe('TextManager', () => {
|
|
|
420
420
|
expect(Array.isArray(result)).toBe(true)
|
|
421
421
|
})
|
|
422
422
|
|
|
423
|
+
it('should not throw when the truncated remeasure yields no spans', () => {
|
|
424
|
+
// The text truncates on the first pass, but once we narrow the width
|
|
425
|
+
// to make room for the ellipsis, nothing measurable remains (e.g. the
|
|
426
|
+
// measurement element can't be laid out). This must not crash.
|
|
427
|
+
vi.spyOn(textManager, 'measureElementTextNodeSpans')
|
|
428
|
+
.mockReturnValueOnce({
|
|
429
|
+
spans: [{ text: 'Hello Wo', box: { x: 0, y: 0, w: 80, h: 16 } }],
|
|
430
|
+
didTruncate: true,
|
|
431
|
+
})
|
|
432
|
+
.mockReturnValueOnce({
|
|
433
|
+
spans: [{ text: '…', box: { x: 0, y: 0, w: 10, h: 16 } }],
|
|
434
|
+
didTruncate: false,
|
|
435
|
+
})
|
|
436
|
+
.mockReturnValueOnce({
|
|
437
|
+
spans: [],
|
|
438
|
+
didTruncate: false,
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
const opts = { ...defaultOpts, overflow: 'truncate-ellipsis' as const }
|
|
442
|
+
let result: ReturnType<typeof textManager.measureTextSpans> | undefined
|
|
443
|
+
expect(() => (result = textManager.measureTextSpans('Hello World', opts))).not.toThrow()
|
|
444
|
+
expect(result).toEqual([])
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it('should not throw when the ellipsis itself cannot be measured', () => {
|
|
448
|
+
// If measuring the ellipsis returns no spans, fall back to a zero width
|
|
449
|
+
// rather than dereferencing a missing span.
|
|
450
|
+
vi.spyOn(textManager, 'measureElementTextNodeSpans')
|
|
451
|
+
.mockReturnValueOnce({
|
|
452
|
+
spans: [{ text: 'Hello Wo', box: { x: 0, y: 0, w: 80, h: 16 } }],
|
|
453
|
+
didTruncate: true,
|
|
454
|
+
})
|
|
455
|
+
.mockReturnValueOnce({ spans: [], didTruncate: false })
|
|
456
|
+
.mockReturnValueOnce({
|
|
457
|
+
spans: [{ text: 'Hello Wo', box: { x: 0, y: 0, w: 80, h: 16 } }],
|
|
458
|
+
didTruncate: false,
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
const opts = { ...defaultOpts, overflow: 'truncate-ellipsis' as const }
|
|
462
|
+
expect(() => textManager.measureTextSpans('Hello World', opts)).not.toThrow()
|
|
463
|
+
})
|
|
464
|
+
|
|
423
465
|
it('should handle truncate-clip overflow', () => {
|
|
424
466
|
vi.spyOn(textManager, 'measureElementTextNodeSpans').mockReturnValue({
|
|
425
467
|
spans: [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BoxModel, TLDefaultHorizontalAlignStyle } from '@tldraw/tlschema'
|
|
2
|
-
import { objectMapKeys } from '@tldraw/utils'
|
|
2
|
+
import { iterateGraphemes, objectMapKeys } from '@tldraw/utils'
|
|
3
3
|
import type { Editor } from '../../Editor'
|
|
4
4
|
import { EditorManager } from '../EditorManager'
|
|
5
5
|
|
|
@@ -94,10 +94,6 @@ export interface TLMeasureTextSpanOpts {
|
|
|
94
94
|
|
|
95
95
|
const spaceCharacterRegex = /\s/
|
|
96
96
|
|
|
97
|
-
// Iterate by grapheme cluster (e.g. emoji sequences, flags, accented letters)
|
|
98
|
-
// rather than by code point, so a single glyph is measured as one unit.
|
|
99
|
-
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
|
100
|
-
|
|
101
97
|
const initialDefaultStyles = Object.freeze({
|
|
102
98
|
'overflow-wrap': 'break-word',
|
|
103
99
|
'word-break': 'auto',
|
|
@@ -309,7 +305,7 @@ export class TextManager extends EditorManager {
|
|
|
309
305
|
for (const childNode of element.childNodes) {
|
|
310
306
|
if (childNode.nodeType !== Node.TEXT_NODE) continue
|
|
311
307
|
|
|
312
|
-
for (const
|
|
308
|
+
for (const segment of iterateGraphemes(childNode.textContent ?? '')) {
|
|
313
309
|
// place the range around the grapheme we're interested in
|
|
314
310
|
range.setStart(textNode, idx)
|
|
315
311
|
range.setEnd(textNode, idx + segment.length)
|
|
@@ -438,7 +434,8 @@ export class TextManager extends EditorManager {
|
|
|
438
434
|
if (opts.overflow === 'truncate-ellipsis' && didTruncate) {
|
|
439
435
|
// we need to measure the ellipsis to know how much space it takes up
|
|
440
436
|
elm.textContent = '…'
|
|
441
|
-
const
|
|
437
|
+
const ellipsisSpan = this.measureElementTextNodeSpans(elm).spans[0]
|
|
438
|
+
const ellipsisWidth = ellipsisSpan ? Math.ceil(ellipsisSpan.box.w) : 0
|
|
442
439
|
|
|
443
440
|
// then, we need to subtract that space from the width we have and measure again:
|
|
444
441
|
elm.style.setProperty('width', `${elementWidth - ellipsisWidth}px`)
|
|
@@ -451,7 +448,12 @@ export class TextManager extends EditorManager {
|
|
|
451
448
|
// have to do this after measuring, not before, because adding the
|
|
452
449
|
// ellipsis changes how whitespace might be getting collapsed by the
|
|
453
450
|
// browser.
|
|
454
|
-
const lastSpan = truncatedSpans[truncatedSpans.length - 1]
|
|
451
|
+
const lastSpan = truncatedSpans[truncatedSpans.length - 1]
|
|
452
|
+
// If nothing measurable remained, return the (possibly empty)
|
|
453
|
+
// spans as-is rather than dereferencing a missing last span.
|
|
454
|
+
if (!lastSpan) {
|
|
455
|
+
return truncatedSpans
|
|
456
|
+
}
|
|
455
457
|
truncatedSpans.push({
|
|
456
458
|
text: '…',
|
|
457
459
|
box: {
|
|
@@ -142,6 +142,19 @@ describe('TickManager', () => {
|
|
|
142
142
|
expect(mockEmit).toHaveBeenCalledWith('frame', 0)
|
|
143
143
|
expect(mockEmit).toHaveBeenCalledWith('tick', 0)
|
|
144
144
|
})
|
|
145
|
+
|
|
146
|
+
it('should schedule the next frame even when a listener throws', () => {
|
|
147
|
+
const staleCancel = vi.fn()
|
|
148
|
+
tickManager.cancelRaf = staleCancel
|
|
149
|
+
mockEmit.mockImplementation(() => {
|
|
150
|
+
throw new Error('listener blew up')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// the error still reaches the caller, but the loop lives on
|
|
154
|
+
expect(() => tickManager.tick()).toThrow('listener blew up')
|
|
155
|
+
expect(mockRequestAnimationFrame).toHaveBeenCalled()
|
|
156
|
+
expect(tickManager.cancelRaf).not.toBe(staleCancel)
|
|
157
|
+
})
|
|
145
158
|
})
|
|
146
159
|
|
|
147
160
|
describe('dispose method', () => {
|
|
@@ -41,9 +41,15 @@ export class TickManager extends EditorManager {
|
|
|
41
41
|
const elapsed = now - this.now
|
|
42
42
|
this.now = now
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
// Schedule the next frame even if a listener throws. Otherwise a single throwing listener
|
|
45
|
+
// ends the loop for good, which silently stops every frame-driven behavior — camera
|
|
46
|
+
// animations, edge scrolling, pointer velocity, following — instead of failing once.
|
|
47
|
+
try {
|
|
48
|
+
this.editor.emit('frame', elapsed)
|
|
49
|
+
this.editor.emit('tick', elapsed)
|
|
50
|
+
} finally {
|
|
51
|
+
this.cancelRaf = throttleToNextFrame(this.tick)
|
|
52
|
+
}
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
dispose() {
|
|
@@ -240,19 +240,20 @@ export function useDocumentEvents() {
|
|
|
240
240
|
|
|
241
241
|
function handleTouchStart(e: TouchEvent) {
|
|
242
242
|
if (container.contains(e.target as Node)) {
|
|
243
|
-
// Center point of the touch area
|
|
244
|
-
|
|
243
|
+
// Center point of the touch area, measured from the edge of the window the touch
|
|
244
|
+
// has to reach to trigger the navigation
|
|
245
|
+
const touchXPosition = e.touches[0].clientX
|
|
245
246
|
// Size of the touch area
|
|
246
247
|
const touchXRadius = e.touches[0].radiusX || 0
|
|
247
248
|
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
|
|
249
|
+
// If the touch area overlaps with the screen edges it's likely to trigger the
|
|
250
|
+
// navigation. We prevent the touchstart event in that case. The gesture belongs to
|
|
251
|
+
// the window, so an editor inset from the window's edges — beside a sidebar, say —
|
|
252
|
+
// is not near an edge the system reacts to.
|
|
253
|
+
const windowWidth = editor.getContainerWindow().innerWidth
|
|
253
254
|
if (
|
|
254
255
|
touchXPosition - touchXRadius < 10 ||
|
|
255
|
-
touchXPosition + touchXRadius >
|
|
256
|
+
touchXPosition + touchXRadius > windowWidth - 10
|
|
256
257
|
) {
|
|
257
258
|
if ((e.target as HTMLElement)?.tagName === 'BUTTON') {
|
|
258
259
|
// Force a click before bailing
|
|
@@ -11,7 +11,15 @@ import { useMaybeEditor } from './useEditor'
|
|
|
11
11
|
* N.B. On iOS, you have to take into account the offsetTop as well so that you get an accurate position
|
|
12
12
|
* while using the virtual keyboard.
|
|
13
13
|
*/
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The height of the visible viewport, including the visual viewport's own offset.
|
|
16
|
+
*
|
|
17
|
+
* @deprecated Read `window.visualViewport` directly: this flattens the visible rectangle to a
|
|
18
|
+
* single scalar, and keeping UI clear of the software keyboard usually needs the other edges too.
|
|
19
|
+
* Unused since v3.14; will be removed in a future release.
|
|
20
|
+
*
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
15
23
|
export function useViewportHeight(): number {
|
|
16
24
|
const editor = useMaybeEditor()
|
|
17
25
|
const win = editor?.getContainerWindow() ?? window
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { EMPTY_ARRAY } from '@tldraw/state'
|
|
2
1
|
import { assert, invLerp, lerp } from '@tldraw/utils'
|
|
3
2
|
import { Box } from '../Box'
|
|
4
3
|
import { Mat } from '../Mat'
|
|
@@ -36,9 +35,15 @@ export class Group2d extends Geometry2d {
|
|
|
36
35
|
|
|
37
36
|
override getVertices(filters: Geometry2dFilters): Vec[] {
|
|
38
37
|
if (this.isExcludedByFilter(filters)) return []
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
const vertices: Vec[] = []
|
|
39
|
+
for (const child of this.children) {
|
|
40
|
+
if (child.isExcludedByFilter(filters)) continue
|
|
41
|
+
const childVertices = child.getVertices(filters)
|
|
42
|
+
for (let i = 0, n = childVertices.length; i < n; i++) {
|
|
43
|
+
vertices.push(childVertices[i])
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return vertices
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
override nearestPoint(point: VecLike, filters?: Geometry2dFilters): Vec {
|
|
@@ -84,9 +89,11 @@ export class Group2d extends Geometry2d {
|
|
|
84
89
|
hitInside: boolean,
|
|
85
90
|
filters = Geometry2dFilters.EXCLUDE_LABELS
|
|
86
91
|
): boolean {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
for (const child of this.children) {
|
|
93
|
+
if (child.isExcludedByFilter(filters)) continue
|
|
94
|
+
if (child.hitTestPoint(point, margin, hitInside)) return true
|
|
95
|
+
}
|
|
96
|
+
return false
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
override hitTestLineSegment(
|
|
@@ -95,42 +102,71 @@ export class Group2d extends Geometry2d {
|
|
|
95
102
|
zoom: number,
|
|
96
103
|
filters = Geometry2dFilters.EXCLUDE_LABELS
|
|
97
104
|
): boolean {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
for (const child of this.children) {
|
|
106
|
+
if (child.isExcludedByFilter(filters)) continue
|
|
107
|
+
if (child.hitTestLineSegment(A, B, zoom)) return true
|
|
108
|
+
}
|
|
109
|
+
return false
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
override intersectLineSegment(A: VecLike, B: VecLike, filters?: Geometry2dFilters) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
const result: VecLike[] = []
|
|
114
|
+
for (const child of this.children) {
|
|
115
|
+
if (child.isExcludedByFilter(filters)) continue
|
|
116
|
+
const hits = child.intersectLineSegment(A, B, filters)
|
|
117
|
+
for (let i = 0, n = hits.length; i < n; i++) {
|
|
118
|
+
result.push(hits[i])
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return result
|
|
108
122
|
}
|
|
109
123
|
|
|
110
124
|
override intersectCircle(center: VecLike, radius: number, filters?: Geometry2dFilters) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
125
|
+
const result: VecLike[] = []
|
|
126
|
+
for (const child of this.children) {
|
|
127
|
+
if (child.isExcludedByFilter(filters)) continue
|
|
128
|
+
const hits = child.intersectCircle(center, radius, filters)
|
|
129
|
+
for (let i = 0, n = hits.length; i < n; i++) {
|
|
130
|
+
result.push(hits[i])
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return result
|
|
115
134
|
}
|
|
116
135
|
|
|
117
136
|
override getBoundsVertices(): Vec[] {
|
|
118
137
|
if (this.excludeFromShapeBounds) return []
|
|
119
|
-
|
|
138
|
+
const vertices: Vec[] = []
|
|
139
|
+
for (const child of this.children) {
|
|
140
|
+
const childVertices = child.getBoundsVertices()
|
|
141
|
+
for (let i = 0, n = childVertices.length; i < n; i++) {
|
|
142
|
+
vertices.push(childVertices[i])
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return vertices
|
|
120
146
|
}
|
|
121
147
|
|
|
122
148
|
override intersectPolygon(polygon: VecLike[], filters?: Geometry2dFilters) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
149
|
+
const result: VecLike[] = []
|
|
150
|
+
for (const child of this.children) {
|
|
151
|
+
if (child.isExcludedByFilter(filters)) continue
|
|
152
|
+
const hits = child.intersectPolygon(polygon, filters)
|
|
153
|
+
for (let i = 0, n = hits.length; i < n; i++) {
|
|
154
|
+
result.push(hits[i])
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return result
|
|
127
158
|
}
|
|
128
159
|
|
|
129
160
|
override intersectPolyline(polyline: VecLike[], filters?: Geometry2dFilters) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
161
|
+
const result: VecLike[] = []
|
|
162
|
+
for (const child of this.children) {
|
|
163
|
+
if (child.isExcludedByFilter(filters)) continue
|
|
164
|
+
const hits = child.intersectPolyline(polyline, filters)
|
|
165
|
+
for (let i = 0, n = hits.length; i < n; i++) {
|
|
166
|
+
result.push(hits[i])
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return result
|
|
134
170
|
}
|
|
135
171
|
|
|
136
172
|
override interpolateAlongEdge(t: number, filters?: Geometry2dFilters): Vec {
|