@tldraw/editor 5.5.0-next.b6a9f7cd7be2 → 5.5.0-next.c91b2de0a423
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 +47 -23
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/TldrawEditor.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +5 -1
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +2 -5
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +3 -3
- package/dist-cjs/lib/components/default-components/DefaultSpinner.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +24 -26
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js +3 -2
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/EditorManager.js +11 -2
- package/dist-cjs/lib/editor/managers/EditorManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/PerformanceManager/PerformanceManager.js +49 -63
- package/dist-cjs/lib/editor/managers/PerformanceManager/PerformanceManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js +1 -1
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +24 -24
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js +7 -1
- package/dist-cjs/lib/editor/managers/SnapManager/HandleSnaps.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js +4 -3
- package/dist-cjs/lib/editor/managers/SnapManager/SnapManager.js.map +2 -2
- package/dist-cjs/lib/editor/types/external-content.js.map +1 -1
- package/dist-cjs/lib/hooks/EditorComponentsContext.js.map +2 -2
- package/dist-cjs/lib/utils/debug-flags.js +0 -6
- package/dist-cjs/lib/utils/debug-flags.js.map +2 -2
- package/dist-cjs/lib/utils/sync/LocalIndexedDb.js +19 -8
- package/dist-cjs/lib/utils/sync/LocalIndexedDb.js.map +2 -2
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +3 -0
- package/dist-cjs/lib/utils/sync/TLLocalSyncClient.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 +47 -23
- package/dist-esm/index.mjs +4 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/TldrawEditor.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +5 -1
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +2 -5
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +3 -3
- package/dist-esm/lib/components/default-components/DefaultSpinner.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +24 -26
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs +3 -2
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/EditorManager.mjs +11 -2
- package/dist-esm/lib/editor/managers/EditorManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/PerformanceManager/PerformanceManager.mjs +49 -63
- package/dist-esm/lib/editor/managers/PerformanceManager/PerformanceManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs +1 -1
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +24 -24
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs +7 -1
- package/dist-esm/lib/editor/managers/SnapManager/HandleSnaps.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs +4 -3
- package/dist-esm/lib/editor/managers/SnapManager/SnapManager.mjs.map +2 -2
- package/dist-esm/lib/hooks/EditorComponentsContext.mjs.map +2 -2
- package/dist-esm/lib/utils/debug-flags.mjs +0 -6
- package/dist-esm/lib/utils/debug-flags.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/LocalIndexedDb.mjs +19 -8
- package/dist-esm/lib/utils/sync/LocalIndexedDb.mjs.map +2 -2
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +3 -0
- package/dist-esm/lib/utils/sync/TLLocalSyncClient.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 +5 -1
- package/src/lib/TldrawEditor.tsx +8 -6
- package/src/lib/components/default-components/DefaultCanvas.tsx +7 -1
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +13 -5
- package/src/lib/components/default-components/DefaultSpinner.tsx +4 -1
- package/src/lib/editor/Editor.test.ts +45 -0
- package/src/lib/editor/Editor.ts +40 -35
- package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +1 -0
- package/src/lib/editor/managers/ClickManager/ClickManager.ts +3 -2
- package/src/lib/editor/managers/EditorManager.test.ts +32 -1
- package/src/lib/editor/managers/EditorManager.ts +15 -3
- package/src/lib/editor/managers/PerformanceManager/PerformanceManager.test.ts +80 -0
- package/src/lib/editor/managers/PerformanceManager/PerformanceManager.ts +54 -81
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.ts +1 -1
- package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +32 -36
- package/src/lib/editor/managers/SnapManager/HandleSnaps.ts +13 -2
- package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +13 -0
- package/src/lib/editor/managers/SnapManager/SnapManager.ts +6 -4
- package/src/lib/editor/types/external-content.ts +5 -0
- package/src/lib/hooks/EditorComponentsContext.tsx +2 -1
- package/src/lib/utils/debug-flags.ts +0 -6
- package/src/lib/utils/sync/LocalIndexedDb.test.ts +93 -1
- package/src/lib/utils/sync/LocalIndexedDb.ts +23 -8
- package/src/lib/utils/sync/TLLocalSyncClient.test.ts +21 -1
- package/src/lib/utils/sync/TLLocalSyncClient.ts +6 -0
- package/src/version.ts +3 -3
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -2013,7 +2013,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
2013
2013
|
this._updateInstanceState(partial, { history: 'ignore', ...historyOptions })
|
|
2014
2014
|
|
|
2015
2015
|
if (partial.isChangingStyle !== undefined) {
|
|
2016
|
-
clearTimeout(this._isChangingStyleTimeout)
|
|
2016
|
+
this.timers.clearTimeout(this._isChangingStyleTimeout)
|
|
2017
2017
|
if (partial.isChangingStyle === true) {
|
|
2018
2018
|
// If we've set to true, set a new reset timeout to change the value back to false after 1 seconds
|
|
2019
2019
|
this._isChangingStyleTimeout = this.timers.setTimeout(() => {
|
|
@@ -10797,22 +10797,22 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10797
10797
|
const releaseMeta = this._metaKeyTimeout !== -1
|
|
10798
10798
|
|
|
10799
10799
|
if (releaseShift) {
|
|
10800
|
-
clearTimeout(this._shiftKeyTimeout)
|
|
10800
|
+
this.timers.clearTimeout(this._shiftKeyTimeout)
|
|
10801
10801
|
this._shiftKeyTimeout = -1
|
|
10802
10802
|
this.inputs.setShiftKey(false)
|
|
10803
10803
|
}
|
|
10804
10804
|
if (releaseAlt) {
|
|
10805
|
-
clearTimeout(this._altKeyTimeout)
|
|
10805
|
+
this.timers.clearTimeout(this._altKeyTimeout)
|
|
10806
10806
|
this._altKeyTimeout = -1
|
|
10807
10807
|
this.inputs.setAltKey(false)
|
|
10808
10808
|
}
|
|
10809
10809
|
if (releaseCtrl) {
|
|
10810
|
-
clearTimeout(this._ctrlKeyTimeout)
|
|
10810
|
+
this.timers.clearTimeout(this._ctrlKeyTimeout)
|
|
10811
10811
|
this._ctrlKeyTimeout = -1
|
|
10812
10812
|
this.inputs.setCtrlKey(false)
|
|
10813
10813
|
}
|
|
10814
10814
|
if (releaseMeta) {
|
|
10815
|
-
clearTimeout(this._metaKeyTimeout)
|
|
10815
|
+
this.timers.clearTimeout(this._metaKeyTimeout)
|
|
10816
10816
|
this._metaKeyTimeout = -1
|
|
10817
10817
|
this.inputs.setMetaKey(false)
|
|
10818
10818
|
}
|
|
@@ -10962,7 +10962,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10962
10962
|
}
|
|
10963
10963
|
|
|
10964
10964
|
if (info.shiftKey) {
|
|
10965
|
-
clearTimeout(this._shiftKeyTimeout)
|
|
10965
|
+
this.timers.clearTimeout(this._shiftKeyTimeout)
|
|
10966
10966
|
this._shiftKeyTimeout = -1
|
|
10967
10967
|
inputs.setShiftKey(true)
|
|
10968
10968
|
} else if (!info.shiftKey && inputs.getShiftKey() && this._shiftKeyTimeout === -1) {
|
|
@@ -10970,7 +10970,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10970
10970
|
}
|
|
10971
10971
|
|
|
10972
10972
|
if (info.altKey) {
|
|
10973
|
-
clearTimeout(this._altKeyTimeout)
|
|
10973
|
+
this.timers.clearTimeout(this._altKeyTimeout)
|
|
10974
10974
|
this._altKeyTimeout = -1
|
|
10975
10975
|
inputs.setAltKey(true)
|
|
10976
10976
|
} else if (!info.altKey && inputs.getAltKey() && this._altKeyTimeout === -1) {
|
|
@@ -10978,7 +10978,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10978
10978
|
}
|
|
10979
10979
|
|
|
10980
10980
|
if (info.ctrlKey) {
|
|
10981
|
-
clearTimeout(this._ctrlKeyTimeout)
|
|
10981
|
+
this.timers.clearTimeout(this._ctrlKeyTimeout)
|
|
10982
10982
|
this._ctrlKeyTimeout = -1
|
|
10983
10983
|
inputs.setCtrlKey(true)
|
|
10984
10984
|
} else if (!info.ctrlKey && inputs.getCtrlKey() && this._ctrlKeyTimeout === -1) {
|
|
@@ -10988,7 +10988,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
10988
10988
|
if (info.metaKey && info.name !== 'key_up') {
|
|
10989
10989
|
// Unlike the other modifiers, the native metaKey property is still true on keyup.
|
|
10990
10990
|
// If we don't have this guard, then the metakey will be left true without the timeout.
|
|
10991
|
-
clearTimeout(this._metaKeyTimeout)
|
|
10991
|
+
this.timers.clearTimeout(this._metaKeyTimeout)
|
|
10992
10992
|
this._metaKeyTimeout = -1
|
|
10993
10993
|
inputs.setMetaKey(true)
|
|
10994
10994
|
} else if (!info.metaKey && inputs.getMetaKey() && this._metaKeyTimeout === -1) {
|
|
@@ -11005,8 +11005,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11005
11005
|
|
|
11006
11006
|
switch (type) {
|
|
11007
11007
|
case 'pinch': {
|
|
11008
|
-
|
|
11009
|
-
clearTimeout(this._longPressTimeout)
|
|
11008
|
+
this.timers.clearTimeout(this._longPressTimeout)
|
|
11010
11009
|
this.inputs.updateFromEvent(info)
|
|
11011
11010
|
|
|
11012
11011
|
switch (info.name) {
|
|
@@ -11043,6 +11042,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11043
11042
|
}
|
|
11044
11043
|
case 'pinch': {
|
|
11045
11044
|
if (!inputs.getIsPinching()) return
|
|
11045
|
+
// Lock the zoom, not the gesture: skipping pinch_start would send the fingers'
|
|
11046
|
+
// pointer events to the tool, and skipping pinch_end would leave isPinching stuck.
|
|
11047
|
+
if (cameraOptions.isLocked) return
|
|
11046
11048
|
|
|
11047
11049
|
const {
|
|
11048
11050
|
point: { z = 1 },
|
|
@@ -11116,8 +11118,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11116
11118
|
let wheelBehavior = cameraOptions.wheelBehavior
|
|
11117
11119
|
const inputMode = this.user.getUserPreferences().inputMode
|
|
11118
11120
|
|
|
11119
|
-
//
|
|
11120
|
-
|
|
11121
|
+
// The user's input mode preference picks between pan and zoom, but it must not
|
|
11122
|
+
// re-enable a wheel the app disabled with `wheelBehavior: 'none'`.
|
|
11123
|
+
if (inputMode !== null && wheelBehavior !== 'none') {
|
|
11121
11124
|
wheelBehavior = inputMode === 'trackpad' ? 'pan' : 'zoom'
|
|
11122
11125
|
}
|
|
11123
11126
|
|
|
@@ -11191,15 +11194,18 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11191
11194
|
// Ignore pointer events while we're pinching
|
|
11192
11195
|
if (inputs.getIsPinching()) return
|
|
11193
11196
|
|
|
11194
|
-
this.inputs.updateFromEvent(info)
|
|
11195
11197
|
const { isPen } = info
|
|
11196
11198
|
const { isPenMode } = instanceState
|
|
11197
11199
|
|
|
11200
|
+
// In pen mode, reject non-pen input (a resting palm) before it touches any
|
|
11201
|
+
// state: otherwise updateFromEvent moves the origin point out from under the
|
|
11202
|
+
// pen's drag, and a palm pointer_up clears the pen's isPointing/isDragging.
|
|
11203
|
+
if (isPenMode && !isPen) return
|
|
11204
|
+
|
|
11205
|
+
this.inputs.updateFromEvent(info)
|
|
11206
|
+
|
|
11198
11207
|
switch (info.name) {
|
|
11199
11208
|
case 'pointer_down': {
|
|
11200
|
-
// If we're in pen mode and the input is not a pen type, then stop here
|
|
11201
|
-
if (isPenMode && !isPen) return
|
|
11202
|
-
|
|
11203
11209
|
// A pointer down starts a new interaction, so flush any modifier that's
|
|
11204
11210
|
// still lingering in its release-debounce window: treat it as released now.
|
|
11205
11211
|
this._releaseDebouncedModifiers()
|
|
@@ -11255,8 +11261,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11255
11261
|
this.interrupt()
|
|
11256
11262
|
}
|
|
11257
11263
|
|
|
11258
|
-
// On devices with erasers (like the Surface Pen or Wacom Pen), button 5 is the eraser
|
|
11259
|
-
|
|
11264
|
+
// On devices with erasers (like the Surface Pen or Wacom Pen), button 5 is the eraser.
|
|
11265
|
+
// Guarded on the eraser tool existing: the bare editor can be configured without
|
|
11266
|
+
// it, and an unguarded transition would throw and crash the editor.
|
|
11267
|
+
if (info.button === STYLUS_ERASER_BUTTON && this.getStateDescendant('eraser')) {
|
|
11260
11268
|
this._restoreToolId = this.getCurrentToolId()
|
|
11261
11269
|
this.complete()
|
|
11262
11270
|
this.setCurrentTool('eraser')
|
|
@@ -11266,10 +11274,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11266
11274
|
this._prevCursor = this.getInstanceState().cursor.type
|
|
11267
11275
|
}
|
|
11268
11276
|
this.inputs.setIsPanning(true)
|
|
11269
|
-
clearTimeout(this._longPressTimeout)
|
|
11277
|
+
this.timers.clearTimeout(this._longPressTimeout)
|
|
11270
11278
|
} else if (info.button === RIGHT_MOUSE_BUTTON && this.options.rightClickPanning) {
|
|
11271
11279
|
this.inputs.setIsRightPointing(true)
|
|
11272
|
-
clearTimeout(this._longPressTimeout)
|
|
11280
|
+
this.timers.clearTimeout(this._longPressTimeout)
|
|
11273
11281
|
return this
|
|
11274
11282
|
}
|
|
11275
11283
|
|
|
@@ -11284,9 +11292,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11284
11292
|
break
|
|
11285
11293
|
}
|
|
11286
11294
|
case 'pointer_move': {
|
|
11287
|
-
// If the user is in pen mode, but the pointer is not a pen, stop here.
|
|
11288
|
-
if (!isPen && isPenMode) return
|
|
11289
|
-
|
|
11290
11295
|
const { x: cx, y: cy, z: cz } = unsafe__withoutCapture(() => this.getCamera())
|
|
11291
11296
|
|
|
11292
11297
|
// Right-click pointing: waiting to see if this becomes a drag
|
|
@@ -11337,7 +11342,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11337
11342
|
) {
|
|
11338
11343
|
// Start dragging
|
|
11339
11344
|
inputs.setIsDragging(true)
|
|
11340
|
-
clearTimeout(this._longPressTimeout)
|
|
11345
|
+
this.timers.clearTimeout(this._longPressTimeout)
|
|
11341
11346
|
}
|
|
11342
11347
|
break
|
|
11343
11348
|
}
|
|
@@ -11345,13 +11350,10 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11345
11350
|
// Stop dragging / pointing
|
|
11346
11351
|
inputs.setIsDragging(false)
|
|
11347
11352
|
inputs.setIsPointing(false)
|
|
11348
|
-
clearTimeout(this._longPressTimeout)
|
|
11353
|
+
this.timers.clearTimeout(this._longPressTimeout)
|
|
11349
11354
|
// Remove the button from the buttons set
|
|
11350
11355
|
inputs.buttons.delete(info.button)
|
|
11351
11356
|
|
|
11352
|
-
// If we're in pen mode and we're not using a pen, stop here
|
|
11353
|
-
if (instanceState.isPenMode && !isPen) return
|
|
11354
|
-
|
|
11355
11357
|
// Right-click pointing ended without dragging—this is a static
|
|
11356
11358
|
// right-click, so let it through to the state chart as right_click.
|
|
11357
11359
|
// Check isPanning first: if we transitioned to panning, isRightPointing
|
|
@@ -11421,7 +11423,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11421
11423
|
})
|
|
11422
11424
|
}
|
|
11423
11425
|
} else {
|
|
11424
|
-
if (info.button === STYLUS_ERASER_BUTTON) {
|
|
11426
|
+
if (info.button === STYLUS_ERASER_BUTTON && this.getStateDescendant('eraser')) {
|
|
11425
11427
|
// If we were erasing with a stylus button, restore the tool we were using before we started erasing
|
|
11426
11428
|
this.complete()
|
|
11427
11429
|
this.setCurrentTool(this._restoreToolId)
|
|
@@ -11461,7 +11463,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11461
11463
|
|
|
11462
11464
|
this.inputs.setIsPanning(true)
|
|
11463
11465
|
this.inputs.setIsSpacebarPanning(true)
|
|
11464
|
-
clearTimeout(this._longPressTimeout)
|
|
11466
|
+
this.timers.clearTimeout(this._longPressTimeout)
|
|
11465
11467
|
this.setCursor({
|
|
11466
11468
|
type: this.inputs.getIsPointing() ? 'grabbing' : 'grab',
|
|
11467
11469
|
rotation: 0,
|
|
@@ -11489,7 +11491,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11489
11491
|
}
|
|
11490
11492
|
}
|
|
11491
11493
|
|
|
11492
|
-
|
|
11494
|
+
// `_animateToViewport` bypasses `setCamera`, so honor the lock here.
|
|
11495
|
+
if (offset && !cameraOptions.isLocked) {
|
|
11493
11496
|
const bounds = this.getViewportPageBounds()
|
|
11494
11497
|
const next = bounds.clone().translate(offset.mulV({ x: bounds.w, y: bounds.h }))
|
|
11495
11498
|
this._animateToViewport(next, { animation: { duration: 320 } })
|
|
@@ -11551,9 +11554,11 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11551
11554
|
this.setCurrentTool('select')
|
|
11552
11555
|
}
|
|
11553
11556
|
|
|
11554
|
-
//
|
|
11557
|
+
// Send the event to the click manager. In pen mode only pen clicks count; otherwise
|
|
11558
|
+
// every pointer does, including an indirect tablet stylus (a pen that never enables
|
|
11559
|
+
// pen mode), which would otherwise never produce double-click events.
|
|
11555
11560
|
const { isPenMode } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
|
|
11556
|
-
if (info.isPen
|
|
11561
|
+
if (!isPenMode || info.isPen) {
|
|
11557
11562
|
// The click manager may return a new event, i.e. a double click event
|
|
11558
11563
|
// depending on the event coming in and its own state. If the event has
|
|
11559
11564
|
// changed then hand both events to the statechart
|
|
@@ -11585,7 +11590,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
11585
11590
|
private maybeTrackPerformance(name: string) {
|
|
11586
11591
|
if (debugFlags.measurePerformance.get()) {
|
|
11587
11592
|
if (this.performanceTracker.isStarted()) {
|
|
11588
|
-
clearTimeout(this.performanceTrackerTimeout)
|
|
11593
|
+
this.timers.clearTimeout(this.performanceTrackerTimeout)
|
|
11589
11594
|
} else {
|
|
11590
11595
|
this.performanceTracker.start(name)
|
|
11591
11596
|
}
|
|
@@ -25,7 +25,7 @@ export class ClickManager {
|
|
|
25
25
|
@bind
|
|
26
26
|
_getClickTimeout(state: TLClickState, id = uniqueId()) {
|
|
27
27
|
this._clickId = id
|
|
28
|
-
clearTimeout(this._clickTimeout)
|
|
28
|
+
this.editor.timers.clearTimeout(this._clickTimeout)
|
|
29
29
|
this._clickTimeout = this.editor.timers.setTimeout(
|
|
30
30
|
() => {
|
|
31
31
|
if (this._clickState === state && this._clickId === id) {
|
|
@@ -166,7 +166,8 @@ export class ClickManager {
|
|
|
166
166
|
*/
|
|
167
167
|
@bind
|
|
168
168
|
cancelDoubleClickTimeout() {
|
|
169
|
-
this.
|
|
169
|
+
this.editor.timers.clearTimeout(this._clickTimeout)
|
|
170
|
+
this._clickTimeout = undefined
|
|
170
171
|
this._clickState = 'idle'
|
|
171
172
|
// when a double click is cancelled, we are no longer pending any further
|
|
172
173
|
// clicks, so we set this to false even if the user is still pressing
|
|
@@ -4,16 +4,24 @@ import { EditorManager } from './EditorManager'
|
|
|
4
4
|
|
|
5
5
|
class TestManager extends EditorManager {
|
|
6
6
|
onTestEvent = vi.fn()
|
|
7
|
+
removeTestEvent: () => void
|
|
7
8
|
|
|
8
9
|
constructor(editor: Editor) {
|
|
9
10
|
super(editor)
|
|
10
|
-
this.addEditorEvent('frame', this.onTestEvent)
|
|
11
|
+
this.removeTestEvent = this.addEditorEvent('frame', this.onTestEvent)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
detachTestEvent() {
|
|
15
|
+
this.unregister(this.removeTestEvent)
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
cancelRaf = vi.fn()
|
|
14
19
|
startRaf() {
|
|
15
20
|
this.register(this.cancelRaf)
|
|
16
21
|
}
|
|
22
|
+
stopRaf() {
|
|
23
|
+
this.unregister(this.cancelRaf)
|
|
24
|
+
}
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
describe('EditorManager', () => {
|
|
@@ -48,4 +56,27 @@ describe('EditorManager', () => {
|
|
|
48
56
|
manager.dispose()
|
|
49
57
|
expect(() => manager.dispose()).not.toThrow()
|
|
50
58
|
})
|
|
59
|
+
|
|
60
|
+
it('unregister runs the disposable now and drops it from dispose', () => {
|
|
61
|
+
manager.startRaf()
|
|
62
|
+
manager.stopRaf()
|
|
63
|
+
expect(manager.cancelRaf).toHaveBeenCalledTimes(1)
|
|
64
|
+
manager.dispose()
|
|
65
|
+
expect(manager.cancelRaf).toHaveBeenCalledTimes(1)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('unregister ignores a disposable that already ran', () => {
|
|
69
|
+
manager.startRaf()
|
|
70
|
+
manager.dispose()
|
|
71
|
+
manager.stopRaf()
|
|
72
|
+
expect(manager.cancelRaf).toHaveBeenCalledTimes(1)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('addEditorEvent returns the registered unsubscribe so it can be detached early', () => {
|
|
76
|
+
manager.detachTestEvent()
|
|
77
|
+
expect(off).toHaveBeenCalledTimes(1)
|
|
78
|
+
expect(off).toHaveBeenCalledWith('frame', manager.onTestEvent)
|
|
79
|
+
manager.dispose()
|
|
80
|
+
expect(off).toHaveBeenCalledTimes(1)
|
|
81
|
+
})
|
|
51
82
|
})
|
|
@@ -16,6 +16,8 @@ import type { TLEventMap } from '../types/emit-types'
|
|
|
16
16
|
* - Reaction (`react(...)` from `@tldraw/state`): `this.register(react(...))`
|
|
17
17
|
* - DOM listener: `this.register(() => el.removeEventListener(...))`
|
|
18
18
|
* - Other resource (cache, index, child manager): `this.register(() => x.dispose())`
|
|
19
|
+
* - Resource held only part of the time (a listener attached while subscribers exist):
|
|
20
|
+
* `this.register(...)` on attach and `this.unregister(...)` on detach, see `PerformanceManager`
|
|
19
21
|
*
|
|
20
22
|
* For timeouts, intervals, and animation frames prefer `editor.timers` (context-grouped
|
|
21
23
|
* and auto-disposed) over raw `setTimeout`. For cleanup on the editor itself rather than a
|
|
@@ -40,16 +42,26 @@ export abstract class EditorManager {
|
|
|
40
42
|
return dispose
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Run a registered teardown now instead of at `dispose()`. A teardown that is no longer
|
|
47
|
+
* registered (already run by `dispose()`, or never registered) is skipped, so nothing is
|
|
48
|
+
* torn down twice.
|
|
49
|
+
*/
|
|
50
|
+
protected unregister(dispose: () => void): void {
|
|
51
|
+
if (this.disposables.delete(dispose)) dispose()
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
/**
|
|
44
55
|
* Subscribe to an editor bus event and register the matching unsubscribe, so the listener
|
|
45
|
-
* is removed automatically on `dispose()`.
|
|
56
|
+
* is removed automatically on `dispose()`. Returns that unsubscribe, for passing to
|
|
57
|
+
* {@link EditorManager.unregister} to detach it before `dispose()`.
|
|
46
58
|
*/
|
|
47
59
|
protected addEditorEvent<E extends keyof TLEventMap>(
|
|
48
60
|
event: E,
|
|
49
61
|
fn: (...args: TLEventMap[E]) => void
|
|
50
|
-
): void {
|
|
62
|
+
): () => void {
|
|
51
63
|
this.editor.on(event, fn as any)
|
|
52
|
-
this.register(() => this.editor.off(event, fn as any))
|
|
64
|
+
return this.register(() => this.editor.off(event, fn as any))
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
/** @internal */
|
|
@@ -38,6 +38,7 @@ function createMockEditor() {
|
|
|
38
38
|
getViewportScreenBounds: () => ({ w: 1920, h: 1080 }),
|
|
39
39
|
timers: {
|
|
40
40
|
setTimeout: (fn: () => void, ms: number) => setTimeout(fn, ms),
|
|
41
|
+
clearTimeout: (id: number) => clearTimeout(id),
|
|
41
42
|
},
|
|
42
43
|
_listeners: listeners,
|
|
43
44
|
} as any
|
|
@@ -94,6 +95,85 @@ describe('PerformanceManager', () => {
|
|
|
94
95
|
})
|
|
95
96
|
})
|
|
96
97
|
|
|
98
|
+
describe('dispose()', () => {
|
|
99
|
+
it('removes every lazily attached editor listener', () => {
|
|
100
|
+
const editor = createMockEditor()
|
|
101
|
+
const pm = new PerformanceManager(editor)
|
|
102
|
+
pm.on('interaction-end', vi.fn())
|
|
103
|
+
pm.on('shapes-created', vi.fn())
|
|
104
|
+
pm.on('shapes-updated', vi.fn())
|
|
105
|
+
pm.on('shapes-deleted', vi.fn())
|
|
106
|
+
expect(editor._listeners['frame']).toHaveLength(1)
|
|
107
|
+
expect(editor._listeners['created-shapes']).toHaveLength(1)
|
|
108
|
+
expect(editor._listeners['edited-shapes']).toHaveLength(1)
|
|
109
|
+
expect(editor._listeners['deleted-shapes']).toHaveLength(1)
|
|
110
|
+
|
|
111
|
+
pm.dispose()
|
|
112
|
+
|
|
113
|
+
expect(editor._listeners).toEqual({
|
|
114
|
+
frame: [],
|
|
115
|
+
'created-shapes': [],
|
|
116
|
+
'edited-shapes': [],
|
|
117
|
+
'deleted-shapes': [],
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('disconnects the LoAF observer', () => {
|
|
122
|
+
const origPO = globalThis.PerformanceObserver
|
|
123
|
+
const mockDisconnect = vi.fn()
|
|
124
|
+
globalThis.PerformanceObserver = class MockPO {
|
|
125
|
+
constructor(_cb: any) {}
|
|
126
|
+
observe() {}
|
|
127
|
+
disconnect = mockDisconnect
|
|
128
|
+
static supportedEntryTypes = ['long-animation-frame']
|
|
129
|
+
} as any
|
|
130
|
+
|
|
131
|
+
const editor = createMockEditor()
|
|
132
|
+
const pm = new PerformanceManager(editor)
|
|
133
|
+
pm.on('interaction-end', vi.fn())
|
|
134
|
+
expect(mockDisconnect).not.toHaveBeenCalled()
|
|
135
|
+
|
|
136
|
+
pm.dispose()
|
|
137
|
+
expect(mockDisconnect).toHaveBeenCalledTimes(1)
|
|
138
|
+
|
|
139
|
+
globalThis.PerformanceObserver = origPO
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('does not tear down a listener that was already detached', () => {
|
|
143
|
+
const editor = createMockEditor()
|
|
144
|
+
const off = vi.spyOn(editor, 'off')
|
|
145
|
+
const pm = new PerformanceManager(editor)
|
|
146
|
+
pm.on('shapes-created', vi.fn())()
|
|
147
|
+
expect(off).toHaveBeenCalledTimes(1)
|
|
148
|
+
|
|
149
|
+
pm.dispose()
|
|
150
|
+
expect(off).toHaveBeenCalledTimes(1)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('makes a stale unsubscribe a no-op after dispose', () => {
|
|
154
|
+
const editor = createMockEditor()
|
|
155
|
+
const off = vi.spyOn(editor, 'off')
|
|
156
|
+
const pm = new PerformanceManager(editor)
|
|
157
|
+
const unsub = pm.on('interaction-end', vi.fn())
|
|
158
|
+
|
|
159
|
+
pm.dispose()
|
|
160
|
+
expect(off).toHaveBeenCalledTimes(1)
|
|
161
|
+
unsub()
|
|
162
|
+
expect(off).toHaveBeenCalledTimes(1)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('attaches again for a subscriber added after dispose', () => {
|
|
166
|
+
const editor = createMockEditor()
|
|
167
|
+
const pm = new PerformanceManager(editor)
|
|
168
|
+
pm.on('shapes-created', vi.fn())
|
|
169
|
+
pm.dispose()
|
|
170
|
+
expect(editor._listeners['created-shapes']).toHaveLength(0)
|
|
171
|
+
|
|
172
|
+
pm.on('shapes-created', vi.fn())
|
|
173
|
+
expect(editor._listeners['created-shapes']).toHaveLength(1)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
97
177
|
describe('interaction tracking', () => {
|
|
98
178
|
it('emits interaction-start when notified', () => {
|
|
99
179
|
const editor = createMockEditor()
|