@tldraw/editor 3.14.0-canary.f8af44c4d1e2 → 3.14.0-canary.f907ed7d9ee5
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 +17 -26
- package/dist-cjs/index.js +8 -10
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/config/TLSessionStateSnapshot.js +1 -12
- package/dist-cjs/lib/config/TLSessionStateSnapshot.js.map +3 -3
- package/dist-cjs/lib/editor/Editor.js +49 -72
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/bindings/BindingUtil.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/bindingsIndex.js +22 -22
- package/dist-cjs/lib/editor/derivations/bindingsIndex.js.map +2 -2
- package/dist-cjs/lib/editor/derivations/parentsToChildren.js +16 -16
- package/dist-cjs/lib/editor/derivations/parentsToChildren.js.map +2 -2
- package/dist-cjs/lib/editor/managers/{ClickManager.js → ClickManager/ClickManager.js} +1 -1
- package/dist-cjs/lib/editor/managers/ClickManager/ClickManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/{EdgeScrollManager.js → EdgeScrollManager/EdgeScrollManager.js} +2 -2
- package/dist-cjs/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/FocusManager/FocusManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/{FontManager.js → FontManager/FontManager.js} +1 -2
- package/dist-cjs/lib/editor/managers/FontManager/FontManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/{HistoryManager.js → HistoryManager/HistoryManager.js} +64 -6
- package/dist-cjs/lib/editor/managers/HistoryManager/HistoryManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/{ScribbleManager.js → ScribbleManager/ScribbleManager.js} +1 -1
- package/dist-cjs/lib/editor/managers/ScribbleManager/ScribbleManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/{TickManager.js → TickManager/TickManager.js} +1 -1
- package/dist-cjs/lib/editor/managers/TickManager/TickManager.js.map +7 -0
- package/dist-cjs/lib/editor/managers/{UserPreferencesManager.js → UserPreferencesManager/UserPreferencesManager.js} +1 -1
- package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +7 -0
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +1 -1
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js +1 -1
- package/dist-cjs/lib/editor/shapes/group/GroupShapeUtil.js.map +1 -1
- package/dist-cjs/lib/exports/getSvgJsx.js.map +1 -1
- package/dist-cjs/lib/primitives/Box.js +0 -6
- package/dist-cjs/lib/primitives/Box.js.map +2 -2
- package/dist-cjs/lib/utils/areShapesContentEqual.js +1 -1
- package/dist-cjs/lib/utils/areShapesContentEqual.js.map +2 -2
- package/dist-cjs/lib/utils/reorderShapes.js +11 -10
- package/dist-cjs/lib/utils/reorderShapes.js.map +2 -2
- package/dist-cjs/lib/utils/richText.js +7 -2
- package/dist-cjs/lib/utils/richText.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +17 -26
- package/dist-esm/index.mjs +12 -10
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/config/TLSessionStateSnapshot.mjs +1 -1
- package/dist-esm/lib/config/TLSessionStateSnapshot.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +49 -72
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/bindings/BindingUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/bindingsIndex.mjs +22 -22
- package/dist-esm/lib/editor/derivations/bindingsIndex.mjs.map +2 -2
- package/dist-esm/lib/editor/derivations/parentsToChildren.mjs +16 -16
- package/dist-esm/lib/editor/derivations/parentsToChildren.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/{ClickManager.mjs → ClickManager/ClickManager.mjs} +1 -1
- package/dist-esm/lib/editor/managers/ClickManager/ClickManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/{EdgeScrollManager.mjs → EdgeScrollManager/EdgeScrollManager.mjs} +2 -2
- package/dist-esm/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/FocusManager/FocusManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/{FontManager.mjs → FontManager/FontManager.mjs} +1 -2
- package/dist-esm/lib/editor/managers/FontManager/FontManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/{HistoryManager.mjs → HistoryManager/HistoryManager.mjs} +60 -2
- package/dist-esm/lib/editor/managers/HistoryManager/HistoryManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/{ScribbleManager.mjs → ScribbleManager/ScribbleManager.mjs} +1 -1
- package/dist-esm/lib/editor/managers/ScribbleManager/ScribbleManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/{TickManager.mjs → TickManager/TickManager.mjs} +1 -1
- package/dist-esm/lib/editor/managers/TickManager/TickManager.mjs.map +7 -0
- package/dist-esm/lib/editor/managers/{UserPreferencesManager.mjs → UserPreferencesManager/UserPreferencesManager.mjs} +1 -1
- package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +7 -0
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +1 -1
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs +1 -1
- package/dist-esm/lib/editor/shapes/group/GroupShapeUtil.mjs.map +1 -1
- package/dist-esm/lib/exports/getSvgJsx.mjs.map +1 -1
- package/dist-esm/lib/primitives/Box.mjs +0 -6
- package/dist-esm/lib/primitives/Box.mjs.map +2 -2
- package/dist-esm/lib/utils/areShapesContentEqual.mjs +1 -1
- package/dist-esm/lib/utils/areShapesContentEqual.mjs.map +2 -2
- package/dist-esm/lib/utils/reorderShapes.mjs +11 -10
- package/dist-esm/lib/utils/reorderShapes.mjs.map +2 -2
- package/dist-esm/lib/utils/richText.mjs +8 -3
- package/dist-esm/lib/utils/richText.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +8 -9
- package/src/index.ts +13 -8
- package/src/lib/config/TLSessionStateSnapshot.ts +1 -1
- package/src/lib/editor/Editor.test.ts +252 -3
- package/src/lib/editor/Editor.ts +47 -71
- package/src/lib/editor/bindings/BindingUtil.ts +6 -0
- package/src/lib/editor/derivations/bindingsIndex.ts +27 -26
- package/src/lib/editor/derivations/parentsToChildren.ts +28 -25
- package/src/lib/editor/managers/ClickManager/ClickManager.test.ts +442 -0
- package/src/lib/editor/managers/{ClickManager.ts → ClickManager/ClickManager.ts} +3 -3
- package/src/lib/editor/managers/EdgeScrollManager/EdgeScrollManager.test.ts +374 -0
- package/src/lib/editor/managers/{EdgeScrollManager.ts → EdgeScrollManager/EdgeScrollManager.ts} +3 -3
- package/src/lib/editor/managers/FocusManager/FocusManager.test.ts +455 -0
- package/src/lib/editor/managers/{FocusManager.ts → FocusManager/FocusManager.ts} +1 -1
- package/src/lib/editor/managers/FontManager/FontManager.test.ts +263 -0
- package/src/lib/editor/managers/{FontManager.ts → FontManager/FontManager.ts} +2 -3
- package/src/lib/editor/managers/{HistoryManager.test.ts → HistoryManager/HistoryManager.test.ts} +388 -1
- package/src/lib/editor/managers/{HistoryManager.ts → HistoryManager/HistoryManager.ts} +73 -2
- package/src/lib/editor/managers/ScribbleManager/ScribbleManager.test.ts +624 -0
- package/src/lib/editor/managers/{ScribbleManager.ts → ScribbleManager/ScribbleManager.ts} +2 -2
- package/src/lib/editor/managers/SnapManager/SnapManager.test.ts +485 -0
- package/src/lib/editor/managers/TextManager/TextManager.test.ts +411 -0
- package/src/lib/editor/managers/{TextManager.ts → TextManager/TextManager.ts} +1 -1
- package/src/lib/editor/managers/TickManager/TickManager.test.ts +314 -0
- package/src/lib/editor/managers/{TickManager.ts → TickManager/TickManager.ts} +2 -2
- package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +591 -0
- package/src/lib/editor/managers/{UserPreferencesManager.ts → UserPreferencesManager/UserPreferencesManager.ts} +2 -2
- package/src/lib/editor/shapes/ShapeUtil.ts +1 -1
- package/src/lib/editor/shapes/group/GroupShapeUtil.tsx +1 -1
- package/src/lib/exports/getSvgJsx.tsx +1 -1
- package/src/lib/primitives/Box.ts +0 -8
- package/src/lib/utils/areShapesContentEqual.ts +1 -2
- package/src/lib/utils/reorderShapes.ts +10 -13
- package/src/lib/utils/richText.ts +10 -4
- package/src/version.ts +3 -3
- package/dist-cjs/lib/editor/managers/ClickManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/EdgeScrollManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/FocusManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/FontManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/HistoryManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/ScribbleManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/Stack.js +0 -82
- package/dist-cjs/lib/editor/managers/Stack.js.map +0 -7
- package/dist-cjs/lib/editor/managers/TextManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/TickManager.js.map +0 -7
- package/dist-cjs/lib/editor/managers/UserPreferencesManager.js.map +0 -7
- package/dist-esm/lib/editor/managers/ClickManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/EdgeScrollManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/FocusManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/FontManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/HistoryManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/ScribbleManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/Stack.mjs +0 -62
- package/dist-esm/lib/editor/managers/Stack.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/TextManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/TickManager.mjs.map +0 -7
- package/dist-esm/lib/editor/managers/UserPreferencesManager.mjs.map +0 -7
- package/src/lib/editor/managers/ScribbleManager.test.ts +0 -32
- package/src/lib/editor/managers/Stack.ts +0 -71
- /package/dist-cjs/lib/editor/managers/{FocusManager.js → FocusManager/FocusManager.js} +0 -0
- /package/dist-cjs/lib/editor/managers/{TextManager.js → TextManager/TextManager.js} +0 -0
- /package/dist-esm/lib/editor/managers/{FocusManager.mjs → FocusManager/FocusManager.mjs} +0 -0
- /package/dist-esm/lib/editor/managers/{TextManager.mjs → TextManager/TextManager.mjs} +0 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { Editor } from '../../Editor'
|
|
2
|
+
import { TLClickEventInfo, TLPointerEventInfo } from '../../types/event-types'
|
|
3
|
+
import { ClickManager } from './ClickManager'
|
|
4
|
+
|
|
5
|
+
// Mock the Editor class
|
|
6
|
+
jest.mock('../../Editor')
|
|
7
|
+
|
|
8
|
+
describe('ClickManager', () => {
|
|
9
|
+
let editor: jest.Mocked<Editor>
|
|
10
|
+
let clickManager: ClickManager
|
|
11
|
+
let mockTimers: any
|
|
12
|
+
|
|
13
|
+
const createPointerEvent = (
|
|
14
|
+
name: 'pointer_down' | 'pointer_up' | 'pointer_move',
|
|
15
|
+
point: { x: number; y: number } = { x: 0, y: 0 }
|
|
16
|
+
): TLPointerEventInfo => ({
|
|
17
|
+
type: 'pointer',
|
|
18
|
+
name,
|
|
19
|
+
point,
|
|
20
|
+
pointerId: 1,
|
|
21
|
+
button: 0,
|
|
22
|
+
isPen: false,
|
|
23
|
+
target: 'canvas',
|
|
24
|
+
shiftKey: false,
|
|
25
|
+
altKey: false,
|
|
26
|
+
ctrlKey: false,
|
|
27
|
+
metaKey: false,
|
|
28
|
+
accelKey: false,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
jest.useFakeTimers()
|
|
33
|
+
mockTimers = {
|
|
34
|
+
setTimeout: jest.fn((fn, delay) => setTimeout(fn, delay)),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
editor = {
|
|
38
|
+
timers: mockTimers,
|
|
39
|
+
dispatch: jest.fn(),
|
|
40
|
+
options: {
|
|
41
|
+
doubleClickDurationMs: 300,
|
|
42
|
+
multiClickDurationMs: 300,
|
|
43
|
+
dragDistanceSquared: 16,
|
|
44
|
+
coarseDragDistanceSquared: 36,
|
|
45
|
+
},
|
|
46
|
+
inputs: {
|
|
47
|
+
currentScreenPoint: { x: 0, y: 0 },
|
|
48
|
+
},
|
|
49
|
+
getInstanceState: jest.fn(() => ({
|
|
50
|
+
isCoarsePointer: false,
|
|
51
|
+
})),
|
|
52
|
+
} as any
|
|
53
|
+
|
|
54
|
+
clickManager = new ClickManager(editor)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
jest.useRealTimers()
|
|
59
|
+
jest.clearAllMocks()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('constructor and initial state', () => {
|
|
63
|
+
it('should initialize with idle state', () => {
|
|
64
|
+
expect(clickManager.clickState).toBe('idle')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should store reference to editor', () => {
|
|
68
|
+
expect(clickManager.editor).toBe(editor)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should initialize lastPointerInfo as empty object', () => {
|
|
72
|
+
expect(clickManager.lastPointerInfo).toEqual({})
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('single click behavior', () => {
|
|
77
|
+
it('should handle pointer_down in idle state', () => {
|
|
78
|
+
const pointerEvent = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
79
|
+
|
|
80
|
+
const result = clickManager.handlePointerEvent(pointerEvent)
|
|
81
|
+
|
|
82
|
+
expect(result).toBe(pointerEvent)
|
|
83
|
+
expect(clickManager.clickState).toBe('pendingDouble')
|
|
84
|
+
expect(clickManager.lastPointerInfo).toBe(pointerEvent)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should handle pointer_up without generating click events in pending state', () => {
|
|
88
|
+
const downEvent = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
89
|
+
const upEvent = createPointerEvent('pointer_up', { x: 100, y: 100 })
|
|
90
|
+
|
|
91
|
+
clickManager.handlePointerEvent(downEvent)
|
|
92
|
+
clickManager.handlePointerEvent(upEvent)
|
|
93
|
+
|
|
94
|
+
expect(clickManager.clickState).toBe('pendingDouble')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should return to idle state after timeout in pendingDouble', () => {
|
|
98
|
+
const pointerEvent = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
99
|
+
|
|
100
|
+
clickManager.handlePointerEvent(pointerEvent)
|
|
101
|
+
expect(clickManager.clickState).toBe('pendingDouble')
|
|
102
|
+
|
|
103
|
+
jest.advanceTimersByTime(350)
|
|
104
|
+
|
|
105
|
+
expect(clickManager.clickState).toBe('idle')
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('double click detection', () => {
|
|
110
|
+
it('should detect double click on second pointer_down', () => {
|
|
111
|
+
const firstDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
112
|
+
const secondDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
113
|
+
|
|
114
|
+
clickManager.handlePointerEvent(firstDown)
|
|
115
|
+
const result = clickManager.handlePointerEvent(secondDown) as TLClickEventInfo
|
|
116
|
+
|
|
117
|
+
expect(result.type).toBe('click')
|
|
118
|
+
expect(result.name).toBe('double_click')
|
|
119
|
+
expect(result.phase).toBe('down')
|
|
120
|
+
expect(clickManager.clickState).toBe('pendingTriple')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should generate double_click up event on pointer_up after double_click down', () => {
|
|
124
|
+
const firstDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
125
|
+
const secondDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
126
|
+
const secondUp = createPointerEvent('pointer_up', { x: 100, y: 100 })
|
|
127
|
+
|
|
128
|
+
clickManager.handlePointerEvent(firstDown)
|
|
129
|
+
clickManager.handlePointerEvent(secondDown)
|
|
130
|
+
const result = clickManager.handlePointerEvent(secondUp) as TLClickEventInfo
|
|
131
|
+
|
|
132
|
+
expect(result.type).toBe('click')
|
|
133
|
+
expect(result.name).toBe('double_click')
|
|
134
|
+
expect(result.phase).toBe('up')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should dispatch double_click settle event after timeout in pendingTriple', () => {
|
|
138
|
+
const firstDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
139
|
+
const secondDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
140
|
+
|
|
141
|
+
clickManager.handlePointerEvent(firstDown)
|
|
142
|
+
clickManager.handlePointerEvent(secondDown)
|
|
143
|
+
|
|
144
|
+
jest.advanceTimersByTime(350)
|
|
145
|
+
|
|
146
|
+
expect(editor.dispatch).toHaveBeenCalledWith(
|
|
147
|
+
expect.objectContaining({
|
|
148
|
+
type: 'click',
|
|
149
|
+
name: 'double_click',
|
|
150
|
+
phase: 'settle',
|
|
151
|
+
})
|
|
152
|
+
)
|
|
153
|
+
expect(clickManager.clickState).toBe('idle')
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
describe('triple and quadruple click detection', () => {
|
|
158
|
+
it('should detect triple click on third pointer_down', () => {
|
|
159
|
+
const firstDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
160
|
+
const secondDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
161
|
+
const thirdDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
162
|
+
|
|
163
|
+
clickManager.handlePointerEvent(firstDown)
|
|
164
|
+
clickManager.handlePointerEvent(secondDown)
|
|
165
|
+
const result = clickManager.handlePointerEvent(thirdDown) as TLClickEventInfo
|
|
166
|
+
|
|
167
|
+
expect(result.type).toBe('click')
|
|
168
|
+
expect(result.name).toBe('triple_click')
|
|
169
|
+
expect(result.phase).toBe('down')
|
|
170
|
+
expect(clickManager.clickState).toBe('pendingQuadruple')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('should detect quadruple click on fourth pointer_down', () => {
|
|
174
|
+
const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
175
|
+
|
|
176
|
+
clickManager.handlePointerEvent(pointerDown) // first
|
|
177
|
+
clickManager.handlePointerEvent(pointerDown) // second (double_click)
|
|
178
|
+
clickManager.handlePointerEvent(pointerDown) // third (triple_click)
|
|
179
|
+
const result = clickManager.handlePointerEvent(pointerDown) as TLClickEventInfo // fourth
|
|
180
|
+
|
|
181
|
+
expect(result.type).toBe('click')
|
|
182
|
+
expect(result.name).toBe('quadruple_click')
|
|
183
|
+
expect(result.phase).toBe('down')
|
|
184
|
+
expect(clickManager.clickState).toBe('pendingOverflow')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should handle overflow state after quadruple click', () => {
|
|
188
|
+
const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
189
|
+
|
|
190
|
+
clickManager.handlePointerEvent(pointerDown) // first
|
|
191
|
+
clickManager.handlePointerEvent(pointerDown) // second
|
|
192
|
+
clickManager.handlePointerEvent(pointerDown) // third
|
|
193
|
+
clickManager.handlePointerEvent(pointerDown) // fourth
|
|
194
|
+
const result = clickManager.handlePointerEvent(pointerDown) // fifth
|
|
195
|
+
|
|
196
|
+
expect(result).toBe(pointerDown)
|
|
197
|
+
expect(clickManager.clickState).toBe('overflow')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('should generate triple_click up event on pointer_up after triple_click down', () => {
|
|
201
|
+
const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
202
|
+
const pointerUp = createPointerEvent('pointer_up', { x: 100, y: 100 })
|
|
203
|
+
|
|
204
|
+
clickManager.handlePointerEvent(pointerDown) // first
|
|
205
|
+
clickManager.handlePointerEvent(pointerDown) // second
|
|
206
|
+
clickManager.handlePointerEvent(pointerDown) // third
|
|
207
|
+
const result = clickManager.handlePointerEvent(pointerUp) as TLClickEventInfo
|
|
208
|
+
|
|
209
|
+
expect(result.type).toBe('click')
|
|
210
|
+
expect(result.name).toBe('triple_click')
|
|
211
|
+
expect(result.phase).toBe('up')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('should generate quadruple_click up event on pointer_up after quadruple_click down', () => {
|
|
215
|
+
const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
216
|
+
const pointerUp = createPointerEvent('pointer_up', { x: 100, y: 100 })
|
|
217
|
+
|
|
218
|
+
clickManager.handlePointerEvent(pointerDown) // first
|
|
219
|
+
clickManager.handlePointerEvent(pointerDown) // second
|
|
220
|
+
clickManager.handlePointerEvent(pointerDown) // third
|
|
221
|
+
clickManager.handlePointerEvent(pointerDown) // fourth
|
|
222
|
+
const result = clickManager.handlePointerEvent(pointerUp) as TLClickEventInfo
|
|
223
|
+
|
|
224
|
+
expect(result.type).toBe('click')
|
|
225
|
+
expect(result.name).toBe('quadruple_click')
|
|
226
|
+
expect(result.phase).toBe('up')
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
describe('timeout behavior and settle events', () => {
|
|
231
|
+
it('should dispatch triple_click settle event after timeout in pendingQuadruple', () => {
|
|
232
|
+
const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
233
|
+
|
|
234
|
+
clickManager.handlePointerEvent(pointerDown) // first
|
|
235
|
+
clickManager.handlePointerEvent(pointerDown) // second
|
|
236
|
+
clickManager.handlePointerEvent(pointerDown) // third
|
|
237
|
+
|
|
238
|
+
jest.advanceTimersByTime(350)
|
|
239
|
+
|
|
240
|
+
expect(editor.dispatch).toHaveBeenCalledWith(
|
|
241
|
+
expect.objectContaining({
|
|
242
|
+
type: 'click',
|
|
243
|
+
name: 'triple_click',
|
|
244
|
+
phase: 'settle',
|
|
245
|
+
})
|
|
246
|
+
)
|
|
247
|
+
expect(clickManager.clickState).toBe('idle')
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('should dispatch quadruple_click settle event after timeout in pendingOverflow', () => {
|
|
251
|
+
const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
252
|
+
|
|
253
|
+
clickManager.handlePointerEvent(pointerDown) // first
|
|
254
|
+
clickManager.handlePointerEvent(pointerDown) // second
|
|
255
|
+
clickManager.handlePointerEvent(pointerDown) // third
|
|
256
|
+
clickManager.handlePointerEvent(pointerDown) // fourth
|
|
257
|
+
|
|
258
|
+
jest.advanceTimersByTime(350)
|
|
259
|
+
|
|
260
|
+
expect(editor.dispatch).toHaveBeenCalledWith(
|
|
261
|
+
expect.objectContaining({
|
|
262
|
+
type: 'click',
|
|
263
|
+
name: 'quadruple_click',
|
|
264
|
+
phase: 'settle',
|
|
265
|
+
})
|
|
266
|
+
)
|
|
267
|
+
expect(clickManager.clickState).toBe('idle')
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('should use different timeout durations for different states', () => {
|
|
271
|
+
const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
272
|
+
|
|
273
|
+
// First click - should use doubleClickDurationMs
|
|
274
|
+
clickManager.handlePointerEvent(pointerDown)
|
|
275
|
+
expect(mockTimers.setTimeout).toHaveBeenCalledWith(
|
|
276
|
+
expect.any(Function),
|
|
277
|
+
editor.options.doubleClickDurationMs
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
jest.clearAllMocks()
|
|
281
|
+
|
|
282
|
+
// Second click - should use multiClickDurationMs
|
|
283
|
+
clickManager.handlePointerEvent(pointerDown)
|
|
284
|
+
expect(mockTimers.setTimeout).toHaveBeenCalledWith(
|
|
285
|
+
expect.any(Function),
|
|
286
|
+
editor.options.multiClickDurationMs
|
|
287
|
+
)
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
describe('distance-based click cancellation', () => {
|
|
292
|
+
it('should reset to idle if clicks are too far apart', () => {
|
|
293
|
+
const firstDown = createPointerEvent('pointer_down', { x: 0, y: 0 })
|
|
294
|
+
const secondDown = createPointerEvent('pointer_down', { x: 50, y: 50 }) // > 40px distance
|
|
295
|
+
|
|
296
|
+
clickManager.handlePointerEvent(firstDown)
|
|
297
|
+
expect(clickManager.clickState).toBe('pendingDouble')
|
|
298
|
+
|
|
299
|
+
const result = clickManager.handlePointerEvent(secondDown)
|
|
300
|
+
|
|
301
|
+
expect(result).toBe(secondDown)
|
|
302
|
+
expect(clickManager.clickState).toBe('pendingDouble') // Reset and started new sequence
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('should continue sequence if clicks are close enough', () => {
|
|
306
|
+
const firstDown = createPointerEvent('pointer_down', { x: 0, y: 0 })
|
|
307
|
+
const secondDown = createPointerEvent('pointer_down', { x: 5, y: 5 }) // < 40px distance
|
|
308
|
+
|
|
309
|
+
clickManager.handlePointerEvent(firstDown)
|
|
310
|
+
const result = clickManager.handlePointerEvent(secondDown) as TLClickEventInfo
|
|
311
|
+
|
|
312
|
+
expect(result.type).toBe('click')
|
|
313
|
+
expect(result.name).toBe('double_click')
|
|
314
|
+
expect(clickManager.clickState).toBe('pendingTriple')
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
describe('pointer move cancellation behavior', () => {
|
|
319
|
+
it('should cancel click sequence on significant pointer move', () => {
|
|
320
|
+
const downEvent = createPointerEvent('pointer_down', { x: 0, y: 0 })
|
|
321
|
+
const moveEvent = createPointerEvent('pointer_move', { x: 10, y: 10 })
|
|
322
|
+
|
|
323
|
+
editor.inputs.currentScreenPoint.x = 10
|
|
324
|
+
editor.inputs.currentScreenPoint.y = 10
|
|
325
|
+
|
|
326
|
+
clickManager.handlePointerEvent(downEvent)
|
|
327
|
+
expect(clickManager.clickState).toBe('pendingDouble')
|
|
328
|
+
|
|
329
|
+
const result = clickManager.handlePointerEvent(moveEvent)
|
|
330
|
+
|
|
331
|
+
expect(result).toBe(moveEvent)
|
|
332
|
+
expect(clickManager.clickState).toBe('idle')
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('should use coarse drag distance for coarse pointers', () => {
|
|
336
|
+
editor.getInstanceState.mockReturnValue({
|
|
337
|
+
...editor.getInstanceState(),
|
|
338
|
+
isCoarsePointer: true,
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
const downEvent = createPointerEvent('pointer_down', { x: 0, y: 0 })
|
|
342
|
+
const moveEvent1 = createPointerEvent('pointer_move', { x: 1, y: 1 })
|
|
343
|
+
const moveEvent2 = createPointerEvent('pointer_move', { x: 5, y: 5 }) // 50
|
|
344
|
+
|
|
345
|
+
clickManager.handlePointerEvent(downEvent)
|
|
346
|
+
expect(clickManager.clickState).toBe('pendingDouble')
|
|
347
|
+
|
|
348
|
+
// Should not cancel for coarse pointer with small movement
|
|
349
|
+
editor.inputs.currentScreenPoint.x = 1
|
|
350
|
+
editor.inputs.currentScreenPoint.y = 1
|
|
351
|
+
clickManager.handlePointerEvent(moveEvent1)
|
|
352
|
+
expect(clickManager.clickState).toBe('pendingDouble')
|
|
353
|
+
|
|
354
|
+
editor.inputs.currentScreenPoint.x = 5
|
|
355
|
+
editor.inputs.currentScreenPoint.y = 5
|
|
356
|
+
clickManager.handlePointerEvent(moveEvent2)
|
|
357
|
+
|
|
358
|
+
expect(clickManager.clickState).toBe('idle')
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it('should not cancel in idle state', () => {
|
|
362
|
+
const moveEvent = createPointerEvent('pointer_move', { x: 100, y: 100 })
|
|
363
|
+
|
|
364
|
+
editor.inputs.currentScreenPoint.x = 100
|
|
365
|
+
editor.inputs.currentScreenPoint.y = 100
|
|
366
|
+
|
|
367
|
+
clickManager.handlePointerEvent(moveEvent)
|
|
368
|
+
|
|
369
|
+
expect(clickManager.clickState).toBe('idle')
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
describe('cancelDoubleClickTimeout method', () => {
|
|
374
|
+
it('should clear timeout and reset state to idle', () => {
|
|
375
|
+
const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
376
|
+
|
|
377
|
+
clickManager.handlePointerEvent(pointerDown)
|
|
378
|
+
expect(clickManager.clickState).toBe('pendingDouble')
|
|
379
|
+
|
|
380
|
+
clickManager.cancelDoubleClickTimeout()
|
|
381
|
+
|
|
382
|
+
expect(clickManager.clickState).toBe('idle')
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('should prevent timeout callback from executing after cancellation', () => {
|
|
386
|
+
const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
387
|
+
|
|
388
|
+
clickManager.handlePointerEvent(pointerDown)
|
|
389
|
+
clickManager.handlePointerEvent(pointerDown) // double click
|
|
390
|
+
expect(clickManager.clickState).toBe('pendingTriple')
|
|
391
|
+
|
|
392
|
+
clickManager.cancelDoubleClickTimeout()
|
|
393
|
+
|
|
394
|
+
// Advance time - should not dispatch settle event
|
|
395
|
+
jest.advanceTimersByTime(350)
|
|
396
|
+
|
|
397
|
+
expect(editor.dispatch).not.toHaveBeenCalled()
|
|
398
|
+
expect(clickManager.clickState).toBe('idle')
|
|
399
|
+
})
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
describe('edge cases', () => {
|
|
403
|
+
it('should handle null click state gracefully', () => {
|
|
404
|
+
// Force null state
|
|
405
|
+
;(clickManager as any)._clickState = null
|
|
406
|
+
|
|
407
|
+
const pointerEvent = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
408
|
+
const result = clickManager.handlePointerEvent(pointerEvent)
|
|
409
|
+
|
|
410
|
+
expect(result).toBe(pointerEvent)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
it('should handle missing previous screen point', () => {
|
|
414
|
+
const firstDown = createPointerEvent('pointer_down', { x: 0, y: 0 })
|
|
415
|
+
|
|
416
|
+
// Clear previous point
|
|
417
|
+
;(clickManager as any)._previousScreenPoint = undefined
|
|
418
|
+
|
|
419
|
+
const result = clickManager.handlePointerEvent(firstDown)
|
|
420
|
+
|
|
421
|
+
expect(result).toBe(firstDown)
|
|
422
|
+
expect(clickManager.clickState).toBe('pendingDouble')
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('should handle overflow state correctly', () => {
|
|
426
|
+
const pointerDown = createPointerEvent('pointer_down', { x: 100, y: 100 })
|
|
427
|
+
const pointerUp = createPointerEvent('pointer_up', { x: 100, y: 100 })
|
|
428
|
+
|
|
429
|
+
// Get to overflow state
|
|
430
|
+
clickManager.handlePointerEvent(pointerDown) // 1
|
|
431
|
+
clickManager.handlePointerEvent(pointerDown) // 2
|
|
432
|
+
clickManager.handlePointerEvent(pointerDown) // 3
|
|
433
|
+
clickManager.handlePointerEvent(pointerDown) // 4
|
|
434
|
+
clickManager.handlePointerEvent(pointerDown) // 5 -> overflow
|
|
435
|
+
|
|
436
|
+
expect(clickManager.clickState).toBe('overflow')
|
|
437
|
+
|
|
438
|
+
// pointer_up in overflow should just return the event
|
|
439
|
+
clickManager.handlePointerEvent(pointerUp)
|
|
440
|
+
})
|
|
441
|
+
})
|
|
442
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { bind, uniqueId } from '@tldraw/utils'
|
|
2
|
-
import { Vec } from '
|
|
3
|
-
import type { Editor } from '
|
|
4
|
-
import { TLClickEventInfo, TLPointerEventInfo } from '
|
|
2
|
+
import { Vec } from '../../../primitives/Vec'
|
|
3
|
+
import type { Editor } from '../../Editor'
|
|
4
|
+
import { TLClickEventInfo, TLPointerEventInfo } from '../../types/event-types'
|
|
5
5
|
|
|
6
6
|
/** @public */
|
|
7
7
|
export type TLClickState =
|