@neo4j-nvl/interaction-handlers 0.3.8 → 0.3.9
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 +24 -0
- package/LICENSE.txt +3 -48
- package/lib/__tests__/box-select-interaction.test.d.ts +1 -0
- package/lib/__tests__/box-select-interaction.test.js +123 -0
- package/lib/__tests__/drag-node-interaction.test.js +138 -22
- package/lib/__tests__/lasso-interaction.test.js +35 -0
- package/lib/__tests__/pan-interaction.test.d.ts +1 -0
- package/lib/__tests__/pan-interaction.test.js +115 -0
- package/lib/__tests__/zoom-interaction.test.d.ts +1 -0
- package/lib/__tests__/zoom-interaction.test.js +104 -0
- package/lib/interaction-handlers/base.d.ts +6 -0
- package/lib/interaction-handlers/base.js +19 -0
- package/lib/interaction-handlers/box-select-interaction.js +6 -8
- package/lib/interaction-handlers/click-interaction.js +2 -3
- package/lib/interaction-handlers/drag-node-interaction.d.ts +2 -1
- package/lib/interaction-handlers/drag-node-interaction.js +18 -16
- package/lib/interaction-handlers/draw-interaction.js +11 -14
- package/lib/interaction-handlers/hover-interaction.d.ts +2 -2
- package/lib/interaction-handlers/hover-interaction.js +33 -28
- package/lib/interaction-handlers/lasso-interaction.js +6 -7
- package/lib/interaction-handlers/pan-interaction.d.ts +1 -0
- package/lib/interaction-handlers/pan-interaction.js +13 -6
- package/lib/interaction-handlers/zoom-interaction.d.ts +5 -0
- package/lib/interaction-handlers/zoom-interaction.js +10 -1
- package/package.json +2 -2
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import NVL from '@neo4j-nvl/base';
|
|
2
|
+
import '@testing-library/jest-dom';
|
|
3
|
+
import { ZoomInteraction } from '../interaction-handlers/zoom-interaction';
|
|
4
|
+
jest.mock('@neo4j-nvl/layout-workers');
|
|
5
|
+
describe('ZoomInteraction', () => {
|
|
6
|
+
let zoomInteraction;
|
|
7
|
+
let myNVL;
|
|
8
|
+
let myNvlContainer;
|
|
9
|
+
const zoomCallbackMock = jest.fn();
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
myNVL = new NVL(document.createElement('div'), [], [], { disableWebGL: true, initialZoom: 1, layout: 'free' });
|
|
12
|
+
myNvlContainer = myNVL.getContainer();
|
|
13
|
+
zoomInteraction = new ZoomInteraction(myNVL);
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
zoomInteraction.destroy();
|
|
17
|
+
myNVL.destroy();
|
|
18
|
+
zoomCallbackMock.mockReset();
|
|
19
|
+
});
|
|
20
|
+
const simulateWheelEvent = async (container, deltaY, delay = 50, eventOptions = {}) => {
|
|
21
|
+
const wheelEvent = new WheelEvent('wheel', {
|
|
22
|
+
deltaY,
|
|
23
|
+
clientX: 150,
|
|
24
|
+
clientY: 150,
|
|
25
|
+
...eventOptions
|
|
26
|
+
});
|
|
27
|
+
container.dispatchEvent(wheelEvent);
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
29
|
+
return wheelEvent;
|
|
30
|
+
};
|
|
31
|
+
const simulateSequentialWheelEvents = async (container, events) => {
|
|
32
|
+
for (const event of events) {
|
|
33
|
+
// eslint-disable-next-line no-await-in-loop
|
|
34
|
+
await simulateWheelEvent(container, event.deltaY, event.delay);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
test('zooming in should increase the scale', async () => {
|
|
38
|
+
zoomInteraction.updateCallback('onZoom', zoomCallbackMock);
|
|
39
|
+
const initialScale = myNVL.getScale();
|
|
40
|
+
const container = myNVL.getContainer();
|
|
41
|
+
await simulateWheelEvent(container, -100);
|
|
42
|
+
expect(zoomCallbackMock).toHaveBeenCalledTimes(1);
|
|
43
|
+
const newScale = myNVL.getScale();
|
|
44
|
+
expect(newScale).toBeGreaterThan(initialScale);
|
|
45
|
+
expect(newScale).toBe(1.2);
|
|
46
|
+
});
|
|
47
|
+
test('zooming out should decrease the scale', async () => {
|
|
48
|
+
zoomInteraction.updateCallback('onZoom', zoomCallbackMock);
|
|
49
|
+
const initialScale = myNVL.getScale();
|
|
50
|
+
await simulateWheelEvent(myNvlContainer, 100);
|
|
51
|
+
expect(zoomCallbackMock).toHaveBeenCalledTimes(1);
|
|
52
|
+
const newScale = myNVL.getScale();
|
|
53
|
+
expect(newScale).toBeLessThan(initialScale);
|
|
54
|
+
expect(newScale).toBe(0.8);
|
|
55
|
+
});
|
|
56
|
+
test('zoom callback should be called with the correct zoom level', async () => {
|
|
57
|
+
zoomInteraction.updateCallback('onZoom', zoomCallbackMock);
|
|
58
|
+
const wheelEvent = await simulateWheelEvent(myNvlContainer, -100);
|
|
59
|
+
expect(zoomCallbackMock).toHaveBeenCalledTimes(1);
|
|
60
|
+
const zoomCallbackMockArguments = zoomCallbackMock.mock.calls[0] ?? [];
|
|
61
|
+
expect(zoomCallbackMockArguments[0]).toBe(1.2);
|
|
62
|
+
expect(zoomCallbackMockArguments[1]).toBe(wheelEvent);
|
|
63
|
+
});
|
|
64
|
+
test('zooming should adjust pan to mouse position', async () => {
|
|
65
|
+
const initialPan = myNVL.getPan();
|
|
66
|
+
const initialScale = myNVL.getScale();
|
|
67
|
+
await simulateWheelEvent(myNvlContainer, -100);
|
|
68
|
+
const newPan = myNVL.getPan();
|
|
69
|
+
const newScale = myNVL.getScale();
|
|
70
|
+
expect(newPan).not.toEqual(initialPan);
|
|
71
|
+
expect(newPan.x).toBe(25);
|
|
72
|
+
expect(newPan.y).toBe(25);
|
|
73
|
+
expect(newScale).toBeGreaterThan(initialScale);
|
|
74
|
+
expect(newScale).toBe(1.2);
|
|
75
|
+
});
|
|
76
|
+
test('zooming should be progressively faster at higher zoom levels', async () => {
|
|
77
|
+
const sequentialWheelEvents = Array(8).fill({ deltaY: -100 });
|
|
78
|
+
await simulateSequentialWheelEvents(myNvlContainer, sequentialWheelEvents);
|
|
79
|
+
const newScale = myNVL.getScale();
|
|
80
|
+
expect(newScale).toBeGreaterThan(3.0);
|
|
81
|
+
expect(newScale).toBeCloseTo(4.299);
|
|
82
|
+
});
|
|
83
|
+
test('handles touchpad-like small incremental scrolling', async () => {
|
|
84
|
+
const initialScale = myNVL.getScale();
|
|
85
|
+
const touchpadEvents = Array(25).fill({ deltaY: -20, delay: 25 });
|
|
86
|
+
await simulateSequentialWheelEvents(myNvlContainer, touchpadEvents);
|
|
87
|
+
const newScale = myNVL.getScale();
|
|
88
|
+
expect(newScale).toBeGreaterThan(initialScale);
|
|
89
|
+
expect(newScale).toBeGreaterThan(2.5);
|
|
90
|
+
expect(newScale).toBeLessThan(2.7);
|
|
91
|
+
});
|
|
92
|
+
test('should not zoom when ctrl key is pressed', async () => {
|
|
93
|
+
const initialScale = myNVL.getScale();
|
|
94
|
+
await simulateWheelEvent(myNvlContainer, -100, 50, { ctrlKey: true });
|
|
95
|
+
expect(zoomCallbackMock).toHaveBeenCalledTimes(0);
|
|
96
|
+
expect(myNVL.getScale()).toBe(initialScale);
|
|
97
|
+
});
|
|
98
|
+
test('should not zoom when meta key is pressed', async () => {
|
|
99
|
+
const initialScale = myNVL.getScale();
|
|
100
|
+
await simulateWheelEvent(myNvlContainer, -100, 50, { metaKey: true });
|
|
101
|
+
expect(zoomCallbackMock).toHaveBeenCalledTimes(0);
|
|
102
|
+
expect(myNVL.getScale()).toBe(initialScale);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -48,6 +48,12 @@ declare abstract class BaseInteraction<T extends Record<string, ((...args: unkno
|
|
|
48
48
|
* @param name - The name of the event
|
|
49
49
|
*/
|
|
50
50
|
removeCallback: (name: keyof T | string) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Enables or disables global text selection during a drag or pan operation.
|
|
53
|
+
* @param enable - Whether to enable or disable global text selection
|
|
54
|
+
* @param eventFunction - The event function to be added/removed when text selection is disabled/enabled
|
|
55
|
+
*/
|
|
56
|
+
toggleGlobalTextSelection: (enable: boolean, eventFunction?: ((this: HTMLElement, ev: MouseEvent) => void) | undefined) => void;
|
|
51
57
|
/**
|
|
52
58
|
* @internal
|
|
53
59
|
*/
|
|
@@ -71,5 +71,24 @@ class BaseInteraction {
|
|
|
71
71
|
removeCallback = (name) => {
|
|
72
72
|
this.callbackMap.delete(name);
|
|
73
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* Enables or disables global text selection during a drag or pan operation.
|
|
76
|
+
* @param enable - Whether to enable or disable global text selection
|
|
77
|
+
* @param eventFunction - The event function to be added/removed when text selection is disabled/enabled
|
|
78
|
+
*/
|
|
79
|
+
toggleGlobalTextSelection = (enable, eventFunction) => {
|
|
80
|
+
if (enable) {
|
|
81
|
+
document.body.style.removeProperty('user-select');
|
|
82
|
+
if (eventFunction) {
|
|
83
|
+
document.body.removeEventListener('mouseup', eventFunction);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
document.body.style.setProperty('user-select', 'none', 'important');
|
|
88
|
+
if (eventFunction) {
|
|
89
|
+
document.body.addEventListener('mouseup', eventFunction);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
74
93
|
}
|
|
75
94
|
export { BaseInteraction };
|
|
@@ -9,10 +9,10 @@ import { getCanvasPosition, getWorldPosition } from './utils';
|
|
|
9
9
|
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/interaction-handlers/#_boxselectinteraction Box Select Interaction documentation page}.
|
|
10
10
|
*/
|
|
11
11
|
export class BoxSelectInteraction extends BaseInteraction {
|
|
12
|
-
mousePosition;
|
|
13
|
-
startWorldPosition;
|
|
12
|
+
mousePosition = { x: 0, y: 0 };
|
|
13
|
+
startWorldPosition = { x: 0, y: 0 };
|
|
14
14
|
overlayRenderer;
|
|
15
|
-
isBoxSelecting;
|
|
15
|
+
isBoxSelecting = false;
|
|
16
16
|
/**
|
|
17
17
|
* Creates a new instance of the multi-select interaction handler.
|
|
18
18
|
* @param nvl - The NVL instance to attach the interaction handler to
|
|
@@ -20,14 +20,10 @@ export class BoxSelectInteraction extends BaseInteraction {
|
|
|
20
20
|
*/
|
|
21
21
|
constructor(nvl, options = { selectOnRelease: false }) {
|
|
22
22
|
super(nvl, options);
|
|
23
|
-
this.mousePosition = { x: 0, y: 0 };
|
|
24
|
-
this.startWorldPosition = { x: 0, y: 0 };
|
|
25
23
|
this.overlayRenderer = new OverlayRenderer(this.containerInstance);
|
|
26
|
-
this.isBoxSelecting = false;
|
|
27
24
|
this.addEventListener('mousedown', this.handleMouseDown, true);
|
|
28
25
|
this.addEventListener('mousemove', this.handleDrag, true);
|
|
29
26
|
this.addEventListener('mouseup', this.endBoxSelect, true);
|
|
30
|
-
this.addEventListener('mouseleave', this.endBoxSelect, true);
|
|
31
27
|
}
|
|
32
28
|
handleMouseDown = (event) => {
|
|
33
29
|
if (event.button !== 0) {
|
|
@@ -86,15 +82,16 @@ export class BoxSelectInteraction extends BaseInteraction {
|
|
|
86
82
|
this.nvlInstance.updateElementsInGraph(nodes.map((node) => ({ id: node.id, selected: true })), rels.map((rel) => ({ id: rel.id, selected: true })));
|
|
87
83
|
}
|
|
88
84
|
this.callCallbackIfRegistered('onBoxSelect', { nodes, rels }, event);
|
|
85
|
+
this.toggleGlobalTextSelection(true, this.endBoxSelect);
|
|
89
86
|
};
|
|
90
87
|
/**
|
|
91
88
|
* Removes all related event listeners and the overlay renderer for the box.
|
|
92
89
|
*/
|
|
93
90
|
destroy() {
|
|
91
|
+
this.toggleGlobalTextSelection(true, this.endBoxSelect);
|
|
94
92
|
this.removeEventListener('mousedown', this.handleMouseDown, true);
|
|
95
93
|
this.removeEventListener('mousemove', this.handleDrag, true);
|
|
96
94
|
this.removeEventListener('mouseup', this.endBoxSelect, true);
|
|
97
|
-
this.removeEventListener('mouseleave', this.endBoxSelect, true);
|
|
98
95
|
this.overlayRenderer.destroy();
|
|
99
96
|
}
|
|
100
97
|
turnOnBoxSelect(event) {
|
|
@@ -106,6 +103,7 @@ export class BoxSelectInteraction extends BaseInteraction {
|
|
|
106
103
|
}
|
|
107
104
|
else {
|
|
108
105
|
this.isBoxSelecting = true;
|
|
106
|
+
this.toggleGlobalTextSelection(false, this.endBoxSelect);
|
|
109
107
|
this.callCallbackIfRegistered('onBoxStarted', event);
|
|
110
108
|
if (this.currentOptions.selectOnRelease === true) {
|
|
111
109
|
this.nvlInstance.deselectAll();
|
|
@@ -7,8 +7,8 @@ import { isDraggingMovement } from './utils';
|
|
|
7
7
|
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/interaction-handlers/#_clickinteraction Click Interaction documentation page}.
|
|
8
8
|
*/
|
|
9
9
|
export class ClickInteraction extends BaseInteraction {
|
|
10
|
-
moved;
|
|
11
|
-
mousePosition;
|
|
10
|
+
moved = false;
|
|
11
|
+
mousePosition = { x: 0, y: 0 };
|
|
12
12
|
/**
|
|
13
13
|
* Creates a new click interaction handler.
|
|
14
14
|
* @param nvl - The NVL instance to attach the interaction handler to
|
|
@@ -16,7 +16,6 @@ export class ClickInteraction extends BaseInteraction {
|
|
|
16
16
|
*/
|
|
17
17
|
constructor(nvl, options = { selectOnClick: false }) {
|
|
18
18
|
super(nvl, options);
|
|
19
|
-
this.mousePosition = { x: 0, y: 0 };
|
|
20
19
|
this.addEventListener('mousedown', this.handleMouseDown, true);
|
|
21
20
|
this.addEventListener('click', this.handleClick, true);
|
|
22
21
|
this.addEventListener('dblclick', this.handleDoubleClick, true);
|
|
@@ -44,7 +44,8 @@ export declare class DragNodeInteraction extends BaseInteraction<DragNodeInterac
|
|
|
44
44
|
constructor(nvl: NVL, options?: {});
|
|
45
45
|
private handleMouseDown;
|
|
46
46
|
private handleMouseMove;
|
|
47
|
-
private
|
|
47
|
+
private handleBodyMouseUp;
|
|
48
|
+
private resetState;
|
|
48
49
|
/**
|
|
49
50
|
* Removes all related event listeners from the container.
|
|
50
51
|
*/
|
|
@@ -9,26 +9,20 @@ import { isDraggingMovement } from './utils';
|
|
|
9
9
|
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/interaction-handlers/#_dragnodeinteraction Drag Node Interaction documentation page}.
|
|
10
10
|
*/
|
|
11
11
|
export class DragNodeInteraction extends BaseInteraction {
|
|
12
|
-
mousePosition;
|
|
13
|
-
mouseDownNode;
|
|
14
|
-
isDragging;
|
|
15
|
-
isDrawing;
|
|
16
|
-
selectedNodes;
|
|
17
|
-
moveSelectedNodes;
|
|
12
|
+
mousePosition = { x: 0, y: 0 };
|
|
13
|
+
mouseDownNode = null;
|
|
14
|
+
isDragging = false;
|
|
15
|
+
isDrawing = false;
|
|
16
|
+
selectedNodes = [];
|
|
17
|
+
moveSelectedNodes = false;
|
|
18
18
|
/**
|
|
19
19
|
* Creates a new instance of the drag node interaction handler.
|
|
20
20
|
* @param nvl - The NVL instance to attach the interaction handler to
|
|
21
21
|
*/
|
|
22
22
|
constructor(nvl, options = {}) {
|
|
23
23
|
super(nvl, options);
|
|
24
|
-
this.mousePosition = { x: 0, y: 0 };
|
|
25
|
-
this.isDragging = false;
|
|
26
|
-
this.isDrawing = false;
|
|
27
|
-
this.selectedNodes = [];
|
|
28
|
-
this.moveSelectedNodes = false;
|
|
29
24
|
this.addEventListener('mousedown', this.handleMouseDown);
|
|
30
25
|
this.addEventListener('mousemove', this.handleMouseMove);
|
|
31
|
-
this.addEventListener('mouseup', this.handleMouseUp);
|
|
32
26
|
}
|
|
33
27
|
handleMouseDown = (event) => {
|
|
34
28
|
this.mousePosition = { x: event.clientX, y: event.clientY };
|
|
@@ -38,6 +32,7 @@ export class DragNodeInteraction extends BaseInteraction {
|
|
|
38
32
|
if (hitNodeEdges.length > 0) {
|
|
39
33
|
this.isDrawing = true;
|
|
40
34
|
this.mouseDownNode = null;
|
|
35
|
+
this.addEventListener('mouseup', this.resetState, { once: true });
|
|
41
36
|
}
|
|
42
37
|
else if (hitNodes.length > 0) {
|
|
43
38
|
this.mouseDownNode = hits.nvlTargets.nodes[0] ?? null;
|
|
@@ -62,6 +57,7 @@ export class DragNodeInteraction extends BaseInteraction {
|
|
|
62
57
|
return;
|
|
63
58
|
}
|
|
64
59
|
if (!this.isDragging) {
|
|
60
|
+
this.toggleGlobalTextSelection(false, this.handleBodyMouseUp);
|
|
65
61
|
if (this.moveSelectedNodes) {
|
|
66
62
|
this.callCallbackIfRegistered('onDragStart', this.selectedNodes, evt);
|
|
67
63
|
}
|
|
@@ -89,15 +85,21 @@ export class DragNodeInteraction extends BaseInteraction {
|
|
|
89
85
|
this.callCallbackIfRegistered('onDrag', [this.mouseDownNode.data], evt);
|
|
90
86
|
}
|
|
91
87
|
};
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
handleBodyMouseUp = (evt) => {
|
|
89
|
+
this.toggleGlobalTextSelection(true, this.handleBodyMouseUp);
|
|
90
|
+
if (this.isDragging && this.mouseDownNode !== null) {
|
|
94
91
|
if (this.moveSelectedNodes) {
|
|
95
92
|
this.callCallbackIfRegistered('onDragEnd', this.selectedNodes, evt);
|
|
96
93
|
}
|
|
97
94
|
else {
|
|
98
|
-
this.callCallbackIfRegistered('onDragEnd', [this.mouseDownNode
|
|
95
|
+
this.callCallbackIfRegistered('onDragEnd', [this.mouseDownNode.data], evt);
|
|
99
96
|
}
|
|
100
97
|
}
|
|
98
|
+
if (this.isDragging) {
|
|
99
|
+
this.resetState();
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
resetState = () => {
|
|
101
103
|
this.isDragging = false;
|
|
102
104
|
this.mouseDownNode = null;
|
|
103
105
|
this.isDrawing = false;
|
|
@@ -108,8 +110,8 @@ export class DragNodeInteraction extends BaseInteraction {
|
|
|
108
110
|
* Removes all related event listeners from the container.
|
|
109
111
|
*/
|
|
110
112
|
destroy = () => {
|
|
113
|
+
this.toggleGlobalTextSelection(true, this.handleBodyMouseUp);
|
|
111
114
|
this.removeEventListener('mousedown', this.handleMouseDown);
|
|
112
115
|
this.removeEventListener('mousemove', this.handleMouseMove);
|
|
113
|
-
this.removeEventListener('mouseup', this.handleMouseUp);
|
|
114
116
|
};
|
|
115
117
|
}
|
|
@@ -16,22 +16,19 @@ const DefaultGhostGraphStyling = {
|
|
|
16
16
|
* @internal
|
|
17
17
|
*/
|
|
18
18
|
export class DrawInteraction extends BaseInteraction {
|
|
19
|
-
isMoved;
|
|
20
|
-
isDrawing;
|
|
21
|
-
isDraggingNode;
|
|
22
|
-
mouseDownNode;
|
|
23
|
-
newTempTargetNode;
|
|
24
|
-
newTempRegularRelationshipToNewTempTargetNode;
|
|
25
|
-
newTempRegularRelationshipToExistingNode;
|
|
26
|
-
newTempSelfReferredRelationship;
|
|
27
|
-
newTargetNodeToAdd;
|
|
28
|
-
newRelationshipToAdd;
|
|
29
|
-
mouseOutsideOfNvlArea;
|
|
19
|
+
isMoved = false;
|
|
20
|
+
isDrawing = false;
|
|
21
|
+
isDraggingNode = false;
|
|
22
|
+
mouseDownNode = undefined;
|
|
23
|
+
newTempTargetNode = null;
|
|
24
|
+
newTempRegularRelationshipToNewTempTargetNode = null;
|
|
25
|
+
newTempRegularRelationshipToExistingNode = null;
|
|
26
|
+
newTempSelfReferredRelationship = null;
|
|
27
|
+
newTargetNodeToAdd = null;
|
|
28
|
+
newRelationshipToAdd = null;
|
|
29
|
+
mouseOutsideOfNvlArea = false;
|
|
30
30
|
constructor(nvl, options = {}) {
|
|
31
31
|
super(nvl, options);
|
|
32
|
-
this.isMoved = false;
|
|
33
|
-
this.isDrawing = false;
|
|
34
|
-
this.isDraggingNode = false;
|
|
35
32
|
this.nvlInstance.setLayout('free');
|
|
36
33
|
this.addEventListener('mousemove', this.handleMouseMove, true);
|
|
37
34
|
this.addEventListener('mousedown', this.handleMouseDown, true);
|
|
@@ -30,7 +30,7 @@ export type HoverInteractionCallbacks = {
|
|
|
30
30
|
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/interaction-handlers/#_hoverinteraction Hover Interaction documentation page}.
|
|
31
31
|
*/
|
|
32
32
|
export declare class HoverInteraction extends BaseInteraction<HoverInteractionCallbacks, HoverInteractionOptions> {
|
|
33
|
-
private
|
|
33
|
+
private currentHoveredElementId;
|
|
34
34
|
private currentHoveredElementIsNode;
|
|
35
35
|
private readonly updates;
|
|
36
36
|
constructor(nvl: NVL, options?: HoverInteractionOptions);
|
|
@@ -39,8 +39,8 @@ export declare class HoverInteraction extends BaseInteraction<HoverInteractionCa
|
|
|
39
39
|
* @param {MouseEvent} event - The mouse event
|
|
40
40
|
*/
|
|
41
41
|
handleHover: (event: MouseEvent) => void;
|
|
42
|
+
private updateElementsInNVL;
|
|
42
43
|
private clearUpdates;
|
|
43
|
-
private currentElementNeedsUnHover;
|
|
44
44
|
private unHoverCurrentElement;
|
|
45
45
|
destroy(): void;
|
|
46
46
|
}
|
|
@@ -5,15 +5,14 @@ import { BaseInteraction } from './base';
|
|
|
5
5
|
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/interaction-handlers/#_hoverinteraction Hover Interaction documentation page}.
|
|
6
6
|
*/
|
|
7
7
|
export class HoverInteraction extends BaseInteraction {
|
|
8
|
-
|
|
9
|
-
currentHoveredElementIsNode;
|
|
10
|
-
updates
|
|
8
|
+
currentHoveredElementId = undefined;
|
|
9
|
+
currentHoveredElementIsNode = undefined;
|
|
10
|
+
updates = {
|
|
11
|
+
nodes: [],
|
|
12
|
+
relationships: []
|
|
13
|
+
};
|
|
11
14
|
constructor(nvl, options = { drawShadowOnHover: false }) {
|
|
12
15
|
super(nvl, options);
|
|
13
|
-
this.updates = {
|
|
14
|
-
nodes: [],
|
|
15
|
-
relationships: []
|
|
16
|
-
};
|
|
17
16
|
this.addEventListener('mousemove', this.handleHover, true);
|
|
18
17
|
}
|
|
19
18
|
/**
|
|
@@ -25,27 +24,39 @@ export class HoverInteraction extends BaseInteraction {
|
|
|
25
24
|
const { nodes = [], relationships = [] } = nvlTargets;
|
|
26
25
|
const mainTarget = nodes[0] ?? relationships[0];
|
|
27
26
|
const hoveredElement = mainTarget?.data;
|
|
28
|
-
|
|
27
|
+
const newHoveredElementIsNode = hoveredElement !== undefined && nodes[0] !== undefined;
|
|
28
|
+
const nothingHasBeenHoveredOrUnHovered = this.currentHoveredElementId === undefined && hoveredElement === undefined;
|
|
29
|
+
const newHoveredElementIsTheSameAsCurrent = hoveredElement?.id !== undefined &&
|
|
30
|
+
this.currentHoveredElementId === hoveredElement.id &&
|
|
31
|
+
newHoveredElementIsNode === this.currentHoveredElementIsNode;
|
|
32
|
+
if (nothingHasBeenHoveredOrUnHovered || newHoveredElementIsTheSameAsCurrent) {
|
|
33
|
+
this.callCallbackIfRegistered('onHover', hoveredElement, nvlTargets, event);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const currentElementNeedsUnHover = this.currentHoveredElementId !== undefined && this.currentHoveredElementId !== hoveredElement?.id;
|
|
37
|
+
if (currentElementNeedsUnHover) {
|
|
29
38
|
this.unHoverCurrentElement();
|
|
30
39
|
}
|
|
31
|
-
|
|
32
|
-
if (currentHoveredElementIsNode) {
|
|
40
|
+
if (newHoveredElementIsNode) {
|
|
33
41
|
this.updates.nodes.push({ id: hoveredElement.id, hovered: true });
|
|
34
|
-
this.
|
|
42
|
+
this.currentHoveredElementId = hoveredElement.id;
|
|
35
43
|
this.currentHoveredElementIsNode = true;
|
|
36
44
|
}
|
|
37
|
-
else if (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
this.
|
|
41
|
-
this.currentHoveredElement = hoveredElement;
|
|
45
|
+
else if (hoveredElement !== undefined) {
|
|
46
|
+
const { id } = hoveredElement;
|
|
47
|
+
this.updates.relationships.push({ id, hovered: true });
|
|
48
|
+
this.currentHoveredElementId = hoveredElement.id;
|
|
42
49
|
this.currentHoveredElementIsNode = false;
|
|
43
50
|
}
|
|
44
51
|
else {
|
|
45
|
-
this.
|
|
52
|
+
this.currentHoveredElementId = undefined;
|
|
46
53
|
this.currentHoveredElementIsNode = undefined;
|
|
47
54
|
}
|
|
48
|
-
this.callCallbackIfRegistered('onHover',
|
|
55
|
+
this.callCallbackIfRegistered('onHover', hoveredElement, nvlTargets, event);
|
|
56
|
+
this.updateElementsInNVL();
|
|
57
|
+
this.clearUpdates();
|
|
58
|
+
};
|
|
59
|
+
updateElementsInNVL() {
|
|
49
60
|
// Use pure update API to avoid the previous removed node to be re-added to the scene.
|
|
50
61
|
if (this.currentOptions.drawShadowOnHover === true) {
|
|
51
62
|
const currentNodes = this.nvlInstance.getNodes();
|
|
@@ -53,30 +64,24 @@ export class HoverInteraction extends BaseInteraction {
|
|
|
53
64
|
this.nvlInstance.updateElementsInGraph(this.updates.nodes, this.updates.relationships);
|
|
54
65
|
}
|
|
55
66
|
}
|
|
56
|
-
|
|
57
|
-
};
|
|
67
|
+
}
|
|
58
68
|
clearUpdates() {
|
|
59
69
|
this.updates.nodes = [];
|
|
60
70
|
this.updates.relationships = [];
|
|
61
71
|
}
|
|
62
|
-
currentElementNeedsUnHover(newHoveredElementId) {
|
|
63
|
-
return this.currentHoveredElement !== undefined && this.currentHoveredElement.id !== newHoveredElementId;
|
|
64
|
-
}
|
|
65
72
|
unHoverCurrentElement() {
|
|
66
|
-
if (this.
|
|
73
|
+
if (this.currentHoveredElementId === undefined) {
|
|
67
74
|
return;
|
|
68
75
|
}
|
|
69
76
|
const update = {
|
|
70
|
-
id: this.
|
|
77
|
+
id: this.currentHoveredElementId,
|
|
71
78
|
hovered: false
|
|
72
79
|
};
|
|
73
80
|
if (this.currentHoveredElementIsNode === true) {
|
|
74
81
|
this.updates.nodes.push(update);
|
|
75
82
|
}
|
|
76
83
|
else {
|
|
77
|
-
|
|
78
|
-
const { from, to } = this.currentHoveredElement;
|
|
79
|
-
this.updates.relationships.push({ ...update, from, to });
|
|
84
|
+
this.updates.relationships.push({ ...update });
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
87
|
destroy() {
|
|
@@ -69,8 +69,8 @@ export const checkPointInside = (x, y, vs) => {
|
|
|
69
69
|
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/interaction-handlers/#_lassointeraction Lasso Interaction documentation page}.
|
|
70
70
|
*/
|
|
71
71
|
export class LassoInteraction extends BaseInteraction {
|
|
72
|
-
active;
|
|
73
|
-
points;
|
|
72
|
+
active = false;
|
|
73
|
+
points = [];
|
|
74
74
|
overlayRenderer;
|
|
75
75
|
/**
|
|
76
76
|
* Creates a new instance of the lasso interaction handler.
|
|
@@ -80,12 +80,9 @@ export class LassoInteraction extends BaseInteraction {
|
|
|
80
80
|
constructor(nvl, options = { selectOnRelease: false }) {
|
|
81
81
|
super(nvl, options);
|
|
82
82
|
this.overlayRenderer = new OverlayRenderer(this.containerInstance);
|
|
83
|
-
this.active = false;
|
|
84
|
-
this.points = [];
|
|
85
83
|
this.addEventListener('mousedown', this.handleMouseDown, true);
|
|
86
84
|
this.addEventListener('mousemove', this.handleDrag, true);
|
|
87
85
|
this.addEventListener('mouseup', this.handleMouseUp, true);
|
|
88
|
-
this.addEventListener('mouseleave', this.endLasso, true);
|
|
89
86
|
}
|
|
90
87
|
startLasso = (event) => {
|
|
91
88
|
const hits = this.nvlInstance.getHits(event, ['node'], { hitNodeMarginWidth: NODE_EDGE_WIDTH });
|
|
@@ -95,6 +92,7 @@ export class LassoInteraction extends BaseInteraction {
|
|
|
95
92
|
else {
|
|
96
93
|
this.active = true;
|
|
97
94
|
this.points = [getCanvasPosition(this.containerInstance, event)];
|
|
95
|
+
this.toggleGlobalTextSelection(false, this.endLasso);
|
|
98
96
|
this.callCallbackIfRegistered('onLassoStarted', event);
|
|
99
97
|
if (this.currentOptions.selectOnRelease === true) {
|
|
100
98
|
this.nvlInstance.deselectAll();
|
|
@@ -155,6 +153,7 @@ export class LassoInteraction extends BaseInteraction {
|
|
|
155
153
|
return;
|
|
156
154
|
}
|
|
157
155
|
this.active = false;
|
|
156
|
+
this.toggleGlobalTextSelection(true, this.endLasso);
|
|
158
157
|
const pointArrays = this.points.map((p) => [p.x, p.y]);
|
|
159
158
|
const hasCrossings = checkIntersection(pointArrays);
|
|
160
159
|
const hull = (hasCrossings ? concaveman(pointArrays, 2) : pointArrays)
|
|
@@ -172,10 +171,10 @@ export class LassoInteraction extends BaseInteraction {
|
|
|
172
171
|
* Removes all related event listeners and the overlay renderer for the box.
|
|
173
172
|
*/
|
|
174
173
|
destroy() {
|
|
174
|
+
this.toggleGlobalTextSelection(true, this.endLasso);
|
|
175
175
|
this.removeEventListener('mousedown', this.handleMouseDown, true);
|
|
176
176
|
this.removeEventListener('mousemove', this.handleDrag, true);
|
|
177
|
-
this.removeEventListener('mouseup', this.
|
|
178
|
-
this.removeEventListener('mouseleave', this.endLasso, true);
|
|
177
|
+
this.removeEventListener('mouseup', this.handleMouseUp, true);
|
|
179
178
|
this.overlayRenderer.destroy();
|
|
180
179
|
}
|
|
181
180
|
}
|
|
@@ -33,6 +33,7 @@ export declare class PanInteraction extends BaseInteraction<PanInteractionCallba
|
|
|
33
33
|
private mousePosition;
|
|
34
34
|
private targets;
|
|
35
35
|
private shouldPan;
|
|
36
|
+
private isPanning;
|
|
36
37
|
/**
|
|
37
38
|
* Creates a new instance of the pan interaction handler.
|
|
38
39
|
* @param nvl - The NVL instance to attach the interaction handler to
|
|
@@ -7,18 +7,16 @@ import { BaseInteraction } from './base';
|
|
|
7
7
|
* For examples, head to the {@link https://neo4j.com/docs/nvl/current/interaction-handlers/#_paninteraction Pan Interaction documentation page}.
|
|
8
8
|
*/
|
|
9
9
|
export class PanInteraction extends BaseInteraction {
|
|
10
|
-
mousePosition;
|
|
11
|
-
targets;
|
|
12
|
-
shouldPan;
|
|
10
|
+
mousePosition = { x: 0, y: 0 };
|
|
11
|
+
targets = [];
|
|
12
|
+
shouldPan = false;
|
|
13
|
+
isPanning = false;
|
|
13
14
|
/**
|
|
14
15
|
* Creates a new instance of the pan interaction handler.
|
|
15
16
|
* @param nvl - The NVL instance to attach the interaction handler to
|
|
16
17
|
*/
|
|
17
18
|
constructor(nvl, options = { excludeNodeMargin: false }) {
|
|
18
19
|
super(nvl, options);
|
|
19
|
-
this.mousePosition = { x: 0, y: 0 };
|
|
20
|
-
this.targets = [];
|
|
21
|
-
this.shouldPan = false;
|
|
22
20
|
this.addEventListener('mousedown', this.handleMouseDown, true);
|
|
23
21
|
this.addEventListener('mousemove', this.handleMouseMove, true);
|
|
24
22
|
this.addEventListener('mouseup', this.handleMouseUp, true);
|
|
@@ -61,6 +59,10 @@ export class PanInteraction extends BaseInteraction {
|
|
|
61
59
|
if (!this.shouldPan || evt.buttons !== 1) {
|
|
62
60
|
return;
|
|
63
61
|
}
|
|
62
|
+
if (!this.isPanning) {
|
|
63
|
+
this.toggleGlobalTextSelection(false, this.handleMouseUp);
|
|
64
|
+
this.isPanning = true;
|
|
65
|
+
}
|
|
64
66
|
const zoom = this.nvlInstance.getScale();
|
|
65
67
|
const { x, y } = this.nvlInstance.getPan();
|
|
66
68
|
const dx = ((evt.clientX - this.mousePosition.x) / zoom) * window.devicePixelRatio;
|
|
@@ -70,12 +72,17 @@ export class PanInteraction extends BaseInteraction {
|
|
|
70
72
|
this.mousePosition = { x: evt.clientX, y: evt.clientY };
|
|
71
73
|
};
|
|
72
74
|
handleMouseUp = () => {
|
|
75
|
+
if (this.isPanning) {
|
|
76
|
+
this.toggleGlobalTextSelection(true, this.handleMouseUp);
|
|
77
|
+
this.isPanning = false;
|
|
78
|
+
}
|
|
73
79
|
this.shouldPan = false;
|
|
74
80
|
};
|
|
75
81
|
/**
|
|
76
82
|
* Removes the related event listeners from the canvas.
|
|
77
83
|
*/
|
|
78
84
|
destroy() {
|
|
85
|
+
this.toggleGlobalTextSelection(true, this.handleMouseUp);
|
|
79
86
|
this.removeEventListener('mousedown', this.handleMouseDown, true);
|
|
80
87
|
this.removeEventListener('mousemove', this.handleMouseMove, true);
|
|
81
88
|
this.removeEventListener('mouseup', this.handleMouseUp, true);
|
|
@@ -34,6 +34,11 @@ export declare class ZoomInteraction extends BaseInteraction<ZoomInteractionCall
|
|
|
34
34
|
* The throttle is set to 25ms.
|
|
35
35
|
*/
|
|
36
36
|
private throttledZoom;
|
|
37
|
+
/**
|
|
38
|
+
* The function to be called on mouse wheel event on the canvas.
|
|
39
|
+
* @param evt - The mouse wheel event
|
|
40
|
+
* @note If the ctrl key or meta key is pressed, it does not zoom to avoid conflicts with default browser behavior.
|
|
41
|
+
*/
|
|
37
42
|
private handleWheel;
|
|
38
43
|
/**
|
|
39
44
|
* Removes the relevant event listeners from the canvas.
|
|
@@ -29,7 +29,8 @@ export class ZoomInteraction extends BaseInteraction {
|
|
|
29
29
|
throttledZoom = throttle((event) => {
|
|
30
30
|
const zoom = this.nvlInstance.getScale();
|
|
31
31
|
const { x, y } = this.nvlInstance.getPan();
|
|
32
|
-
const
|
|
32
|
+
const baseSpeed = event.deltaY / 500;
|
|
33
|
+
const factor = zoom >= 1 ? baseSpeed * Math.pow(zoom, 1) : baseSpeed;
|
|
33
34
|
const newZoomTarget = zoom - factor * Math.min(1, zoom);
|
|
34
35
|
const offs = getCanvasCenterOffset(this.containerInstance, event);
|
|
35
36
|
const newPanX = x + (offs.x / zoom - offs.x / newZoomTarget);
|
|
@@ -37,7 +38,15 @@ export class ZoomInteraction extends BaseInteraction {
|
|
|
37
38
|
this.nvlInstance.setZoomAndPan(newZoomTarget, newPanX, newPanY);
|
|
38
39
|
this.callCallbackIfRegistered('onZoom', newZoomTarget, event);
|
|
39
40
|
}, 25, { leading: true });
|
|
41
|
+
/**
|
|
42
|
+
* The function to be called on mouse wheel event on the canvas.
|
|
43
|
+
* @param evt - The mouse wheel event
|
|
44
|
+
* @note If the ctrl key or meta key is pressed, it does not zoom to avoid conflicts with default browser behavior.
|
|
45
|
+
*/
|
|
40
46
|
handleWheel = (evt) => {
|
|
47
|
+
if (evt.ctrlKey || evt.metaKey) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
41
50
|
evt.preventDefault();
|
|
42
51
|
this.throttledZoom(evt);
|
|
43
52
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neo4j-nvl/interaction-handlers",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"license": "SEE LICENSE IN 'LICENSE.txt'",
|
|
5
5
|
"homepage": "https://neo4j.com/docs/nvl/current/",
|
|
6
6
|
"description": "Interaction handlers for the Neo4j Visualization Library",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"eslint": "yarn global:eslint ./src/"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@neo4j-nvl/base": "0.3.
|
|
30
|
+
"@neo4j-nvl/base": "0.3.9",
|
|
31
31
|
"concaveman": "^1.2.1",
|
|
32
32
|
"lodash": "4.17.21"
|
|
33
33
|
},
|