@neo4j-nvl/interaction-handlers 1.2.2 → 2.0.0
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/CHANGELOG.md +31 -1
- package/README.md +4 -4
- package/lib/__tests__/click-interaction.test.js +450 -264
- package/lib/__tests__/hover-interaction.test.js +83 -15
- package/lib/__tests__/zoom-interaction.test.js +20 -46
- package/lib/interaction-handlers/box-select-interaction.d.ts +1 -1
- package/lib/interaction-handlers/click-interaction.d.ts +46 -16
- package/lib/interaction-handlers/click-interaction.js +55 -34
- package/lib/interaction-handlers/hover-interaction.d.ts +5 -5
- package/lib/interaction-handlers/lasso-interaction.d.ts +1 -1
- package/lib/interaction-handlers/pan-interaction.d.ts +1 -1
- package/lib/interaction-handlers/zoom-interaction.d.ts +1 -8
- package/lib/interaction-handlers/zoom-interaction.js +0 -1
- package/package.json +2 -2
|
@@ -19,6 +19,7 @@ describe('HoverInteraction', () => {
|
|
|
19
19
|
hoverInteraction.destroy();
|
|
20
20
|
myNVL.destroy();
|
|
21
21
|
callbackMock.mockReset();
|
|
22
|
+
jest.restoreAllMocks();
|
|
22
23
|
});
|
|
23
24
|
describe('handleHover', () => {
|
|
24
25
|
it('should register hover event on canvas', () => {
|
|
@@ -27,7 +28,7 @@ describe('HoverInteraction', () => {
|
|
|
27
28
|
clientY: 10
|
|
28
29
|
});
|
|
29
30
|
hoverInteraction.handleHover(mouseEvent);
|
|
30
|
-
expect(callbackMock).toHaveBeenCalledWith(undefined, { nodes: [], relationships: [] }, mouseEvent);
|
|
31
|
+
expect(callbackMock).toHaveBeenCalledWith(undefined, { nodes: [], relationships: [], clusters: [] }, mouseEvent);
|
|
31
32
|
expect(callbackMock).toHaveBeenCalledTimes(1);
|
|
32
33
|
});
|
|
33
34
|
it('should register hover event for node', () => {
|
|
@@ -55,7 +56,8 @@ describe('HoverInteraction', () => {
|
|
|
55
56
|
toTargetCoordinates: { x: 200, y: 200 },
|
|
56
57
|
pointerCoordinates: { x: 100, y: 100 }
|
|
57
58
|
}
|
|
58
|
-
]
|
|
59
|
+
],
|
|
60
|
+
clusters: []
|
|
59
61
|
}, mouseEvent);
|
|
60
62
|
expect(callbackMock).toHaveBeenCalledTimes(1);
|
|
61
63
|
});
|
|
@@ -75,7 +77,8 @@ describe('HoverInteraction', () => {
|
|
|
75
77
|
toTargetCoordinates: { x: 200, y: 200 },
|
|
76
78
|
pointerCoordinates: { x: 150, y: 150 }
|
|
77
79
|
}
|
|
78
|
-
]
|
|
80
|
+
],
|
|
81
|
+
clusters: []
|
|
79
82
|
}, mouseEvent);
|
|
80
83
|
expect(callbackMock).toHaveBeenCalledTimes(1);
|
|
81
84
|
});
|
|
@@ -104,7 +107,8 @@ describe('HoverInteraction', () => {
|
|
|
104
107
|
toTargetCoordinates: { x: 200, y: 200 },
|
|
105
108
|
pointerCoordinates: { x: 100, y: 100 }
|
|
106
109
|
}
|
|
107
|
-
]
|
|
110
|
+
],
|
|
111
|
+
clusters: []
|
|
108
112
|
}, mouseEvent);
|
|
109
113
|
expect(callbackMock).toHaveBeenCalledTimes(1);
|
|
110
114
|
const mouseEvent2 = new MouseEvent('mousemove', {
|
|
@@ -131,9 +135,10 @@ describe('HoverInteraction', () => {
|
|
|
131
135
|
toTargetCoordinates: { x: 200, y: 200 },
|
|
132
136
|
pointerCoordinates: { x: 100, y: 100 }
|
|
133
137
|
}
|
|
134
|
-
]
|
|
138
|
+
],
|
|
139
|
+
clusters: []
|
|
135
140
|
}, mouseEvent2);
|
|
136
|
-
expect(callbackMock).toHaveBeenCalledWith(undefined, { nodes: [], relationships: [] }, mouseEvent2);
|
|
141
|
+
expect(callbackMock).toHaveBeenCalledWith(undefined, { nodes: [], relationships: [], clusters: [] }, mouseEvent2);
|
|
137
142
|
expect(callbackMock).toHaveBeenCalledTimes(2);
|
|
138
143
|
});
|
|
139
144
|
it('should register un-hover event for relationship', () => {
|
|
@@ -152,7 +157,8 @@ describe('HoverInteraction', () => {
|
|
|
152
157
|
toTargetCoordinates: { x: 200, y: 200 },
|
|
153
158
|
pointerCoordinates: { x: 150, y: 150 }
|
|
154
159
|
}
|
|
155
|
-
]
|
|
160
|
+
],
|
|
161
|
+
clusters: []
|
|
156
162
|
}, mouseEvent);
|
|
157
163
|
expect(callbackMock).toHaveBeenCalledTimes(1);
|
|
158
164
|
const mouseEvent2 = new MouseEvent('mousemove', {
|
|
@@ -170,9 +176,10 @@ describe('HoverInteraction', () => {
|
|
|
170
176
|
toTargetCoordinates: { x: 200, y: 200 },
|
|
171
177
|
pointerCoordinates: { x: 150, y: 150 }
|
|
172
178
|
}
|
|
173
|
-
]
|
|
179
|
+
],
|
|
180
|
+
clusters: []
|
|
174
181
|
}, mouseEvent2);
|
|
175
|
-
expect(callbackMock).toHaveBeenCalledWith(undefined, { nodes: [], relationships: [] }, mouseEvent2);
|
|
182
|
+
expect(callbackMock).toHaveBeenCalledWith(undefined, { nodes: [], relationships: [], clusters: [] }, mouseEvent2);
|
|
176
183
|
expect(callbackMock).toHaveBeenCalledTimes(2);
|
|
177
184
|
});
|
|
178
185
|
it('should un-hover node and hover relationships when moving mouse from node to relationship', () => {
|
|
@@ -200,7 +207,8 @@ describe('HoverInteraction', () => {
|
|
|
200
207
|
toTargetCoordinates: { x: 200, y: 200 },
|
|
201
208
|
pointerCoordinates: { x: 100, y: 100 }
|
|
202
209
|
}
|
|
203
|
-
]
|
|
210
|
+
],
|
|
211
|
+
clusters: []
|
|
204
212
|
}, mouseEvent);
|
|
205
213
|
expect(callbackMock).toHaveBeenCalledTimes(1);
|
|
206
214
|
const mouseEvent2 = new MouseEvent('mousemove', {
|
|
@@ -227,7 +235,8 @@ describe('HoverInteraction', () => {
|
|
|
227
235
|
toTargetCoordinates: { x: 200, y: 200 },
|
|
228
236
|
pointerCoordinates: { x: 100, y: 100 }
|
|
229
237
|
}
|
|
230
|
-
]
|
|
238
|
+
],
|
|
239
|
+
clusters: []
|
|
231
240
|
}, mouseEvent2);
|
|
232
241
|
expect(callbackMock).toHaveBeenCalledWith({ id: '10', from: '0', to: '1', hovered: true }, {
|
|
233
242
|
nodes: [],
|
|
@@ -239,7 +248,8 @@ describe('HoverInteraction', () => {
|
|
|
239
248
|
toTargetCoordinates: { x: 200, y: 200 },
|
|
240
249
|
pointerCoordinates: { x: 150, y: 150 }
|
|
241
250
|
}
|
|
242
|
-
]
|
|
251
|
+
],
|
|
252
|
+
clusters: []
|
|
243
253
|
}, mouseEvent2);
|
|
244
254
|
expect(callbackMock).toHaveBeenCalledTimes(2);
|
|
245
255
|
});
|
|
@@ -259,7 +269,8 @@ describe('HoverInteraction', () => {
|
|
|
259
269
|
toTargetCoordinates: { x: 200, y: 200 },
|
|
260
270
|
pointerCoordinates: { x: 150, y: 150 }
|
|
261
271
|
}
|
|
262
|
-
]
|
|
272
|
+
],
|
|
273
|
+
clusters: []
|
|
263
274
|
}, mouseEvent);
|
|
264
275
|
expect(callbackMock).toHaveBeenCalledTimes(1);
|
|
265
276
|
const mouseEvent2 = new MouseEvent('mousemove', {
|
|
@@ -286,7 +297,8 @@ describe('HoverInteraction', () => {
|
|
|
286
297
|
toTargetCoordinates: { x: 200, y: 200 },
|
|
287
298
|
pointerCoordinates: { x: 100, y: 100 }
|
|
288
299
|
}
|
|
289
|
-
]
|
|
300
|
+
],
|
|
301
|
+
clusters: []
|
|
290
302
|
}, mouseEvent2);
|
|
291
303
|
expect(callbackMock).toHaveBeenCalledWith({ id: '10', from: '0', to: '1', hovered: false }, {
|
|
292
304
|
nodes: [],
|
|
@@ -298,9 +310,65 @@ describe('HoverInteraction', () => {
|
|
|
298
310
|
toTargetCoordinates: { x: 200, y: 200 },
|
|
299
311
|
pointerCoordinates: { x: 150, y: 150 }
|
|
300
312
|
}
|
|
301
|
-
]
|
|
313
|
+
],
|
|
314
|
+
clusters: []
|
|
302
315
|
}, mouseEvent2);
|
|
303
316
|
expect(callbackMock).toHaveBeenCalledTimes(2);
|
|
304
317
|
});
|
|
318
|
+
it('should report a hovered cluster', () => {
|
|
319
|
+
const nvlTargets = {
|
|
320
|
+
nodes: [],
|
|
321
|
+
relationships: [],
|
|
322
|
+
clusters: [
|
|
323
|
+
{
|
|
324
|
+
data: { id: 'group-a', nodeIds: ['0', '1'] },
|
|
325
|
+
pointerCoordinates: { x: 50, y: 50 },
|
|
326
|
+
hull: [
|
|
327
|
+
{ x: 0, y: 0 },
|
|
328
|
+
{ x: 100, y: 0 },
|
|
329
|
+
{ x: 100, y: 100 },
|
|
330
|
+
{ x: 0, y: 100 }
|
|
331
|
+
]
|
|
332
|
+
}
|
|
333
|
+
]
|
|
334
|
+
};
|
|
335
|
+
jest.spyOn(myNVL, 'getHits').mockImplementation((event) => Object.assign(event, { nvlTargets }));
|
|
336
|
+
const mouseEvent = new MouseEvent('mousemove', { clientX: 50, clientY: 50 });
|
|
337
|
+
hoverInteraction.handleHover(mouseEvent);
|
|
338
|
+
expect(callbackMock).toHaveBeenCalledWith(undefined, nvlTargets, mouseEvent);
|
|
339
|
+
expect(callbackMock).toHaveBeenCalledTimes(1);
|
|
340
|
+
});
|
|
341
|
+
it('should keep the cluster in the hit elements when hovering one of its nodes', () => {
|
|
342
|
+
const nvlTargets = {
|
|
343
|
+
nodes: [
|
|
344
|
+
{
|
|
345
|
+
data: { id: '0', x: 100, y: 100 },
|
|
346
|
+
distance: 0,
|
|
347
|
+
targetCoordinates: { x: 100, y: 100 },
|
|
348
|
+
pointerCoordinates: { x: 100, y: 100 },
|
|
349
|
+
distanceVector: { x: 0, y: 0 },
|
|
350
|
+
insideNode: true
|
|
351
|
+
}
|
|
352
|
+
],
|
|
353
|
+
relationships: [],
|
|
354
|
+
clusters: [
|
|
355
|
+
{
|
|
356
|
+
data: { id: 'group-a', nodeIds: ['0', '1'] },
|
|
357
|
+
pointerCoordinates: { x: 100, y: 100 },
|
|
358
|
+
hull: [
|
|
359
|
+
{ x: 0, y: 0 },
|
|
360
|
+
{ x: 200, y: 0 },
|
|
361
|
+
{ x: 200, y: 200 },
|
|
362
|
+
{ x: 0, y: 200 }
|
|
363
|
+
]
|
|
364
|
+
}
|
|
365
|
+
]
|
|
366
|
+
};
|
|
367
|
+
jest.spyOn(myNVL, 'getHits').mockImplementation((event) => Object.assign(event, { nvlTargets }));
|
|
368
|
+
const mouseEvent = new MouseEvent('mousemove', { clientX: 100, clientY: 100 });
|
|
369
|
+
hoverInteraction.handleHover(mouseEvent);
|
|
370
|
+
expect(callbackMock).toHaveBeenCalledWith({ id: '0', x: 100, y: 100 }, nvlTargets, mouseEvent);
|
|
371
|
+
expect(callbackMock).toHaveBeenCalledTimes(1);
|
|
372
|
+
});
|
|
305
373
|
});
|
|
306
374
|
});
|
|
@@ -6,7 +6,6 @@ describe('ZoomInteraction', () => {
|
|
|
6
6
|
let zoomInteraction;
|
|
7
7
|
let myNVL;
|
|
8
8
|
let myNvlContainer;
|
|
9
|
-
const zoomCallbackMock = jest.fn();
|
|
10
9
|
const zoomAndPanCallbackMock = jest.fn();
|
|
11
10
|
beforeEach(() => {
|
|
12
11
|
myNVL = new NVL(document.createElement('div'), [], [], { disableWebGL: true, initialZoom: 1, layout: 'free' });
|
|
@@ -16,7 +15,6 @@ describe('ZoomInteraction', () => {
|
|
|
16
15
|
afterEach(() => {
|
|
17
16
|
zoomInteraction.destroy();
|
|
18
17
|
myNVL.destroy();
|
|
19
|
-
zoomCallbackMock.mockReset();
|
|
20
18
|
zoomAndPanCallbackMock.mockReset();
|
|
21
19
|
});
|
|
22
20
|
const simulateWheelEvent = async (container, deltaY, delay = 50, eventOptions = {}) => {
|
|
@@ -38,31 +36,33 @@ describe('ZoomInteraction', () => {
|
|
|
38
36
|
};
|
|
39
37
|
describe('Scrolling', () => {
|
|
40
38
|
test('zooming in should increase the scale', async () => {
|
|
41
|
-
zoomInteraction.updateCallback('
|
|
39
|
+
zoomInteraction.updateCallback('onZoomAndPan', zoomAndPanCallbackMock);
|
|
42
40
|
const initialScale = myNVL.getScale();
|
|
43
41
|
const container = myNVL.getContainer();
|
|
44
42
|
await simulateWheelEvent(container, -100);
|
|
45
|
-
expect(
|
|
43
|
+
expect(zoomAndPanCallbackMock).toHaveBeenCalledTimes(1);
|
|
46
44
|
const newScale = myNVL.getScale();
|
|
47
45
|
expect(newScale).toBeGreaterThan(initialScale);
|
|
48
46
|
expect(newScale).toBe(1.2);
|
|
49
47
|
});
|
|
50
48
|
test('zooming out should decrease the scale', async () => {
|
|
51
|
-
zoomInteraction.updateCallback('
|
|
49
|
+
zoomInteraction.updateCallback('onZoomAndPan', zoomAndPanCallbackMock);
|
|
52
50
|
const initialScale = myNVL.getScale();
|
|
53
51
|
await simulateWheelEvent(myNvlContainer, 100);
|
|
54
|
-
expect(
|
|
52
|
+
expect(zoomAndPanCallbackMock).toHaveBeenCalledTimes(1);
|
|
55
53
|
const newScale = myNVL.getScale();
|
|
56
54
|
expect(newScale).toBeLessThan(initialScale);
|
|
57
55
|
expect(newScale).toBe(0.8);
|
|
58
56
|
});
|
|
59
|
-
test('zoom callback should be called with the correct zoom level', async () => {
|
|
60
|
-
zoomInteraction.updateCallback('
|
|
57
|
+
test('zoom callback should be called with the correct zoom level and pan values', async () => {
|
|
58
|
+
zoomInteraction.updateCallback('onZoomAndPan', zoomAndPanCallbackMock);
|
|
61
59
|
const wheelEvent = await simulateWheelEvent(myNvlContainer, -100);
|
|
62
|
-
expect(
|
|
63
|
-
const
|
|
64
|
-
expect(
|
|
65
|
-
expect(
|
|
60
|
+
expect(zoomAndPanCallbackMock).toHaveBeenCalledTimes(1);
|
|
61
|
+
const zoomAndPanCallbackMockArguments = zoomAndPanCallbackMock.mock.calls[0] ?? [];
|
|
62
|
+
expect(zoomAndPanCallbackMockArguments[0]).toBe(1.2);
|
|
63
|
+
expect(zoomAndPanCallbackMockArguments[1]).toBe(25);
|
|
64
|
+
expect(zoomAndPanCallbackMockArguments[2]).toBe(25);
|
|
65
|
+
expect(zoomAndPanCallbackMockArguments[3]).toBe(wheelEvent);
|
|
66
66
|
});
|
|
67
67
|
test('zooming should adjust pan to mouse position', async () => {
|
|
68
68
|
const initialPan = myNVL.getPan();
|
|
@@ -92,53 +92,27 @@ describe('ZoomInteraction', () => {
|
|
|
92
92
|
expect(newScale).toBeGreaterThan(2.5);
|
|
93
93
|
expect(newScale).toBeLessThan(2.7);
|
|
94
94
|
});
|
|
95
|
-
test('onZoomAndPan callback should be called with zoom level and pan values', async () => {
|
|
96
|
-
zoomInteraction.updateCallback('onZoomAndPan', zoomAndPanCallbackMock);
|
|
97
|
-
const wheelEvent = await simulateWheelEvent(myNvlContainer, -100);
|
|
98
|
-
expect(zoomAndPanCallbackMock).toHaveBeenCalledTimes(1);
|
|
99
|
-
const zoomAndPanCallbackMockArguments = zoomAndPanCallbackMock.mock.calls[0] ?? [];
|
|
100
|
-
expect(zoomAndPanCallbackMockArguments[0]).toBe(1.2);
|
|
101
|
-
expect(zoomAndPanCallbackMockArguments[1]).toBe(25);
|
|
102
|
-
expect(zoomAndPanCallbackMockArguments[2]).toBe(25);
|
|
103
|
-
expect(zoomAndPanCallbackMockArguments[3]).toBe(wheelEvent);
|
|
104
|
-
expect(zoomCallbackMock).toHaveBeenCalledTimes(0);
|
|
105
|
-
});
|
|
106
|
-
test('both callbacks should be called when both are registered', async () => {
|
|
107
|
-
zoomInteraction.updateCallback('onZoom', zoomCallbackMock);
|
|
108
|
-
zoomInteraction.updateCallback('onZoomAndPan', zoomAndPanCallbackMock);
|
|
109
|
-
const wheelEvent = await simulateWheelEvent(myNvlContainer, -100);
|
|
110
|
-
expect(zoomCallbackMock).toHaveBeenCalledTimes(1);
|
|
111
|
-
expect(zoomAndPanCallbackMock).toHaveBeenCalledTimes(1);
|
|
112
|
-
const zoomCallbackMockArguments = zoomCallbackMock.mock.calls[0] ?? [];
|
|
113
|
-
expect(zoomCallbackMockArguments[0]).toBe(1.2);
|
|
114
|
-
expect(zoomCallbackMockArguments[1]).toBe(wheelEvent);
|
|
115
|
-
const zoomAndPanCallbackMockArguments = zoomAndPanCallbackMock.mock.calls[0] ?? [];
|
|
116
|
-
expect(zoomAndPanCallbackMockArguments[0]).toBe(1.2);
|
|
117
|
-
expect(zoomAndPanCallbackMockArguments[1]).toBe(25);
|
|
118
|
-
expect(zoomAndPanCallbackMockArguments[2]).toBe(25);
|
|
119
|
-
expect(zoomAndPanCallbackMockArguments[3]).toBe(wheelEvent);
|
|
120
|
-
});
|
|
121
95
|
});
|
|
122
96
|
describe('Trackpad pinch gestures', () => {
|
|
123
97
|
test('trackpad pinch out should zoom in', async () => {
|
|
124
|
-
zoomInteraction.updateCallback('
|
|
98
|
+
zoomInteraction.updateCallback('onZoomAndPan', zoomAndPanCallbackMock);
|
|
125
99
|
await simulateWheelEvent(myNvlContainer, -75, 50, { ctrlKey: true });
|
|
126
|
-
expect(
|
|
100
|
+
expect(zoomAndPanCallbackMock).toHaveBeenCalledTimes(1);
|
|
127
101
|
const newScale = myNVL.getScale();
|
|
128
102
|
expect(newScale).toBe(2);
|
|
129
103
|
});
|
|
130
104
|
test('trackpad pinch in should zoom out', async () => {
|
|
131
|
-
zoomInteraction.updateCallback('
|
|
105
|
+
zoomInteraction.updateCallback('onZoomAndPan', zoomAndPanCallbackMock);
|
|
132
106
|
await simulateWheelEvent(myNvlContainer, 100, 50, { ctrlKey: true });
|
|
133
|
-
expect(
|
|
107
|
+
expect(zoomAndPanCallbackMock).toHaveBeenCalledTimes(1);
|
|
134
108
|
const newScale = myNVL.getScale();
|
|
135
109
|
expect(newScale).toBe(0.075);
|
|
136
110
|
});
|
|
137
111
|
test('trackpad pinch should work with meta key (macOS)', async () => {
|
|
138
|
-
zoomInteraction.updateCallback('
|
|
112
|
+
zoomInteraction.updateCallback('onZoomAndPan', zoomAndPanCallbackMock);
|
|
139
113
|
const initialScale = myNVL.getScale();
|
|
140
114
|
await simulateWheelEvent(myNvlContainer, -50, 50, { metaKey: true });
|
|
141
|
-
expect(
|
|
115
|
+
expect(zoomAndPanCallbackMock).toHaveBeenCalledTimes(1);
|
|
142
116
|
const newScale = myNVL.getScale();
|
|
143
117
|
expect(newScale).toBeGreaterThan(initialScale);
|
|
144
118
|
});
|
|
@@ -183,12 +157,12 @@ describe('ZoomInteraction', () => {
|
|
|
183
157
|
myNVL = new NVL(document.createElement('div'), [], [], { disableWebGL: true, initialZoom: 1, layout: 'free' });
|
|
184
158
|
myNvlContainer = myNVL.getContainer();
|
|
185
159
|
const zoomInteractionWithUpdate = new ZoomInteraction(myNVL, { controlledZoom: false });
|
|
186
|
-
zoomInteractionWithUpdate.updateCallback('
|
|
160
|
+
zoomInteractionWithUpdate.updateCallback('onZoomAndPan', zoomAndPanCallbackMock);
|
|
187
161
|
const initialScale = myNVL.getScale();
|
|
188
162
|
await simulateWheelEvent(myNvlContainer, -100);
|
|
189
163
|
expect(myNVL.getScale()).not.toBe(initialScale);
|
|
190
164
|
expect(myNVL.getScale()).toBe(1.2);
|
|
191
|
-
expect(
|
|
165
|
+
expect(zoomAndPanCallbackMock).toHaveBeenCalledTimes(1);
|
|
192
166
|
zoomInteractionWithUpdate.destroy();
|
|
193
167
|
});
|
|
194
168
|
test('when controlledZoom is true, should provide correct callback values for multiple operations', async () => {
|
|
@@ -20,7 +20,7 @@ export type BoxSelectInteractionCallbacks = {
|
|
|
20
20
|
*/
|
|
21
21
|
onBoxStarted?: ((event: MouseEvent) => void) | boolean;
|
|
22
22
|
/**
|
|
23
|
-
* Called
|
|
23
|
+
* Called once the user releases the mouse after multi-selecting.
|
|
24
24
|
* @param selectionObject - The selected nodes and relationships.
|
|
25
25
|
* @param event - The original mouse event.
|
|
26
26
|
*/
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import type { HitTargets, NVL, Node, Relationship } from '@neo4j-nvl/base';
|
|
1
|
+
import type { Cluster, HitTargets, NVL, Node, Relationship } from '@neo4j-nvl/base';
|
|
2
2
|
import { BaseInteraction } from './base';
|
|
3
3
|
/**
|
|
4
4
|
* Options for the click interaction handler to customize its behavior.
|
|
5
5
|
*/
|
|
6
6
|
export type ClickInteractionOptions = {
|
|
7
7
|
/**
|
|
8
|
-
* Whether to select the node or
|
|
8
|
+
* Whether to select the node, relationship, or cluster members on click.
|
|
9
|
+
* Clicking something that is already selected clears the selection.
|
|
9
10
|
* @defaultValue false
|
|
10
11
|
*/
|
|
11
12
|
selectOnClick?: boolean;
|
|
@@ -15,66 +16,87 @@ export type ClickInteractionOptions = {
|
|
|
15
16
|
*/
|
|
16
17
|
export type ClickInteractionCallbacks = {
|
|
17
18
|
/**
|
|
18
|
-
* Called when a node is clicked
|
|
19
|
-
* @param
|
|
19
|
+
* Called when a node is clicked.
|
|
20
|
+
* @param node - The node that was clicked.
|
|
20
21
|
* @param hitElements - All elements that were hit by the click.
|
|
21
22
|
* @param event - The original mouse event.
|
|
22
23
|
*/
|
|
23
|
-
onNodeClick?: ((
|
|
24
|
+
onNodeClick?: ((node: Node, hitElements: HitTargets, event: MouseEvent) => void) | boolean;
|
|
24
25
|
/**
|
|
25
|
-
* Called when a relationship is clicked
|
|
26
|
+
* Called when a relationship is clicked.
|
|
26
27
|
* @param relationship - The relationship that was clicked.
|
|
27
28
|
* @param hitElements - All elements that were hit by the click.
|
|
28
29
|
* @param event - The original mouse event.
|
|
29
30
|
*/
|
|
30
31
|
onRelationshipClick?: ((relationship: Relationship, hitElements: HitTargets, event: MouseEvent) => void) | boolean;
|
|
31
32
|
/**
|
|
32
|
-
* Called when
|
|
33
|
+
* Called when a cluster hull is clicked (empty space inside a clusterBy hull).
|
|
34
|
+
* @param cluster - The cluster that was clicked.
|
|
35
|
+
* @param hitElements - All elements that were hit by the click.
|
|
36
|
+
* @param event - The original mouse event.
|
|
37
|
+
*/
|
|
38
|
+
onClusterClick?: ((cluster: Cluster, hitElements: HitTargets, event: MouseEvent) => void) | boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Called when the canvas is clicked.
|
|
33
41
|
* @param event - The original mouse event.
|
|
34
42
|
*/
|
|
35
43
|
onCanvasClick?: ((event: MouseEvent) => void) | boolean;
|
|
36
44
|
/**
|
|
37
|
-
* Called when
|
|
45
|
+
* Called when the canvas is double-clicked.
|
|
38
46
|
* @param event - The original mouse event.
|
|
39
47
|
*/
|
|
40
48
|
onCanvasDoubleClick?: ((event: MouseEvent) => void) | boolean;
|
|
41
49
|
/**
|
|
42
|
-
* Called when
|
|
50
|
+
* Called when the canvas is right-clicked.
|
|
43
51
|
* @param event - The original mouse event.
|
|
44
52
|
*/
|
|
45
53
|
onCanvasRightClick?: ((event: MouseEvent) => void) | boolean;
|
|
46
54
|
/**
|
|
47
|
-
* Called when a node is double-clicked
|
|
48
|
-
* @param
|
|
55
|
+
* Called when a node is double-clicked.
|
|
56
|
+
* @param node - The node that was double-clicked.
|
|
49
57
|
* @param hitElements - All elements that were hit by the double-click.
|
|
50
58
|
* @param event - The original mouse event.
|
|
51
59
|
*/
|
|
52
60
|
onNodeDoubleClick?: ((node: Node, hitElements: HitTargets, event: MouseEvent) => void) | boolean;
|
|
53
61
|
/**
|
|
54
|
-
* Called when a node is right-clicked
|
|
55
|
-
* @param
|
|
62
|
+
* Called when a node is right-clicked.
|
|
63
|
+
* @param node - The node that was right-clicked.
|
|
56
64
|
* @param hitElements - All elements that were hit by the right-click.
|
|
57
65
|
* @param event - The original mouse event.
|
|
58
66
|
*/
|
|
59
67
|
onNodeRightClick?: ((node: Node, hitElements: HitTargets, event: MouseEvent) => void) | boolean;
|
|
60
68
|
/**
|
|
61
|
-
* Called when a relationship is double-clicked
|
|
69
|
+
* Called when a relationship is double-clicked.
|
|
62
70
|
* @param relationship - The relationship that was double-clicked.
|
|
63
71
|
* @param hitElements - All elements that were hit by the double-click.
|
|
64
72
|
* @param event - The original mouse event.
|
|
65
73
|
*/
|
|
66
74
|
onRelationshipDoubleClick?: ((relationship: Relationship, hitElements: HitTargets, event: MouseEvent) => void) | boolean;
|
|
67
75
|
/**
|
|
68
|
-
* Called when a relationship is right-clicked
|
|
76
|
+
* Called when a relationship is right-clicked.
|
|
69
77
|
* @param relationship - The relationship that was right-clicked.
|
|
70
78
|
* @param hitElements - All elements that were hit by the right-click.
|
|
71
79
|
* @param event - The original mouse event.
|
|
72
80
|
*/
|
|
73
81
|
onRelationshipRightClick?: ((relationship: Relationship, hitElements: HitTargets, event: MouseEvent) => void) | boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Called when a cluster hull is double-clicked.
|
|
84
|
+
* @param cluster - The cluster that was double-clicked.
|
|
85
|
+
* @param hitElements - All elements that were hit by the double-click.
|
|
86
|
+
* @param event - The original mouse event.
|
|
87
|
+
*/
|
|
88
|
+
onClusterDoubleClick?: ((cluster: Cluster, hitElements: HitTargets, event: MouseEvent) => void) | boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Called when a cluster hull is right-clicked.
|
|
91
|
+
* @param cluster - The cluster that was right-clicked.
|
|
92
|
+
* @param hitElements - All elements that were hit by the right-click.
|
|
93
|
+
* @param event - The original mouse event.
|
|
94
|
+
*/
|
|
95
|
+
onClusterRightClick?: ((cluster: Cluster, hitElements: HitTargets, event: MouseEvent) => void) | boolean;
|
|
74
96
|
};
|
|
75
97
|
/**
|
|
76
98
|
* Click interaction handler that handles click, double click and right click events on
|
|
77
|
-
* nodes, relationships, and the scene.
|
|
99
|
+
* nodes, relationships, clusters, and the scene.
|
|
78
100
|
*
|
|
79
101
|
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/interaction-handlers/#_clickinteraction Click Interaction documentation page}.
|
|
80
102
|
*/
|
|
@@ -91,6 +113,14 @@ export declare class ClickInteraction extends BaseInteraction<ClickInteractionOp
|
|
|
91
113
|
private handleRightClick;
|
|
92
114
|
private handleDoubleClick;
|
|
93
115
|
private handleClick;
|
|
116
|
+
/**
|
|
117
|
+
* Makes the given elements the entire selection, deselecting everything else.
|
|
118
|
+
*
|
|
119
|
+
* Clicking something that is already fully selected clears the selection instead, so a repeated
|
|
120
|
+
* click toggles it off. A cluster counts as selected once all of its members are, which keeps
|
|
121
|
+
* cluster clicks behaving like node and relationship clicks.
|
|
122
|
+
*/
|
|
123
|
+
private setSelection;
|
|
94
124
|
/**
|
|
95
125
|
* Removes all related event listeners from the container.
|
|
96
126
|
*/
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { BaseInteraction } from './base';
|
|
2
2
|
import { isDraggingMovement } from './utils';
|
|
3
|
+
/**
|
|
4
|
+
* Marks the given ids as selected and deselects everything that is currently selected but not in that list.
|
|
5
|
+
*/
|
|
6
|
+
const buildSelectionUpdates = (idsToSelect, currentlySelected) => {
|
|
7
|
+
const keptSelected = new Set(idsToSelect);
|
|
8
|
+
return [
|
|
9
|
+
...idsToSelect.map((id) => ({ id, selected: true })),
|
|
10
|
+
...currentlySelected.filter(({ id }) => !keptSelected.has(id)).map(({ id }) => ({ id, selected: false }))
|
|
11
|
+
];
|
|
12
|
+
};
|
|
3
13
|
/**
|
|
4
14
|
* Click interaction handler that handles click, double click and right click events on
|
|
5
|
-
* nodes, relationships, and the scene.
|
|
15
|
+
* nodes, relationships, clusters, and the scene.
|
|
6
16
|
*
|
|
7
17
|
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/interaction-handlers/#_clickinteraction Click Interaction documentation page}.
|
|
8
18
|
*/
|
|
@@ -27,8 +37,8 @@ export class ClickInteraction extends BaseInteraction {
|
|
|
27
37
|
handleRightClick = (event) => {
|
|
28
38
|
event.preventDefault();
|
|
29
39
|
const { nvlTargets } = this.nvlInstance.getHits(event);
|
|
30
|
-
const { nodes = [], relationships = [] } = nvlTargets;
|
|
31
|
-
if (nodes.length === 0 && relationships.length === 0) {
|
|
40
|
+
const { nodes = [], relationships = [], clusters = [] } = nvlTargets;
|
|
41
|
+
if (nodes.length === 0 && relationships.length === 0 && clusters.length === 0) {
|
|
32
42
|
this.callCallbackIfRegistered('onCanvasRightClick', event);
|
|
33
43
|
return;
|
|
34
44
|
}
|
|
@@ -38,11 +48,14 @@ export class ClickInteraction extends BaseInteraction {
|
|
|
38
48
|
else if (relationships.length > 0) {
|
|
39
49
|
this.callCallbackIfRegistered('onRelationshipRightClick', relationships[0]?.data, nvlTargets, event);
|
|
40
50
|
}
|
|
51
|
+
else if (clusters.length > 0) {
|
|
52
|
+
this.callCallbackIfRegistered('onClusterRightClick', clusters[0]?.data, nvlTargets, event);
|
|
53
|
+
}
|
|
41
54
|
};
|
|
42
55
|
handleDoubleClick = (event) => {
|
|
43
56
|
const { nvlTargets } = this.nvlInstance.getHits(event);
|
|
44
|
-
const { nodes = [], relationships = [] } = nvlTargets;
|
|
45
|
-
if (nodes.length === 0 && relationships.length === 0) {
|
|
57
|
+
const { nodes = [], relationships = [], clusters = [] } = nvlTargets;
|
|
58
|
+
if (nodes.length === 0 && relationships.length === 0 && clusters.length === 0) {
|
|
46
59
|
this.callCallbackIfRegistered('onCanvasDoubleClick', event);
|
|
47
60
|
return;
|
|
48
61
|
}
|
|
@@ -52,14 +65,17 @@ export class ClickInteraction extends BaseInteraction {
|
|
|
52
65
|
else if (relationships.length > 0) {
|
|
53
66
|
this.callCallbackIfRegistered('onRelationshipDoubleClick', relationships[0]?.data, nvlTargets, event);
|
|
54
67
|
}
|
|
68
|
+
else if (clusters.length > 0) {
|
|
69
|
+
this.callCallbackIfRegistered('onClusterDoubleClick', clusters[0]?.data, nvlTargets, event);
|
|
70
|
+
}
|
|
55
71
|
};
|
|
56
72
|
handleClick = (event) => {
|
|
57
73
|
if (isDraggingMovement(event, this.mousePosition) || event.button !== 0) {
|
|
58
74
|
return;
|
|
59
75
|
}
|
|
60
76
|
const { nvlTargets } = this.nvlInstance.getHits(event);
|
|
61
|
-
const { nodes = [], relationships = [] } = nvlTargets;
|
|
62
|
-
if (nodes.length === 0 && relationships.length === 0) {
|
|
77
|
+
const { nodes = [], relationships = [], clusters = [] } = nvlTargets;
|
|
78
|
+
if (nodes.length === 0 && relationships.length === 0 && clusters.length === 0) {
|
|
63
79
|
if (this.currentOptions.selectOnClick === true) {
|
|
64
80
|
this.nvlInstance.deselectAll();
|
|
65
81
|
}
|
|
@@ -67,41 +83,46 @@ export class ClickInteraction extends BaseInteraction {
|
|
|
67
83
|
return;
|
|
68
84
|
}
|
|
69
85
|
if (nodes.length > 0) {
|
|
70
|
-
const
|
|
86
|
+
const node = nodes[0]?.data;
|
|
71
87
|
if (this.currentOptions.selectOnClick === true) {
|
|
72
|
-
|
|
73
|
-
const selectedRelationships = this.nvlInstance.getSelectedRelationships();
|
|
74
|
-
const newSelectedNodes = hitNodeData[0] ? [{ id: hitNodeData[0].id, selected: true }] : [];
|
|
75
|
-
const nodeUpdates = [...newSelectedNodes, ...selectedNodes.map((node) => ({ id: node.id, selected: false }))];
|
|
76
|
-
const relationshipUpdates = selectedRelationships.map((relationship) => ({
|
|
77
|
-
...relationship,
|
|
78
|
-
selected: false
|
|
79
|
-
}));
|
|
80
|
-
this.nvlInstance.updateElementsInGraph(nodeUpdates, relationshipUpdates);
|
|
88
|
+
this.setSelection(node ? [node.id] : [], []);
|
|
81
89
|
}
|
|
82
|
-
this.callCallbackIfRegistered('onNodeClick',
|
|
90
|
+
this.callCallbackIfRegistered('onNodeClick', node, nvlTargets, event);
|
|
83
91
|
}
|
|
84
92
|
else if (relationships.length > 0) {
|
|
85
|
-
const
|
|
93
|
+
const relationship = relationships[0]?.data;
|
|
94
|
+
if (this.currentOptions.selectOnClick === true) {
|
|
95
|
+
this.setSelection([], relationship ? [relationship.id] : []);
|
|
96
|
+
}
|
|
97
|
+
this.callCallbackIfRegistered('onRelationshipClick', relationship, nvlTargets, event);
|
|
98
|
+
}
|
|
99
|
+
else if (clusters.length > 0) {
|
|
100
|
+
const cluster = clusters[0]?.data;
|
|
86
101
|
if (this.currentOptions.selectOnClick === true) {
|
|
87
|
-
|
|
88
|
-
const selectedRelationships = this.nvlInstance.getSelectedRelationships();
|
|
89
|
-
const nodeUpdates = selectedNodes.map((node) => ({ id: node.id, selected: false }));
|
|
90
|
-
const newSelectedRelationships = hitRelationshipData[0]
|
|
91
|
-
? [{ id: hitRelationshipData[0].id, selected: true }]
|
|
92
|
-
: [];
|
|
93
|
-
const relationshipUpdates = [
|
|
94
|
-
...newSelectedRelationships,
|
|
95
|
-
...selectedRelationships.map((relationship) => ({
|
|
96
|
-
...relationship,
|
|
97
|
-
selected: false
|
|
98
|
-
}))
|
|
99
|
-
];
|
|
100
|
-
this.nvlInstance.updateElementsInGraph(nodeUpdates, relationshipUpdates);
|
|
102
|
+
this.setSelection(cluster ? cluster.nodeIds : [], []);
|
|
101
103
|
}
|
|
102
|
-
this.callCallbackIfRegistered('
|
|
104
|
+
this.callCallbackIfRegistered('onClusterClick', cluster, nvlTargets, event);
|
|
103
105
|
}
|
|
104
106
|
};
|
|
107
|
+
/**
|
|
108
|
+
* Makes the given elements the entire selection, deselecting everything else.
|
|
109
|
+
*
|
|
110
|
+
* Clicking something that is already fully selected clears the selection instead, so a repeated
|
|
111
|
+
* click toggles it off. A cluster counts as selected once all of its members are, which keeps
|
|
112
|
+
* cluster clicks behaving like node and relationship clicks.
|
|
113
|
+
*/
|
|
114
|
+
setSelection(nodeIds, relationshipIds) {
|
|
115
|
+
const selectedNodes = this.nvlInstance.getSelectedNodes();
|
|
116
|
+
const selectedRelationships = this.nvlInstance.getSelectedRelationships();
|
|
117
|
+
const selectedNodeIds = new Set(selectedNodes.map((n) => n.id));
|
|
118
|
+
const selectedRelationshipIds = new Set(selectedRelationships.map((r) => r.id));
|
|
119
|
+
const isAlreadySelected = nodeIds.length + relationshipIds.length > 0 &&
|
|
120
|
+
nodeIds.every((id) => selectedNodeIds.has(id)) &&
|
|
121
|
+
relationshipIds.every((id) => selectedRelationshipIds.has(id));
|
|
122
|
+
const nodeIdsToSelect = isAlreadySelected ? [] : nodeIds;
|
|
123
|
+
const relationshipIdsToSelect = isAlreadySelected ? [] : relationshipIds;
|
|
124
|
+
this.nvlInstance.updateElementsInGraph(buildSelectionUpdates(nodeIdsToSelect, selectedNodes), buildSelectionUpdates(relationshipIdsToSelect, selectedRelationships));
|
|
125
|
+
}
|
|
105
126
|
/**
|
|
106
127
|
* Removes all related event listeners from the container.
|
|
107
128
|
*/
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { HitTargets, NVL, Node, Relationship } from '@neo4j-nvl/base';
|
|
2
2
|
import { BaseInteraction } from './base';
|
|
3
3
|
/**
|
|
4
|
-
* Options for the
|
|
4
|
+
* Options for the hover interaction handler to customize its behavior.
|
|
5
5
|
*/
|
|
6
6
|
export type HoverInteractionOptions = {
|
|
7
7
|
/**
|
|
8
|
-
* Whether to draw a shadow on hover
|
|
8
|
+
* Whether to draw a shadow on hover for nodes/relationships
|
|
9
9
|
* @default false
|
|
10
10
|
*/
|
|
11
11
|
drawShadowOnHover?: boolean;
|
|
@@ -15,9 +15,9 @@ export type HoverInteractionOptions = {
|
|
|
15
15
|
*/
|
|
16
16
|
export type HoverInteractionCallbacks = {
|
|
17
17
|
/**
|
|
18
|
-
* Called when a node or relationship is hovered
|
|
19
|
-
* @param element - The node or relationship
|
|
20
|
-
* @param hitElements - All
|
|
18
|
+
* Called when a node or relationship is hovered.
|
|
19
|
+
* @param element - The highest-priority node or relationship under the pointer.
|
|
20
|
+
* @param hitElements - All nodes, relationships, and clusters under the pointer.
|
|
21
21
|
* @param event - The original mouse event.
|
|
22
22
|
* This callback is called every time the mouse moves,
|
|
23
23
|
* even when no element is hovered or the current element is already hovered.
|
|
@@ -21,7 +21,7 @@ export type LassoInteractionCallbacks = {
|
|
|
21
21
|
onLassoStarted?: ((event: MouseEvent) => void) | boolean;
|
|
22
22
|
/**
|
|
23
23
|
* Called once when the user releases the mouse after lasso selecting.
|
|
24
|
-
* @param selectionObject - The selected
|
|
24
|
+
* @param selectionObject - The selected nodes and relationships.
|
|
25
25
|
* @param event - The original mouse event.
|
|
26
26
|
*/
|
|
27
27
|
onLassoSelect?: (({ nodes, rels }: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { NVL } from '@neo4j-nvl/base';
|
|
2
2
|
import { BaseInteraction } from './base';
|
|
3
3
|
/**
|
|
4
|
-
* Options for the
|
|
4
|
+
* Options for the pan interaction handler to customize its behavior.
|
|
5
5
|
*/
|
|
6
6
|
export type PanInteractionOptions = {
|
|
7
7
|
/**
|