@tldraw/editor 5.4.0-canary.042da660fa66 → 5.4.0-canary.059d049864ed
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 +31 -1
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/components/MenuClickCapture.js +6 -8
- package/dist-cjs/lib/components/MenuClickCapture.js.map +2 -2
- package/dist-cjs/lib/components/default-components/CanvasOverlays.js +1 -3
- package/dist-cjs/lib/components/default-components/CanvasOverlays.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultErrorFallback.js.map +1 -1
- package/dist-cjs/lib/config/TLUserPreferences.js +6 -2
- package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
- package/dist-cjs/lib/config/createTLStore.js +1 -1
- package/dist-cjs/lib/config/createTLStore.js.map +2 -2
- package/dist-cjs/lib/editor/Editor.js +139 -268
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js +8 -8
- package/dist-cjs/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js +2 -1
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js +79 -6
- package/dist-cjs/lib/editor/managers/InputsManager/InputsManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js +0 -2
- package/dist-cjs/lib/editor/managers/SnapManager/BoundsSnaps.js.map +2 -2
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +1 -0
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
- package/dist-cjs/lib/editor/overlays/OverlayManager.js +3 -7
- package/dist-cjs/lib/editor/overlays/OverlayManager.js.map +2 -2
- package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js +0 -1
- package/dist-cjs/lib/editor/overlays/strokeShapeIndicators.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js +3 -1
- package/dist-cjs/lib/editor/shapes/BaseFrameLikeShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js +1 -0
- package/dist-cjs/lib/editor/shapes/shared/getPerfectDashProps.js.map +2 -2
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js +1 -1
- package/dist-cjs/lib/editor/tools/BaseBoxShapeTool/children/Pointing.js.map +2 -2
- package/dist-cjs/lib/editor/tools/StateNode.js +3 -2
- package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
- package/dist-cjs/lib/globals/menus.js +9 -4
- package/dist-cjs/lib/globals/menus.js.map +2 -2
- package/dist-cjs/lib/hooks/useCanvasEvents.js +2 -0
- package/dist-cjs/lib/hooks/useCanvasEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useDocumentEvents.js +9 -7
- package/dist-cjs/lib/hooks/useDocumentEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/useLocalStore.js +10 -6
- package/dist-cjs/lib/hooks/useLocalStore.js.map +2 -2
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js +1 -0
- package/dist-cjs/lib/hooks/usePassThroughWheelEvents.js.map +2 -2
- package/dist-cjs/lib/hooks/usePeerIds.js +2 -1
- package/dist-cjs/lib/hooks/usePeerIds.js.map +2 -2
- package/dist-cjs/lib/primitives/Box.js +2 -1
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- package/dist-cjs/lib/primitives/Vec.js +4 -3
- package/dist-cjs/lib/primitives/Vec.js.map +2 -2
- package/dist-cjs/lib/primitives/utils.js +2 -1
- package/dist-cjs/lib/primitives/utils.js.map +2 -2
- package/dist-cjs/lib/utils/rotation.js +1 -9
- package/dist-cjs/lib/utils/rotation.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 +31 -1
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/components/MenuClickCapture.mjs +6 -8
- package/dist-esm/lib/components/MenuClickCapture.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/CanvasOverlays.mjs +1 -3
- package/dist-esm/lib/components/default-components/CanvasOverlays.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultErrorFallback.mjs.map +1 -1
- package/dist-esm/lib/config/TLUserPreferences.mjs +6 -2
- package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
- package/dist-esm/lib/config/createTLStore.mjs +1 -1
- package/dist-esm/lib/config/createTLStore.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +147 -269
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs +9 -9
- package/dist-esm/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs +2 -1
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs +81 -8
- package/dist-esm/lib/editor/managers/InputsManager/InputsManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs +0 -2
- package/dist-esm/lib/editor/managers/SnapManager/BoundsSnaps.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +1 -0
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
- package/dist-esm/lib/editor/overlays/OverlayManager.mjs +3 -7
- package/dist-esm/lib/editor/overlays/OverlayManager.mjs.map +2 -2
- package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs +0 -1
- package/dist-esm/lib/editor/overlays/strokeShapeIndicators.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs +3 -1
- package/dist-esm/lib/editor/shapes/BaseFrameLikeShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs +1 -0
- package/dist-esm/lib/editor/shapes/shared/getPerfectDashProps.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs +1 -1
- package/dist-esm/lib/editor/tools/BaseBoxShapeTool/children/Pointing.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/StateNode.mjs +3 -2
- package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
- package/dist-esm/lib/globals/menus.mjs +9 -4
- package/dist-esm/lib/globals/menus.mjs.map +2 -2
- package/dist-esm/lib/hooks/useCanvasEvents.mjs +2 -0
- package/dist-esm/lib/hooks/useCanvasEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useDocumentEvents.mjs +9 -7
- package/dist-esm/lib/hooks/useDocumentEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/useLocalStore.mjs +11 -7
- package/dist-esm/lib/hooks/useLocalStore.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs +1 -0
- package/dist-esm/lib/hooks/usePassThroughWheelEvents.mjs.map +2 -2
- package/dist-esm/lib/hooks/usePeerIds.mjs +2 -1
- package/dist-esm/lib/hooks/usePeerIds.mjs.map +2 -2
- package/dist-esm/lib/primitives/Box.mjs +2 -1
- package/dist-esm/lib/primitives/Box.mjs.map +2 -2
- package/dist-esm/lib/primitives/Vec.mjs +4 -3
- package/dist-esm/lib/primitives/Vec.mjs.map +2 -2
- package/dist-esm/lib/primitives/utils.mjs +2 -1
- package/dist-esm/lib/primitives/utils.mjs.map +2 -2
- package/dist-esm/lib/utils/rotation.mjs +1 -9
- package/dist-esm/lib/utils/rotation.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/lib/components/MenuClickCapture.tsx +19 -18
- package/src/lib/components/default-components/CanvasOverlays.tsx +1 -3
- package/src/lib/components/default-components/DefaultErrorFallback.tsx +1 -1
- package/src/lib/config/TLUserPreferences.test.ts +18 -1
- package/src/lib/config/TLUserPreferences.ts +8 -4
- package/src/lib/config/createTLStore.ts +1 -1
- package/src/lib/editor/Editor.ts +196 -341
- package/src/lib/editor/managers/CollaboratorsManager/CollaboratorsManager.ts +9 -9
- package/src/lib/editor/managers/FontManager/FontManager.ts +2 -1
- package/src/lib/editor/managers/InputsManager/InputsManager.test.ts +199 -3
- package/src/lib/editor/managers/InputsManager/InputsManager.ts +128 -9
- package/src/lib/editor/managers/SnapManager/BoundsSnaps.ts +0 -2
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +1 -0
- package/src/lib/editor/overlays/OverlayManager.ts +3 -7
- package/src/lib/editor/overlays/strokeShapeIndicators.ts +0 -1
- package/src/lib/editor/shapes/BaseFrameLikeShapeUtil.tsx +5 -1
- package/src/lib/editor/shapes/shared/getPerfectDashProps.ts +3 -0
- package/src/lib/editor/tools/BaseBoxShapeTool/children/Pointing.ts +4 -1
- package/src/lib/editor/tools/StateNode.test.ts +64 -0
- package/src/lib/editor/tools/StateNode.ts +12 -2
- package/src/lib/globals/menus.test.ts +33 -0
- package/src/lib/globals/menus.ts +11 -4
- package/src/lib/hooks/useCanvasEvents.ts +2 -0
- package/src/lib/hooks/useDocumentEvents.ts +14 -7
- package/src/lib/hooks/useLocalStore.ts +16 -8
- package/src/lib/hooks/usePassThroughWheelEvents.ts +3 -0
- package/src/lib/hooks/usePeerIds.ts +2 -1
- package/src/lib/primitives/Box.test.ts +9 -1
- package/src/lib/primitives/Box.ts +3 -1
- package/src/lib/primitives/Vec.test.ts +10 -0
- package/src/lib/primitives/Vec.ts +4 -4
- package/src/lib/primitives/utils.test.ts +10 -1
- package/src/lib/primitives/utils.ts +4 -1
- package/src/lib/test/useCanvasEvents.test.tsx +74 -0
- package/src/lib/test/useDocumentEvents.test.tsx +31 -0
- package/src/lib/test/usePassThroughWheelEvents.test.tsx +25 -2
- package/src/lib/utils/rotation.ts +4 -12
- package/src/version.ts +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EMPTY_ARRAY, atom, computed } from '@tldraw/state'
|
|
2
2
|
import type { TLInstancePresence } from '@tldraw/tlschema'
|
|
3
|
-
import { areArraysShallowEqual
|
|
3
|
+
import { areArraysShallowEqual } from '@tldraw/utils'
|
|
4
4
|
import type { Editor } from '../../Editor'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -54,14 +54,14 @@ export class CollaboratorsManager {
|
|
|
54
54
|
getCollaborators(): TLInstancePresence[] {
|
|
55
55
|
const allPresenceRecords = this._getCollaboratorsQuery().get()
|
|
56
56
|
if (!allPresenceRecords.length) return EMPTY_ARRAY
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
const latestByUserId = new Map<string, TLInstancePresence>()
|
|
58
|
+
for (const presence of allPresenceRecords) {
|
|
59
|
+
const latest = latestByUserId.get(presence.userId)
|
|
60
|
+
if (!latest || (presence.lastActivityTimestamp ?? 0) > (latest.lastActivityTimestamp ?? 0)) {
|
|
61
|
+
latestByUserId.set(presence.userId, presence)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return [...latestByUserId.keys()].sort().map((id) => latestByUserId.get(id)!)
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/**
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
mapObjectMapValues,
|
|
9
9
|
objectMapEntries,
|
|
10
10
|
} from '@tldraw/utils'
|
|
11
|
+
import { areShapesContentEqual } from '../../../utils/areShapesContentEqual'
|
|
11
12
|
import type { Editor } from '../../Editor'
|
|
12
13
|
|
|
13
14
|
interface FontState {
|
|
@@ -41,7 +42,7 @@ export class FontManager {
|
|
|
41
42
|
},
|
|
42
43
|
{
|
|
43
44
|
areResultsEqual: areArraysShallowEqual,
|
|
44
|
-
areRecordsEqual:
|
|
45
|
+
areRecordsEqual: areShapesContentEqual,
|
|
45
46
|
}
|
|
46
47
|
)
|
|
47
48
|
|
|
@@ -1,16 +1,39 @@
|
|
|
1
|
+
import { InstancePresenceRecordType, TLPOINTER_ID, createUserId } from '@tldraw/tlschema'
|
|
2
|
+
import { vi } from 'vitest'
|
|
1
3
|
import { createTLStore } from '../../../config/createTLStore'
|
|
4
|
+
import { TldrawOptions } from '../../../options'
|
|
2
5
|
import { Editor } from '../../Editor'
|
|
6
|
+
import { StateNode } from '../../tools/StateNode'
|
|
3
7
|
|
|
4
|
-
function
|
|
8
|
+
function createPresence(editor: Editor) {
|
|
9
|
+
return InstancePresenceRecordType.create({
|
|
10
|
+
id: InstancePresenceRecordType.createId('peer'),
|
|
11
|
+
userId: createUserId('peer'),
|
|
12
|
+
userName: 'Peer',
|
|
13
|
+
currentPageId: editor.getCurrentPageId(),
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createTestEditor(options?: Partial<TldrawOptions>) {
|
|
5
18
|
const store = createTLStore({})
|
|
6
19
|
store.ensureStoreIsUsable()
|
|
7
|
-
|
|
20
|
+
// Attached to the document so that events dispatched on the container
|
|
21
|
+
// propagate to the window, where the activity listeners live.
|
|
22
|
+
const container = document.createElement('div')
|
|
23
|
+
document.body.appendChild(container)
|
|
24
|
+
const editor = new Editor({
|
|
8
25
|
store,
|
|
9
26
|
bindingUtils: [],
|
|
10
27
|
shapeUtils: [],
|
|
11
|
-
getContainer: () =>
|
|
28
|
+
getContainer: () => container,
|
|
12
29
|
tools: [],
|
|
30
|
+
options,
|
|
13
31
|
})
|
|
32
|
+
editor.disposables.add(() => container.remove())
|
|
33
|
+
// Input listeners only attach once the editor is mounted, which is normally
|
|
34
|
+
// emitted by `TldrawEditor` when the container is in the DOM.
|
|
35
|
+
editor.emit('mount')
|
|
36
|
+
return editor
|
|
14
37
|
}
|
|
15
38
|
|
|
16
39
|
describe('InputsManager', () => {
|
|
@@ -37,6 +60,179 @@ describe('InputsManager', () => {
|
|
|
37
60
|
expect(editor.inputs.getPointerVelocity().len()).toBeGreaterThan(0)
|
|
38
61
|
})
|
|
39
62
|
|
|
63
|
+
describe('markActivity', () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
vi.useFakeTimers()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
vi.useRealTimers()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('does nothing when there are no collaborators', () => {
|
|
73
|
+
editor.inputs.markActivity()
|
|
74
|
+
|
|
75
|
+
expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(0)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('stamps the pointer record, preserving its position', () => {
|
|
79
|
+
editor.store.put([createPresence(editor)])
|
|
80
|
+
editor.store.put([{ ...editor.store.get(TLPOINTER_ID)!, x: 5, y: 6 }])
|
|
81
|
+
|
|
82
|
+
editor.inputs.markActivity()
|
|
83
|
+
|
|
84
|
+
expect(editor.store.get(TLPOINTER_ID)!).toMatchObject({
|
|
85
|
+
x: 5,
|
|
86
|
+
y: 6,
|
|
87
|
+
lastActivityTimestamp: Date.now(),
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('throttles stamps on the leading edge', () => {
|
|
92
|
+
editor.store.put([createPresence(editor)])
|
|
93
|
+
|
|
94
|
+
editor.inputs.markActivity()
|
|
95
|
+
const firstStamp = editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp
|
|
96
|
+
|
|
97
|
+
vi.advanceTimersByTime(500)
|
|
98
|
+
editor.inputs.markActivity()
|
|
99
|
+
expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(firstStamp)
|
|
100
|
+
|
|
101
|
+
vi.advanceTimersByTime(600)
|
|
102
|
+
editor.inputs.markActivity()
|
|
103
|
+
expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(firstStamp + 1100)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('stamps on keyboard input in the container', () => {
|
|
107
|
+
editor.store.put([createPresence(editor)])
|
|
108
|
+
|
|
109
|
+
editor.getContainer().dispatchEvent(new KeyboardEvent('keydown', { key: 'a', code: 'KeyA' }))
|
|
110
|
+
|
|
111
|
+
expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(Date.now())
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('stamps on pointer input in the container', () => {
|
|
115
|
+
editor.store.put([createPresence(editor)])
|
|
116
|
+
|
|
117
|
+
editor.getContainer().dispatchEvent(new Event('pointermove', { bubbles: true }))
|
|
118
|
+
|
|
119
|
+
expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(Date.now())
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('stamps on text input that fires no keydown, e.g. IME and gesture typing', () => {
|
|
123
|
+
editor.store.put([createPresence(editor)])
|
|
124
|
+
|
|
125
|
+
editor.getContainer().dispatchEvent(new Event('beforeinput', { bubbles: true }))
|
|
126
|
+
|
|
127
|
+
expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(Date.now())
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('stamps on input elsewhere in the tab, outside the container', () => {
|
|
131
|
+
editor.store.put([createPresence(editor)])
|
|
132
|
+
|
|
133
|
+
document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', code: 'KeyA' }))
|
|
134
|
+
|
|
135
|
+
expect(editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(Date.now())
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('keeps a minimum throttle window when the idle timeout is tiny', () => {
|
|
139
|
+
const tinyEditor = createTestEditor({ collaboratorIdleTimeoutMs: 0 })
|
|
140
|
+
try {
|
|
141
|
+
tinyEditor.store.put([createPresence(tinyEditor)])
|
|
142
|
+
|
|
143
|
+
tinyEditor.inputs.markActivity()
|
|
144
|
+
const stamp = tinyEditor.store.get(TLPOINTER_ID)!.lastActivityTimestamp
|
|
145
|
+
|
|
146
|
+
vi.advanceTimersByTime(50)
|
|
147
|
+
tinyEditor.inputs.markActivity()
|
|
148
|
+
expect(tinyEditor.store.get(TLPOINTER_ID)!.lastActivityTimestamp).toBe(stamp)
|
|
149
|
+
} finally {
|
|
150
|
+
tinyEditor.dispose()
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('attaches input listeners only while sharing with someone', () => {
|
|
155
|
+
const addEventListener = vi.spyOn(window, 'addEventListener')
|
|
156
|
+
const removeEventListener = vi.spyOn(window, 'removeEventListener')
|
|
157
|
+
const soloEditor = createTestEditor()
|
|
158
|
+
try {
|
|
159
|
+
expect(addEventListener).not.toHaveBeenCalled()
|
|
160
|
+
|
|
161
|
+
soloEditor.store.put([createPresence(soloEditor)])
|
|
162
|
+
expect(addEventListener).toHaveBeenCalledWith('pointermove', expect.anything(), {
|
|
163
|
+
capture: true,
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
soloEditor.store.remove([createPresence(soloEditor).id])
|
|
167
|
+
expect(removeEventListener).toHaveBeenCalledWith('pointermove', expect.anything(), {
|
|
168
|
+
capture: true,
|
|
169
|
+
})
|
|
170
|
+
} finally {
|
|
171
|
+
soloEditor.dispose()
|
|
172
|
+
addEventListener.mockRestore()
|
|
173
|
+
removeEventListener.mockRestore()
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('attaches no input listeners when construction fails before mount', () => {
|
|
178
|
+
class DuplicateTool extends StateNode {
|
|
179
|
+
static override id = 'duplicate-tool'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const addEventListener = vi.spyOn(window, 'addEventListener')
|
|
183
|
+
try {
|
|
184
|
+
const store = createTLStore({})
|
|
185
|
+
store.ensureStoreIsUsable()
|
|
186
|
+
expect(
|
|
187
|
+
() =>
|
|
188
|
+
new Editor({
|
|
189
|
+
store,
|
|
190
|
+
bindingUtils: [],
|
|
191
|
+
shapeUtils: [],
|
|
192
|
+
getContainer: () => document.createElement('div'),
|
|
193
|
+
// The duplicate id makes the constructor throw after the inputs
|
|
194
|
+
// manager is built, so the editor is never disposed.
|
|
195
|
+
tools: [DuplicateTool, DuplicateTool],
|
|
196
|
+
})
|
|
197
|
+
).toThrow()
|
|
198
|
+
|
|
199
|
+
expect(addEventListener).not.toHaveBeenCalled()
|
|
200
|
+
} finally {
|
|
201
|
+
addEventListener.mockRestore()
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('does not stamp on pointer moves with no DOM input, e.g. following a user', () => {
|
|
206
|
+
editor.store.put([createPresence(editor)])
|
|
207
|
+
|
|
208
|
+
editor.inputs.markActivity()
|
|
209
|
+
const stamp = editor.store.get(TLPOINTER_ID)!.lastActivityTimestamp
|
|
210
|
+
|
|
211
|
+
vi.advanceTimersByTime(5000)
|
|
212
|
+
editor.inputs.updateFromEvent({
|
|
213
|
+
type: 'pointer',
|
|
214
|
+
name: 'pointer_move',
|
|
215
|
+
point: { x: 100, y: 50 },
|
|
216
|
+
pointerId: 1,
|
|
217
|
+
button: 0,
|
|
218
|
+
isPen: false,
|
|
219
|
+
target: 'canvas',
|
|
220
|
+
shiftKey: false,
|
|
221
|
+
altKey: false,
|
|
222
|
+
ctrlKey: false,
|
|
223
|
+
metaKey: false,
|
|
224
|
+
accelKey: false,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// The pointer position updates, but the activity clock doesn't.
|
|
228
|
+
expect(editor.store.get(TLPOINTER_ID)!).toMatchObject({
|
|
229
|
+
x: 100,
|
|
230
|
+
y: 50,
|
|
231
|
+
lastActivityTimestamp: stamp,
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
40
236
|
it('stops updating pointer velocity after dispose', () => {
|
|
41
237
|
const point = editor.inputs.getCurrentScreenPoint()
|
|
42
238
|
point.x = 0
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { atom, computed, unsafe__withoutCapture } from '@tldraw/state'
|
|
1
|
+
import { atom, computed, react, unsafe__withoutCapture } from '@tldraw/state'
|
|
2
2
|
import { AtomSet } from '@tldraw/store'
|
|
3
3
|
import { TLINSTANCE_ID, TLPOINTER_ID } from '@tldraw/tlschema'
|
|
4
|
-
import { bind } from '@tldraw/utils'
|
|
5
|
-
import { INTERNAL_POINTER_IDS } from '../../../constants'
|
|
4
|
+
import { bind, throttle } from '@tldraw/utils'
|
|
6
5
|
import { Vec } from '../../../primitives/Vec'
|
|
7
6
|
import { isAccelKey } from '../../../utils/keyboard'
|
|
8
7
|
import type { Editor } from '../../Editor'
|
|
@@ -12,11 +11,85 @@ import { EditorManager } from '../EditorManager'
|
|
|
12
11
|
const POINTER_VELOCITY_REFERENCE_INTERVAL_MS = 16
|
|
13
12
|
const POINTER_VELOCITY_REFERENCE_SMOOTHING = 0.5
|
|
14
13
|
|
|
14
|
+
// How often `markActivity` is allowed to stamp the activity timestamp. Each
|
|
15
|
+
// stamp re-broadcasts presence, so this must be long enough to keep input
|
|
16
|
+
// bursts (e.g. typing, held keys) from flooding the network. It must also stay
|
|
17
|
+
// comfortably shorter than `collaboratorIdleTimeoutMs` so a continuously-active
|
|
18
|
+
// peer never flickers to idle between stamps; the throttle window is capped
|
|
19
|
+
// below that option when it's configured shorter than the default, but never
|
|
20
|
+
// below the floor, so a degenerate idle timeout can't disable throttling.
|
|
21
|
+
const ACTIVITY_TIMESTAMP_THROTTLE_MS = 1000
|
|
22
|
+
const ACTIVITY_TIMESTAMP_THROTTLE_FLOOR_MS = 100
|
|
23
|
+
|
|
24
|
+
// DOM events that count as presence activity. `beforeinput` covers IME and
|
|
25
|
+
// mobile keyboards (e.g. gesture typing), which often don't fire per-character
|
|
26
|
+
// keydown events. `gesturestart`/`gesturechange` are Safari's proprietary
|
|
27
|
+
// trackpad-pinch events, which produce neither pointer nor wheel events.
|
|
28
|
+
const ACTIVITY_EVENTS = [
|
|
29
|
+
'pointerdown',
|
|
30
|
+
'pointermove',
|
|
31
|
+
'pointerup',
|
|
32
|
+
'keydown',
|
|
33
|
+
'beforeinput',
|
|
34
|
+
'wheel',
|
|
35
|
+
'gesturestart',
|
|
36
|
+
'gesturechange',
|
|
37
|
+
]
|
|
38
|
+
|
|
15
39
|
/** @public */
|
|
16
40
|
export class InputsManager extends EditorManager {
|
|
17
41
|
constructor(editor: Editor) {
|
|
18
42
|
super(editor)
|
|
19
43
|
this.addEditorEvent('frame', this._onFrame)
|
|
44
|
+
|
|
45
|
+
// Track input only while mounted and sharing with someone: a solo user
|
|
46
|
+
// pays for no listeners at all, and the window is resolved on attach
|
|
47
|
+
// rather than up front, when the container is guaranteed to exist.
|
|
48
|
+
//
|
|
49
|
+
// The mount check must come first. This runs eagerly during the editor's
|
|
50
|
+
// constructor, where `editor.collaborators` isn't assigned yet, so reading
|
|
51
|
+
// collaborators here would throw; a never-mounted editor short-circuits
|
|
52
|
+
// before that, and re-runs once mount flips the atom.
|
|
53
|
+
this.register(
|
|
54
|
+
react('presence activity listeners', () => {
|
|
55
|
+
if (this.editor.getIsMounted() && this._getHasCollaborators()) {
|
|
56
|
+
this._attachActivityListeners()
|
|
57
|
+
} else {
|
|
58
|
+
this._detachActivityListeners()
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
)
|
|
62
|
+
this.register(() => {
|
|
63
|
+
this._detachActivityListeners()
|
|
64
|
+
this._throttledActivityStamp.cancel()
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private _detachActivityListenersFn: null | (() => void) = null
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* User input anywhere in the editor's tab counts as activity for collaborator presence —
|
|
72
|
+
* presence means "this person is here", whether or not the input lands on the editor itself
|
|
73
|
+
* (e.g. typing in a sidebar next to the canvas). Listen in the capture phase so input that
|
|
74
|
+
* other handlers swallow still counts.
|
|
75
|
+
*/
|
|
76
|
+
private _attachActivityListeners() {
|
|
77
|
+
if (this._detachActivityListenersFn) return
|
|
78
|
+
|
|
79
|
+
const win = this.editor.getContainerWindow()
|
|
80
|
+
for (const name of ACTIVITY_EVENTS) {
|
|
81
|
+
win.addEventListener(name, this.markActivity, { capture: true })
|
|
82
|
+
}
|
|
83
|
+
this._detachActivityListenersFn = () => {
|
|
84
|
+
for (const name of ACTIVITY_EVENTS) {
|
|
85
|
+
win.removeEventListener(name, this.markActivity, { capture: true })
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private _detachActivityListeners() {
|
|
91
|
+
this._detachActivityListenersFn?.()
|
|
92
|
+
this._detachActivityListenersFn = null
|
|
20
93
|
}
|
|
21
94
|
|
|
22
95
|
@bind
|
|
@@ -462,6 +535,53 @@ export class InputsManager extends EditorManager {
|
|
|
462
535
|
return this.editor.getCollaborators().length > 0 // could we do this more efficiently?
|
|
463
536
|
}
|
|
464
537
|
|
|
538
|
+
private readonly _throttledActivityStamp = throttle(
|
|
539
|
+
() => {
|
|
540
|
+
const pointer = this.editor.store.unsafeGetWithoutCapture(TLPOINTER_ID)
|
|
541
|
+
if (!pointer) return
|
|
542
|
+
|
|
543
|
+
this.editor.run(
|
|
544
|
+
() => {
|
|
545
|
+
this.editor.store.put([{ ...pointer, lastActivityTimestamp: Date.now() }])
|
|
546
|
+
},
|
|
547
|
+
{ history: 'ignore' }
|
|
548
|
+
)
|
|
549
|
+
},
|
|
550
|
+
// Stay comfortably below the idle timeout so a continuously-active peer
|
|
551
|
+
// never flickers to idle between throttled stamps.
|
|
552
|
+
Math.max(
|
|
553
|
+
ACTIVITY_TIMESTAMP_THROTTLE_FLOOR_MS,
|
|
554
|
+
Math.min(ACTIVITY_TIMESTAMP_THROTTLE_MS, this.editor.options.collaboratorIdleTimeoutMs / 3)
|
|
555
|
+
),
|
|
556
|
+
{ trailing: false }
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Mark the current user as active for collaborator presence. User input anywhere in the
|
|
561
|
+
* editor's tab counts as activity automatically; call this to make input that doesn't produce
|
|
562
|
+
* DOM events count too, so peers don't classify this user as idle or inactive while they're
|
|
563
|
+
* still interacting. Calls are throttled on the leading edge, so it's safe to call from
|
|
564
|
+
* high-frequency input events.
|
|
565
|
+
*
|
|
566
|
+
* @example
|
|
567
|
+
* ```ts
|
|
568
|
+
* // Gamepads are polled rather than event-driven, so stamp activity from the poll
|
|
569
|
+
* editor.timers.setInterval(() => {
|
|
570
|
+
* const isPressed = navigator
|
|
571
|
+
* .getGamepads()
|
|
572
|
+
* .some((pad) => pad?.buttons.some((button) => button.pressed))
|
|
573
|
+
* if (isPressed) editor.inputs.markActivity()
|
|
574
|
+
* }, 50)
|
|
575
|
+
* ```
|
|
576
|
+
*/
|
|
577
|
+
@bind
|
|
578
|
+
markActivity() {
|
|
579
|
+
// Check for collaborators before consuming the throttle window, so input
|
|
580
|
+
// while alone doesn't delay the first stamp after a peer joins.
|
|
581
|
+
if (!this._getHasCollaborators()) return
|
|
582
|
+
this._throttledActivityStamp()
|
|
583
|
+
}
|
|
584
|
+
|
|
465
585
|
/**
|
|
466
586
|
* The previous point used for velocity calculation (updated each tick, not each pointer event).
|
|
467
587
|
* @internal
|
|
@@ -553,13 +673,12 @@ export class InputsManager extends EditorManager {
|
|
|
553
673
|
typeName: 'pointer',
|
|
554
674
|
x: pagePoint.x,
|
|
555
675
|
y: pagePoint.y,
|
|
676
|
+
// The activity timestamp is stamped by the container's input
|
|
677
|
+
// listeners (see `markActivity`), not by pointer position updates:
|
|
678
|
+
// synthetic moves, like the camera following another user, don't
|
|
679
|
+
// count as activity.
|
|
556
680
|
lastActivityTimestamp:
|
|
557
|
-
|
|
558
|
-
// update our last activity timestamp; otherwise, update it to the current timestamp.
|
|
559
|
-
info.type === 'pointer' && info.pointerId === INTERNAL_POINTER_IDS.CAMERA_MOVE
|
|
560
|
-
? (this.editor.store.unsafeGetWithoutCapture(TLPOINTER_ID)
|
|
561
|
-
?.lastActivityTimestamp ?? Date.now())
|
|
562
|
-
: Date.now(),
|
|
681
|
+
this.editor.store.unsafeGetWithoutCapture(TLPOINTER_ID)?.lastActivityTimestamp ?? 0,
|
|
563
682
|
meta: {},
|
|
564
683
|
},
|
|
565
684
|
])
|
|
@@ -206,12 +206,10 @@ export class BoundsSnaps {
|
|
|
206
206
|
const { editor } = this
|
|
207
207
|
return editor.store.createComputedCache<BoundsSnapPoint[], TLShape>('snapPoints', (shape) => {
|
|
208
208
|
const pageTransform = editor.getShapePageTransform(shape.id)
|
|
209
|
-
if (!pageTransform) return undefined
|
|
210
209
|
const boundsSnapGeometry = editor.getShapeUtil(shape).getBoundsSnapGeometry(shape)
|
|
211
210
|
const snapPoints =
|
|
212
211
|
boundsSnapGeometry.points ?? editor.getShapeGeometry(shape).bounds.cornersAndCenter
|
|
213
212
|
|
|
214
|
-
if (!pageTransform || !snapPoints) return undefined
|
|
215
213
|
return snapPoints.map((point, i) => {
|
|
216
214
|
const { x, y } = Mat.applyToPoint(pageTransform, point)
|
|
217
215
|
return { x, y, id: `${shape.id}:${i}` }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { atom, computed } from '@tldraw/state'
|
|
2
|
+
import { WeakCache } from '@tldraw/utils'
|
|
2
3
|
import { Geometry2d } from '../../primitives/geometry/Geometry2d'
|
|
3
4
|
import { VecLike } from '../../primitives/Vec'
|
|
4
5
|
import type { Editor } from '../Editor'
|
|
@@ -119,7 +120,7 @@ export class OverlayManager {
|
|
|
119
120
|
// while getActiveOverlayEntries() keeps returning the same overlay
|
|
120
121
|
// instances; when its reactive deps change, getOverlays() emits fresh
|
|
121
122
|
// objects and stale entries fall out by GC.
|
|
122
|
-
private _geometryCache = new
|
|
123
|
+
private _geometryCache = new WeakCache<TLOverlay, Geometry2d | null>()
|
|
123
124
|
|
|
124
125
|
/**
|
|
125
126
|
* Get hit-test geometry for an overlay, cached by overlay identity. Lets
|
|
@@ -130,12 +131,7 @@ export class OverlayManager {
|
|
|
130
131
|
* @public
|
|
131
132
|
*/
|
|
132
133
|
getOverlayGeometry(overlay: TLOverlay): Geometry2d | null {
|
|
133
|
-
|
|
134
|
-
if (cached !== undefined) return cached
|
|
135
|
-
const util = this.getOverlayUtil(overlay)
|
|
136
|
-
const geometry = util.getGeometry(overlay)
|
|
137
|
-
this._geometryCache.set(overlay, geometry)
|
|
138
|
-
return geometry
|
|
134
|
+
return this._geometryCache.get(overlay, (o) => this.getOverlayUtil(o).getGeometry(o))
|
|
139
135
|
}
|
|
140
136
|
|
|
141
137
|
/**
|
|
@@ -40,7 +40,6 @@ export function strokeShapeIndicators(
|
|
|
40
40
|
if (!shape || shape.isLocked) continue
|
|
41
41
|
|
|
42
42
|
const pageTransform = editor.getShapePageTransform(shape)
|
|
43
|
-
if (!pageTransform) continue
|
|
44
43
|
|
|
45
44
|
const indicatorPath = indicatorPathCache.get(editor, shape.id)
|
|
46
45
|
if (!indicatorPath) continue
|
|
@@ -93,7 +93,11 @@ export abstract class BaseFrameLikeShapeUtil<
|
|
|
93
93
|
const currentChildren = compact(
|
|
94
94
|
editor.getSortedChildIdsForParent(shape).map((id) => editor.getShape(id))
|
|
95
95
|
)
|
|
96
|
-
if (
|
|
96
|
+
if (
|
|
97
|
+
previousChildren.every(
|
|
98
|
+
(s) => !currentChildren.find((c) => c.index === initialIndices.get(s.id))
|
|
99
|
+
)
|
|
100
|
+
) {
|
|
97
101
|
canRestoreOriginalIndices = true
|
|
98
102
|
}
|
|
99
103
|
}
|
|
@@ -87,6 +87,9 @@ export function getPerfectDashProps(
|
|
|
87
87
|
|
|
88
88
|
dashCount = Math.floor(totalLength / dashLength / (2 * ratio))
|
|
89
89
|
dashCount -= dashCount % snap
|
|
90
|
+
// A path shorter than one dash period (or of zero length, where the count is NaN) would
|
|
91
|
+
// otherwise divide by zero below and produce an invalid dasharray; draw a single dash instead
|
|
92
|
+
if (!(dashCount >= 1)) dashCount = 1
|
|
90
93
|
|
|
91
94
|
if (dashCount < 3 && style === 'dashed') {
|
|
92
95
|
if (totalLength / strokeWidth < 4) {
|
|
@@ -75,7 +75,10 @@ export class Pointing extends StateNode {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
override onComplete() {
|
|
78
|
-
|
|
78
|
+
// `complete` arrives mid-press from undo, menus opening, page changes, and focus loss,
|
|
79
|
+
// none of which is a release; committing here would drop a default-size shape the user
|
|
80
|
+
// never asked for. Match the text and arrow tools and leave nothing behind.
|
|
81
|
+
this.cancel()
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
override onInterrupt() {
|
|
@@ -238,6 +238,70 @@ describe('StateNode.addChild', () => {
|
|
|
238
238
|
})
|
|
239
239
|
})
|
|
240
240
|
|
|
241
|
+
describe('StateNode.enter', () => {
|
|
242
|
+
let idleExits = 0
|
|
243
|
+
|
|
244
|
+
class Idle extends StateNode {
|
|
245
|
+
static override id = 'idle'
|
|
246
|
+
override onExit() {
|
|
247
|
+
idleExits++
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
class Pointing extends StateNode {
|
|
252
|
+
static override id = 'pointing'
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
class ToolWithEnterTransition extends StateNode {
|
|
256
|
+
static override id = 'enterTransition'
|
|
257
|
+
static override initial = 'idle'
|
|
258
|
+
static override children() {
|
|
259
|
+
return [Idle, Pointing]
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
override onEnter(info: { startPointing?: boolean }) {
|
|
263
|
+
if (info?.startPointing) this.transition('pointing', info)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
class OtherTool extends StateNode {
|
|
268
|
+
static override id = 'other'
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let editor: Editor
|
|
272
|
+
|
|
273
|
+
beforeEach(() => {
|
|
274
|
+
idleExits = 0
|
|
275
|
+
editor = new Editor({
|
|
276
|
+
initialState: 'other',
|
|
277
|
+
shapeUtils: [],
|
|
278
|
+
bindingUtils: [],
|
|
279
|
+
tools: [OtherTool, ToolWithEnterTransition],
|
|
280
|
+
store: createTLStore({ shapeUtils: [], bindingUtils: [] }),
|
|
281
|
+
getContainer: () => document.body,
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('enters the initial child when onEnter does not transition', () => {
|
|
286
|
+
editor.setCurrentTool('enterTransition')
|
|
287
|
+
expect(editor.root.getPath()).toBe('root.enterTransition.idle')
|
|
288
|
+
expect(editor.root.children!['enterTransition'].children!['idle'].getIsActive()).toBe(true)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('does not enter the initial child on top of a child that onEnter transitioned to', () => {
|
|
292
|
+
editor.setCurrentTool('enterTransition', { startPointing: true })
|
|
293
|
+
const tool = editor.root.children!['enterTransition']
|
|
294
|
+
expect(editor.root.getPath()).toBe('root.enterTransition.pointing')
|
|
295
|
+
expect(tool.children!['pointing'].getIsActive()).toBe(true)
|
|
296
|
+
expect(tool.children!['idle'].getIsActive()).toBe(false)
|
|
297
|
+
// The stale (never re-entered) idle child must not get a spurious onExit
|
|
298
|
+
expect(idleExits).toBe(0)
|
|
299
|
+
|
|
300
|
+
editor.setCurrentTool('other')
|
|
301
|
+
expect(tool.children!['pointing'].getIsActive()).toBe(false)
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
241
305
|
describe('current tool id mask', () => {
|
|
242
306
|
// Tool mask test classes
|
|
243
307
|
class ToolA extends StateNode {
|
|
@@ -152,7 +152,9 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
if (prevChildState?.id !== nextChildState.id) {
|
|
155
|
-
|
|
155
|
+
// The previous child is inactive when transitioning from a parent's onEnter (or from a
|
|
156
|
+
// node that was already exited); exiting it again would fire a spurious onExit
|
|
157
|
+
if (prevChildState?.getIsActive()) prevChildState.exit(info, id)
|
|
156
158
|
currState._current.set(nextChildState)
|
|
157
159
|
nextChildState.enter(info, prevChildState?.id || 'initial')
|
|
158
160
|
if (!nextChildState.getIsActive()) break
|
|
@@ -188,10 +190,18 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
|
|
|
188
190
|
this.editor.performance._notifyInteractionStart(this.id, this.getPath())
|
|
189
191
|
}
|
|
190
192
|
|
|
193
|
+
const currentBeforeEnter = this.getCurrent()
|
|
191
194
|
this._isActive.set(true)
|
|
192
195
|
this.onEnter?.(info, from)
|
|
193
196
|
|
|
194
|
-
|
|
197
|
+
// If onEnter already transitioned to a child, entering the initial child on top of it would
|
|
198
|
+
// leave that child active but orphaned
|
|
199
|
+
if (
|
|
200
|
+
this.children &&
|
|
201
|
+
this.initial &&
|
|
202
|
+
this.getIsActive() &&
|
|
203
|
+
this.getCurrent() === currentBeforeEnter
|
|
204
|
+
) {
|
|
195
205
|
const initial = this.children[this.initial]
|
|
196
206
|
this._current.set(initial)
|
|
197
207
|
initial.enter(info, from)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { tlmenus } from './menus'
|
|
2
|
+
|
|
3
|
+
describe('tlmenus', () => {
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
tlmenus.clearOpenMenus()
|
|
6
|
+
tlmenus._hiddenMenus = []
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('reports whether a menu is open with and without a context', () => {
|
|
10
|
+
tlmenus.addOpenMenu('main')
|
|
11
|
+
tlmenus.addOpenMenu('main', 'editor-1')
|
|
12
|
+
|
|
13
|
+
expect(tlmenus.isMenuOpen('main')).toBe(true)
|
|
14
|
+
expect(tlmenus.isMenuOpen('main', 'editor-1')).toBe(true)
|
|
15
|
+
expect(tlmenus.isMenuOpen('main', 'editor-2')).toBe(false)
|
|
16
|
+
expect(tlmenus.isMenuOpen('other')).toBe(false)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('hides and restores the open menus of a context', () => {
|
|
20
|
+
tlmenus.addOpenMenu('main', 'editor-1')
|
|
21
|
+
tlmenus.addOpenMenu('main', 'editor-2')
|
|
22
|
+
|
|
23
|
+
tlmenus.hideOpenMenus('editor-1')
|
|
24
|
+
expect(tlmenus.getOpenMenus()).toEqual(['main-editor-2'])
|
|
25
|
+
|
|
26
|
+
tlmenus.showOpenMenus('editor-1')
|
|
27
|
+
expect(tlmenus.getOpenMenus().sort()).toEqual(['main-editor-1', 'main-editor-2'])
|
|
28
|
+
|
|
29
|
+
// closing the real menu afterwards leaves nothing behind
|
|
30
|
+
tlmenus.deleteOpenMenu('main', 'editor-1')
|
|
31
|
+
expect(tlmenus.hasOpenMenus('editor-1')).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
})
|