@neo4j-nvl/interaction-handlers 0.2.1
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/Neo4j Early Access Agreement - Visualization Library.pdf +0 -0
- package/README.md +1 -0
- package/lib/constants.d.ts +1 -0
- package/lib/constants.js +1 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.js +8 -0
- package/lib/interaction-handlers/base.d.ts +54 -0
- package/lib/interaction-handlers/base.js +110 -0
- package/lib/interaction-handlers/click-interaction.d.ts +109 -0
- package/lib/interaction-handlers/click-interaction.js +162 -0
- package/lib/interaction-handlers/drag-node-interaction.d.ts +46 -0
- package/lib/interaction-handlers/drag-node-interaction.js +137 -0
- package/lib/interaction-handlers/draw-interaction.d.ts +30 -0
- package/lib/interaction-handlers/draw-interaction.js +257 -0
- package/lib/interaction-handlers/hover-interaction.d.ts +57 -0
- package/lib/interaction-handlers/hover-interaction.js +113 -0
- package/lib/interaction-handlers/multi-select-interaction.d.ts +58 -0
- package/lib/interaction-handlers/multi-select-interaction.js +133 -0
- package/lib/interaction-handlers/pan-interaction.d.ts +58 -0
- package/lib/interaction-handlers/pan-interaction.js +115 -0
- package/lib/interaction-handlers/utils.d.ts +1 -0
- package/lib/interaction-handlers/utils.js +1 -0
- package/lib/interaction-handlers/zoom-interaction.d.ts +52 -0
- package/lib/interaction-handlers/zoom-interaction.js +91 -0
- package/lib/overlay-renderer/overlay-renderer.d.ts +12 -0
- package/lib/overlay-renderer/overlay-renderer.js +111 -0
- package/package.json +36 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { NVL, Node, Relationship } from '@neo4j-nvl/core';
|
|
2
|
+
import { BaseInteraction } from './base';
|
|
3
|
+
/**
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export declare class DrawInteraction extends BaseInteraction<{
|
|
7
|
+
onDrawEnd: (newRelationshipToAdd: Relationship | null, newTargetNodeToAdd: Node | null) => void;
|
|
8
|
+
}> {
|
|
9
|
+
private overlayRenderer;
|
|
10
|
+
private isMoved;
|
|
11
|
+
private isDrawing;
|
|
12
|
+
private isDraggingNode;
|
|
13
|
+
private mouseDownNode;
|
|
14
|
+
private newTempTargetNode;
|
|
15
|
+
private newTempRegularRelationshipToNewTempTargetNode;
|
|
16
|
+
private newTempRegularRelationshipToExistingNode;
|
|
17
|
+
private newTempSelfReferredRelationship;
|
|
18
|
+
private newTargetNodeToAdd;
|
|
19
|
+
private newRelationshipToAdd;
|
|
20
|
+
constructor(nvl: NVL);
|
|
21
|
+
private handleMouseMove;
|
|
22
|
+
private handleWheel;
|
|
23
|
+
private setNewRegularRelationship;
|
|
24
|
+
private setNewRegularRelationshipToNewTempTargetNode;
|
|
25
|
+
private setNewRegularRelationshipToExistingNode;
|
|
26
|
+
private setNewSelfReferredRelationship;
|
|
27
|
+
private handleMouseDown;
|
|
28
|
+
private handleMouseUp;
|
|
29
|
+
destroy: () => void;
|
|
30
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { OverlayRenderer } from '../overlay-renderer/overlay-renderer';
|
|
2
|
+
import { generateUniqueId } from './utils';
|
|
3
|
+
import { NODE_EDGE_WIDTH } from '../constants';
|
|
4
|
+
import { BaseInteraction } from './base';
|
|
5
|
+
/**
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export class DrawInteraction extends BaseInteraction {
|
|
9
|
+
constructor(nvl) {
|
|
10
|
+
super(nvl);
|
|
11
|
+
Object.defineProperty(this, "overlayRenderer", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: void 0
|
|
16
|
+
});
|
|
17
|
+
Object.defineProperty(this, "isMoved", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true,
|
|
21
|
+
value: void 0
|
|
22
|
+
});
|
|
23
|
+
Object.defineProperty(this, "isDrawing", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
writable: true,
|
|
27
|
+
value: void 0
|
|
28
|
+
});
|
|
29
|
+
Object.defineProperty(this, "isDraggingNode", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
configurable: true,
|
|
32
|
+
writable: true,
|
|
33
|
+
value: void 0
|
|
34
|
+
});
|
|
35
|
+
Object.defineProperty(this, "mouseDownNode", {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
configurable: true,
|
|
38
|
+
writable: true,
|
|
39
|
+
value: void 0
|
|
40
|
+
});
|
|
41
|
+
Object.defineProperty(this, "newTempTargetNode", {
|
|
42
|
+
enumerable: true,
|
|
43
|
+
configurable: true,
|
|
44
|
+
writable: true,
|
|
45
|
+
value: void 0
|
|
46
|
+
});
|
|
47
|
+
Object.defineProperty(this, "newTempRegularRelationshipToNewTempTargetNode", {
|
|
48
|
+
enumerable: true,
|
|
49
|
+
configurable: true,
|
|
50
|
+
writable: true,
|
|
51
|
+
value: void 0
|
|
52
|
+
});
|
|
53
|
+
Object.defineProperty(this, "newTempRegularRelationshipToExistingNode", {
|
|
54
|
+
enumerable: true,
|
|
55
|
+
configurable: true,
|
|
56
|
+
writable: true,
|
|
57
|
+
value: void 0
|
|
58
|
+
});
|
|
59
|
+
Object.defineProperty(this, "newTempSelfReferredRelationship", {
|
|
60
|
+
enumerable: true,
|
|
61
|
+
configurable: true,
|
|
62
|
+
writable: true,
|
|
63
|
+
value: void 0
|
|
64
|
+
});
|
|
65
|
+
Object.defineProperty(this, "newTargetNodeToAdd", {
|
|
66
|
+
enumerable: true,
|
|
67
|
+
configurable: true,
|
|
68
|
+
writable: true,
|
|
69
|
+
value: void 0
|
|
70
|
+
});
|
|
71
|
+
Object.defineProperty(this, "newRelationshipToAdd", {
|
|
72
|
+
enumerable: true,
|
|
73
|
+
configurable: true,
|
|
74
|
+
writable: true,
|
|
75
|
+
value: void 0
|
|
76
|
+
});
|
|
77
|
+
Object.defineProperty(this, "handleMouseMove", {
|
|
78
|
+
enumerable: true,
|
|
79
|
+
configurable: true,
|
|
80
|
+
writable: true,
|
|
81
|
+
value: (event) => {
|
|
82
|
+
var _a, _b, _c, _d, _e, _f;
|
|
83
|
+
this.isMoved = true;
|
|
84
|
+
if (this.isDrawing) {
|
|
85
|
+
this.overlayRenderer.clear();
|
|
86
|
+
// Rename to getMousePosition in world coordinate
|
|
87
|
+
const pos = this.nvlInstance.getMousePosition(event);
|
|
88
|
+
const hits = this.nvlInstance.getHits(event, ['node']);
|
|
89
|
+
const hitNode = hits.nvlTargets.nodes.filter(n => n.data.id !== this.newTempTargetNode.id)[0];
|
|
90
|
+
const targetNode = hitNode ? { id: hitNode.data.id, x: hitNode.targetCoordinates.x, y: hitNode.targetCoordinates.y, size: hitNode.data.size } : undefined;
|
|
91
|
+
// ArrowBundler has race-condition to update the dataset, if we remove the node with the same ID after mouse releases, the same node will be removed then be added again, which will cause the issue.
|
|
92
|
+
// So always make sure the new node id to be added to the dataset after mouse releasing is a new id.
|
|
93
|
+
const newTargetNodeId = generateUniqueId(13);
|
|
94
|
+
const newTargetNode = targetNode ? null : { id: newTargetNodeId, size: 25, selected: false, x: pos.x, y: pos.y };
|
|
95
|
+
const newRelationshipId = generateUniqueId(13);
|
|
96
|
+
const relationship = { id: newRelationshipId, from: this.mouseDownNode.data.id, to: targetNode ? targetNode.id : newTargetNodeId, captions: [{ value: 'TEST' }] };
|
|
97
|
+
let x = pos.x;
|
|
98
|
+
let y = pos.y;
|
|
99
|
+
let size = 25;
|
|
100
|
+
if (hitNode) {
|
|
101
|
+
x = hitNode.targetCoordinates.x;
|
|
102
|
+
y = hitNode.targetCoordinates.y;
|
|
103
|
+
size = hitNode.data.size;
|
|
104
|
+
if (hitNode.data.id === this.mouseDownNode.data.id && !this.newTempSelfReferredRelationship) {
|
|
105
|
+
this.nvlInstance.removeRelationshipsWithIds([(_a = this.newTempRegularRelationshipToNewTempTargetNode) === null || _a === void 0 ? void 0 : _a.id, (_b = this.newTempRegularRelationshipToExistingNode) === null || _b === void 0 ? void 0 : _b.id]);
|
|
106
|
+
this.newTempRegularRelationshipToNewTempTargetNode = null;
|
|
107
|
+
this.newTempRegularRelationshipToExistingNode = null;
|
|
108
|
+
this.setNewSelfReferredRelationship();
|
|
109
|
+
this.nvlInstance.addElementsToGraph([], [this.newTempSelfReferredRelationship]);
|
|
110
|
+
}
|
|
111
|
+
else if (hitNode.data.id !== this.mouseDownNode.data.id && !this.newTempRegularRelationshipToExistingNode) {
|
|
112
|
+
this.nvlInstance.removeRelationshipsWithIds([(_c = this.newTempSelfReferredRelationship) === null || _c === void 0 ? void 0 : _c.id, (_d = this.newTempRegularRelationshipToNewTempTargetNode) === null || _d === void 0 ? void 0 : _d.id]);
|
|
113
|
+
this.newTempSelfReferredRelationship = null;
|
|
114
|
+
this.newTempRegularRelationshipToNewTempTargetNode = null;
|
|
115
|
+
this.setNewRegularRelationshipToExistingNode(hitNode.data.id);
|
|
116
|
+
this.nvlInstance.addElementsToGraph([], [this.newTempRegularRelationshipToExistingNode]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (!this.newTempRegularRelationshipToNewTempTargetNode) {
|
|
120
|
+
this.nvlInstance.removeRelationshipsWithIds([(_e = this.newTempSelfReferredRelationship) === null || _e === void 0 ? void 0 : _e.id, (_f = this.newTempRegularRelationshipToExistingNode) === null || _f === void 0 ? void 0 : _f.id]);
|
|
121
|
+
this.newTempSelfReferredRelationship = null;
|
|
122
|
+
this.newTempRegularRelationshipToExistingNode = null;
|
|
123
|
+
this.setNewRegularRelationshipToNewTempTargetNode();
|
|
124
|
+
this.nvlInstance.addElementsToGraph([], [this.newTempRegularRelationshipToNewTempTargetNode]);
|
|
125
|
+
}
|
|
126
|
+
this.nvlInstance.setNodePositions([{ id: this.newTempTargetNode.id, x, y }]);
|
|
127
|
+
this.nvlInstance.updateElementsInGraph([{ id: this.newTempTargetNode.id, x, y, size }], []);
|
|
128
|
+
this.newRelationshipToAdd = relationship;
|
|
129
|
+
this.newTargetNodeToAdd = newTargetNode;
|
|
130
|
+
}
|
|
131
|
+
else if (!this.isDraggingNode) {
|
|
132
|
+
this.newRelationshipToAdd = null;
|
|
133
|
+
this.newTargetNodeToAdd = null;
|
|
134
|
+
const hits = this.nvlInstance.getHits(event, ['node'], { hitNodeMarginWidth: NODE_EDGE_WIDTH });
|
|
135
|
+
const hitNodeEdges = hits.nvlTargets.nodes.filter(node => !node.insideNode);
|
|
136
|
+
if (hitNodeEdges.length > 0) {
|
|
137
|
+
const node = hitNodeEdges[0];
|
|
138
|
+
const zoom = this.nvlInstance.getScale();
|
|
139
|
+
const { x, y } = this.nvlInstance.getPan();
|
|
140
|
+
this.overlayRenderer.drawNodeRing(node.targetCoordinates.x, node.targetCoordinates.y, node.data.size, zoom, x, y);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
this.overlayRenderer.clear();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
Object.defineProperty(this, "handleWheel", {
|
|
149
|
+
enumerable: true,
|
|
150
|
+
configurable: true,
|
|
151
|
+
writable: true,
|
|
152
|
+
value: () => {
|
|
153
|
+
this.overlayRenderer.clear();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
Object.defineProperty(this, "handleMouseDown", {
|
|
157
|
+
enumerable: true,
|
|
158
|
+
configurable: true,
|
|
159
|
+
writable: true,
|
|
160
|
+
value: (event) => {
|
|
161
|
+
this.isMoved = false;
|
|
162
|
+
this.newRelationshipToAdd = null;
|
|
163
|
+
this.newTargetNodeToAdd = null;
|
|
164
|
+
const hits = this.nvlInstance.getHits(event, ['node'], { hitNodeMarginWidth: NODE_EDGE_WIDTH });
|
|
165
|
+
const hitNodes = hits.nvlTargets.nodes.filter(node => node.insideNode);
|
|
166
|
+
const hitNodeEdges = hits.nvlTargets.nodes.filter(node => !node.insideNode);
|
|
167
|
+
if (hitNodes.length > 0) {
|
|
168
|
+
this.isDraggingNode = true;
|
|
169
|
+
this.isDrawing = false;
|
|
170
|
+
}
|
|
171
|
+
else if (hitNodeEdges.length > 0) {
|
|
172
|
+
this.isDrawing = true;
|
|
173
|
+
this.isDraggingNode = false;
|
|
174
|
+
this.mouseDownNode = hitNodeEdges[0];
|
|
175
|
+
const pos = this.nvlInstance.getMousePosition(event);
|
|
176
|
+
this.newTempTargetNode = { id: generateUniqueId(13), size: 25, selected: false, x: pos.x, y: pos.y, color: 'black' };
|
|
177
|
+
this.setNewRegularRelationshipToNewTempTargetNode();
|
|
178
|
+
this.nvlInstance.addAndUpdateElementsInGraph([this.newTempTargetNode], [this.newTempRegularRelationshipToNewTempTargetNode]);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
this.mouseDownNode = undefined;
|
|
182
|
+
this.isDrawing = false;
|
|
183
|
+
this.isDraggingNode = false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
Object.defineProperty(this, "handleMouseUp", {
|
|
188
|
+
enumerable: true,
|
|
189
|
+
configurable: true,
|
|
190
|
+
writable: true,
|
|
191
|
+
value: () => {
|
|
192
|
+
var _a, _b, _c, _d;
|
|
193
|
+
this.nvlInstance.removeRelationshipsWithIds([(_a = this.newTempRegularRelationshipToNewTempTargetNode) === null || _a === void 0 ? void 0 : _a.id, (_b = this.newTempRegularRelationshipToExistingNode) === null || _b === void 0 ? void 0 : _b.id, (_c = this.newTempSelfReferredRelationship) === null || _c === void 0 ? void 0 : _c.id]);
|
|
194
|
+
this.nvlInstance.removeNodesWithIds([(_d = this.newTempTargetNode) === null || _d === void 0 ? void 0 : _d.id]);
|
|
195
|
+
if (this.isDrawing && this.isMoved) {
|
|
196
|
+
this.newTargetNodeToAdd && this.nvlInstance.setNodePositions([this.newTargetNodeToAdd]);
|
|
197
|
+
// Avoid relationship glitch
|
|
198
|
+
this.nvlInstance.addAndUpdateElementsInGraph(this.newTargetNodeToAdd ? [{ id: this.newTargetNodeToAdd.id }] : [], [this.newRelationshipToAdd]);
|
|
199
|
+
this.callCallbackIfRegistered('onDrawEnd', this.newRelationshipToAdd, this.newTargetNodeToAdd);
|
|
200
|
+
}
|
|
201
|
+
this.newTempTargetNode = null;
|
|
202
|
+
this.newTempRegularRelationshipToNewTempTargetNode = null;
|
|
203
|
+
this.newTempRegularRelationshipToExistingNode = null;
|
|
204
|
+
this.newTempSelfReferredRelationship = null;
|
|
205
|
+
this.isMoved = false;
|
|
206
|
+
this.isDrawing = false;
|
|
207
|
+
this.isDraggingNode = false;
|
|
208
|
+
this.overlayRenderer.clear();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
Object.defineProperty(this, "destroy", {
|
|
212
|
+
enumerable: true,
|
|
213
|
+
configurable: true,
|
|
214
|
+
writable: true,
|
|
215
|
+
value: () => {
|
|
216
|
+
this.removeEventListener('mousemove', this.handleMouseMove, true);
|
|
217
|
+
this.removeEventListener('wheel', this.handleWheel, true);
|
|
218
|
+
this.removeEventListener('mousedown', this.handleMouseDown, true);
|
|
219
|
+
this.removeEventListener('mouseup', this.handleMouseUp, true);
|
|
220
|
+
this.overlayRenderer.destroy();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
this.overlayRenderer = new OverlayRenderer(this.containerInstance);
|
|
224
|
+
this.isMoved = false;
|
|
225
|
+
this.isDrawing = false;
|
|
226
|
+
this.isDraggingNode = false;
|
|
227
|
+
this.nvlInstance.setLayout('free');
|
|
228
|
+
this.addEventListener('mousemove', this.handleMouseMove, true);
|
|
229
|
+
this.addEventListener('wheel', this.handleWheel, true);
|
|
230
|
+
this.addEventListener('mousedown', this.handleMouseDown, true);
|
|
231
|
+
this.addEventListener('mouseup', this.handleMouseUp, true);
|
|
232
|
+
}
|
|
233
|
+
setNewRegularRelationship(targetId) {
|
|
234
|
+
if (!this.mouseDownNode) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
return { id: generateUniqueId(13), from: this.mouseDownNode.data.id, to: targetId, captions: [{ value: 'TEST' }], color: 'red' };
|
|
238
|
+
}
|
|
239
|
+
setNewRegularRelationshipToNewTempTargetNode() {
|
|
240
|
+
if (!this.mouseDownNode || !this.newTempTargetNode) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
this.newTempRegularRelationshipToNewTempTargetNode = this.setNewRegularRelationship(this.newTempTargetNode.id);
|
|
244
|
+
}
|
|
245
|
+
setNewRegularRelationshipToExistingNode(targetId) {
|
|
246
|
+
if (!this.mouseDownNode) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
this.newTempRegularRelationshipToExistingNode = this.setNewRegularRelationship(targetId);
|
|
250
|
+
}
|
|
251
|
+
setNewSelfReferredRelationship() {
|
|
252
|
+
if (!this.mouseDownNode) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
this.newTempSelfReferredRelationship = { id: generateUniqueId(13), from: this.mouseDownNode.data.id, to: this.mouseDownNode.data.id, captions: [{ value: 'TEST' }], color: 'red' };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { HitTargets, NVL, Node, Relationship } from '@neo4j-nvl/core';
|
|
2
|
+
import { BaseInteraction } from './base';
|
|
3
|
+
/**
|
|
4
|
+
* Options for the click interaction handler to customize its behavior.
|
|
5
|
+
*/
|
|
6
|
+
export type HoverInteractionOptions = {
|
|
7
|
+
/**
|
|
8
|
+
* Whether to draw a shadow on hover
|
|
9
|
+
* @default false
|
|
10
|
+
*/
|
|
11
|
+
drawShadowOnHover?: boolean;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Callbacks for the hover interaction handler.
|
|
15
|
+
*/
|
|
16
|
+
export type HoverInteractionCallbacks = {
|
|
17
|
+
/**
|
|
18
|
+
* Called when a node or relationship is hovered
|
|
19
|
+
* @param element - The node or relationship that was hovered
|
|
20
|
+
* @param hitElements - All elements that were hit by the hover
|
|
21
|
+
* @param event - The original mouse event
|
|
22
|
+
* @note This callback is called every time the mouse moves,
|
|
23
|
+
* even when no element is hovered or the current element is already hovered.
|
|
24
|
+
*/
|
|
25
|
+
onHover: (element: Node | Relationship, hitElements: HitTargets, event: MouseEvent) => void;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Interaction handler for hovering nodes and relationships.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```js
|
|
32
|
+
* const nvl = new NVL(container, nodes, relationships)
|
|
33
|
+
* const hoverInteraction = new HoverInteraction(nvl)
|
|
34
|
+
*
|
|
35
|
+
* hoverInteraction.updateCallback('onHover', (element, hitElements, event) => {
|
|
36
|
+
* console.log('Hovered element:', element)
|
|
37
|
+
* console.log('Hit elements:', hitElements)
|
|
38
|
+
* console.log('Mouse event:', event)
|
|
39
|
+
* })
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare class HoverInteraction extends BaseInteraction<HoverInteractionCallbacks> {
|
|
43
|
+
private currentHoveredElement;
|
|
44
|
+
private currentHoveredElementIsNode;
|
|
45
|
+
private readonly updates;
|
|
46
|
+
private readonly options;
|
|
47
|
+
constructor(nvl: NVL, options?: HoverInteractionOptions);
|
|
48
|
+
/**
|
|
49
|
+
* Handle mouse hover events
|
|
50
|
+
* @param {MouseEvent} event - The mouse event
|
|
51
|
+
*/
|
|
52
|
+
handleHover: (event: MouseEvent) => void;
|
|
53
|
+
private clearUpdates;
|
|
54
|
+
private currentElementNeedsUnHover;
|
|
55
|
+
private unHoverCurrentElement;
|
|
56
|
+
destroy(): void;
|
|
57
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { BaseInteraction } from './base';
|
|
2
|
+
/**
|
|
3
|
+
* Interaction handler for hovering nodes and relationships.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```js
|
|
7
|
+
* const nvl = new NVL(container, nodes, relationships)
|
|
8
|
+
* const hoverInteraction = new HoverInteraction(nvl)
|
|
9
|
+
*
|
|
10
|
+
* hoverInteraction.updateCallback('onHover', (element, hitElements, event) => {
|
|
11
|
+
* console.log('Hovered element:', element)
|
|
12
|
+
* console.log('Hit elements:', hitElements)
|
|
13
|
+
* console.log('Mouse event:', event)
|
|
14
|
+
* })
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export class HoverInteraction extends BaseInteraction {
|
|
18
|
+
constructor(nvl, options = { drawShadowOnHover: false }) {
|
|
19
|
+
super(nvl);
|
|
20
|
+
Object.defineProperty(this, "currentHoveredElement", {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
configurable: true,
|
|
23
|
+
writable: true,
|
|
24
|
+
value: void 0
|
|
25
|
+
});
|
|
26
|
+
Object.defineProperty(this, "currentHoveredElementIsNode", {
|
|
27
|
+
enumerable: true,
|
|
28
|
+
configurable: true,
|
|
29
|
+
writable: true,
|
|
30
|
+
value: void 0
|
|
31
|
+
});
|
|
32
|
+
Object.defineProperty(this, "updates", {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
writable: true,
|
|
36
|
+
value: void 0
|
|
37
|
+
});
|
|
38
|
+
Object.defineProperty(this, "options", {
|
|
39
|
+
enumerable: true,
|
|
40
|
+
configurable: true,
|
|
41
|
+
writable: true,
|
|
42
|
+
value: void 0
|
|
43
|
+
});
|
|
44
|
+
/**
|
|
45
|
+
* Handle mouse hover events
|
|
46
|
+
* @param {MouseEvent} event - The mouse event
|
|
47
|
+
*/
|
|
48
|
+
Object.defineProperty(this, "handleHover", {
|
|
49
|
+
enumerable: true,
|
|
50
|
+
configurable: true,
|
|
51
|
+
writable: true,
|
|
52
|
+
value: (event) => {
|
|
53
|
+
const { nvlTargets } = this.nvlInstance.getHits(event);
|
|
54
|
+
const { nodes = [], relationships = [] } = nvlTargets;
|
|
55
|
+
const mainTarget = nodes[0] || relationships[0];
|
|
56
|
+
const hoveredElement = mainTarget === null || mainTarget === void 0 ? void 0 : mainTarget.data;
|
|
57
|
+
if (this.currentElementNeedsUnHover(hoveredElement === null || hoveredElement === void 0 ? void 0 : hoveredElement.id)) {
|
|
58
|
+
this.unHoverCurrentElement();
|
|
59
|
+
}
|
|
60
|
+
const currentHoveredElementIsNode = hoveredElement && nodes[0] !== undefined;
|
|
61
|
+
if (currentHoveredElementIsNode) {
|
|
62
|
+
this.updates.nodes.push({ id: hoveredElement.id, hovered: true });
|
|
63
|
+
this.currentHoveredElement = hoveredElement;
|
|
64
|
+
this.currentHoveredElementIsNode = true;
|
|
65
|
+
}
|
|
66
|
+
else if (mainTarget) {
|
|
67
|
+
const { id, from, to } = hoveredElement;
|
|
68
|
+
this.updates.relationships.push({ id, from, to, hovered: true });
|
|
69
|
+
this.currentHoveredElement = hoveredElement;
|
|
70
|
+
this.currentHoveredElementIsNode = false;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
this.currentHoveredElement = undefined;
|
|
74
|
+
this.currentHoveredElementIsNode = undefined;
|
|
75
|
+
}
|
|
76
|
+
this.callCallbackIfRegistered('onHover', this.currentHoveredElement, nvlTargets, event);
|
|
77
|
+
// Use pure update API to avoid the previous removed node to be re-added to the scene.
|
|
78
|
+
this.options.drawShadowOnHover &&
|
|
79
|
+
this.nvlInstance.updateElementsInGraph(this.updates.nodes, this.updates.relationships);
|
|
80
|
+
this.clearUpdates();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
this.updates = {
|
|
84
|
+
nodes: [],
|
|
85
|
+
relationships: []
|
|
86
|
+
};
|
|
87
|
+
this.options = options;
|
|
88
|
+
this.addEventListener('mousemove', this.handleHover, true);
|
|
89
|
+
}
|
|
90
|
+
clearUpdates() {
|
|
91
|
+
this.updates.nodes = [];
|
|
92
|
+
this.updates.relationships = [];
|
|
93
|
+
}
|
|
94
|
+
currentElementNeedsUnHover(newHoveredElementId) {
|
|
95
|
+
return this.currentHoveredElement !== undefined && this.currentHoveredElement.id !== newHoveredElementId;
|
|
96
|
+
}
|
|
97
|
+
unHoverCurrentElement() {
|
|
98
|
+
const update = {
|
|
99
|
+
id: this.currentHoveredElement.id,
|
|
100
|
+
hovered: false
|
|
101
|
+
};
|
|
102
|
+
if (this.currentHoveredElementIsNode) {
|
|
103
|
+
this.updates.nodes.push(update);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
const { from, to } = this.currentHoveredElement;
|
|
107
|
+
this.updates.relationships.push(Object.assign(Object.assign({}, update), { from, to }));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
destroy() {
|
|
111
|
+
this.removeEventListener('mousemove', this.handleHover, true);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { NVL, Node, Relationship } from '@neo4j-nvl/core';
|
|
2
|
+
import { BaseInteraction } from './base';
|
|
3
|
+
/**
|
|
4
|
+
* Options for the multi-select interaction handler to customize its behavior.
|
|
5
|
+
*/
|
|
6
|
+
export type MultiSelectInteractionOptions = {
|
|
7
|
+
/**
|
|
8
|
+
* If true, the selection will be applied when the mouse is released.
|
|
9
|
+
* @default false
|
|
10
|
+
*/
|
|
11
|
+
selectOnRelease?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type MultiSelectInteractionCallbacks = {
|
|
14
|
+
/**
|
|
15
|
+
* Called after once the user releases the mouse after multi-selecting.
|
|
16
|
+
* @param nodes - The nodes that were selected
|
|
17
|
+
* @param rels - The relationships that were selected
|
|
18
|
+
*/
|
|
19
|
+
onMultiSelect: ({ nodes, rels }: {
|
|
20
|
+
nodes: Node[];
|
|
21
|
+
rels: Relationship[];
|
|
22
|
+
}) => void;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Interaction handler for multi-selecting nodes and relationships.
|
|
26
|
+
* When dragging, draws a box on the canvas and selects all
|
|
27
|
+
* nodes and relationships that are inside the box.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```js
|
|
31
|
+
* const nvl = new NVL(container, nodes, relationships)
|
|
32
|
+
* const multiSelectInteraction = new MultiSelectInteraction(nvl)
|
|
33
|
+
*
|
|
34
|
+
* multiSelectInteraction.updateCallback('onMultiSelect', ({ nodes, rels }) => {
|
|
35
|
+
* console.log('Selected elements:', nodes, rels)
|
|
36
|
+
* })
|
|
37
|
+
*/
|
|
38
|
+
export declare class MultiSelectInteraction extends BaseInteraction<MultiSelectInteractionCallbacks> {
|
|
39
|
+
private mousePosition;
|
|
40
|
+
private startWorldPosition;
|
|
41
|
+
private overlayRenderer;
|
|
42
|
+
private isMultiSelecting;
|
|
43
|
+
private readonly options;
|
|
44
|
+
/**
|
|
45
|
+
* Creates a new instance of the multi-select interaction handler.
|
|
46
|
+
* @param nvl - The NVL instance to attach the interaction handler to
|
|
47
|
+
* @param options - Options for the multi-select interaction handler to customize its behavior
|
|
48
|
+
*/
|
|
49
|
+
constructor(nvl: NVL, options?: MultiSelectInteractionOptions);
|
|
50
|
+
private handleMouseDown;
|
|
51
|
+
private handleDrag;
|
|
52
|
+
private endMultiSelect;
|
|
53
|
+
/**
|
|
54
|
+
* Removes all related event listeners and the overlay renderer for the box.
|
|
55
|
+
*/
|
|
56
|
+
destroy(): void;
|
|
57
|
+
private turnOnMultiSelect;
|
|
58
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { NODE_EDGE_WIDTH } from '../constants';
|
|
2
|
+
import { OverlayRenderer } from '../overlay-renderer/overlay-renderer';
|
|
3
|
+
import { BaseInteraction } from './base';
|
|
4
|
+
/**
|
|
5
|
+
* Interaction handler for multi-selecting nodes and relationships.
|
|
6
|
+
* When dragging, draws a box on the canvas and selects all
|
|
7
|
+
* nodes and relationships that are inside the box.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```js
|
|
11
|
+
* const nvl = new NVL(container, nodes, relationships)
|
|
12
|
+
* const multiSelectInteraction = new MultiSelectInteraction(nvl)
|
|
13
|
+
*
|
|
14
|
+
* multiSelectInteraction.updateCallback('onMultiSelect', ({ nodes, rels }) => {
|
|
15
|
+
* console.log('Selected elements:', nodes, rels)
|
|
16
|
+
* })
|
|
17
|
+
*/
|
|
18
|
+
export class MultiSelectInteraction extends BaseInteraction {
|
|
19
|
+
/**
|
|
20
|
+
* Creates a new instance of the multi-select interaction handler.
|
|
21
|
+
* @param nvl - The NVL instance to attach the interaction handler to
|
|
22
|
+
* @param options - Options for the multi-select interaction handler to customize its behavior
|
|
23
|
+
*/
|
|
24
|
+
constructor(nvl, options = { selectOnRelease: false }) {
|
|
25
|
+
super(nvl);
|
|
26
|
+
Object.defineProperty(this, "mousePosition", {
|
|
27
|
+
enumerable: true,
|
|
28
|
+
configurable: true,
|
|
29
|
+
writable: true,
|
|
30
|
+
value: void 0
|
|
31
|
+
});
|
|
32
|
+
Object.defineProperty(this, "startWorldPosition", {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
writable: true,
|
|
36
|
+
value: void 0
|
|
37
|
+
});
|
|
38
|
+
Object.defineProperty(this, "overlayRenderer", {
|
|
39
|
+
enumerable: true,
|
|
40
|
+
configurable: true,
|
|
41
|
+
writable: true,
|
|
42
|
+
value: void 0
|
|
43
|
+
});
|
|
44
|
+
Object.defineProperty(this, "isMultiSelecting", {
|
|
45
|
+
enumerable: true,
|
|
46
|
+
configurable: true,
|
|
47
|
+
writable: true,
|
|
48
|
+
value: void 0
|
|
49
|
+
});
|
|
50
|
+
Object.defineProperty(this, "options", {
|
|
51
|
+
enumerable: true,
|
|
52
|
+
configurable: true,
|
|
53
|
+
writable: true,
|
|
54
|
+
value: void 0
|
|
55
|
+
});
|
|
56
|
+
Object.defineProperty(this, "handleMouseDown", {
|
|
57
|
+
enumerable: true,
|
|
58
|
+
configurable: true,
|
|
59
|
+
writable: true,
|
|
60
|
+
value: (event) => {
|
|
61
|
+
if (event.button !== 0) {
|
|
62
|
+
this.isMultiSelecting = false;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.turnOnMultiSelect(event);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
Object.defineProperty(this, "handleDrag", {
|
|
69
|
+
enumerable: true,
|
|
70
|
+
configurable: true,
|
|
71
|
+
writable: true,
|
|
72
|
+
value: (event) => {
|
|
73
|
+
if (this.isMultiSelecting) {
|
|
74
|
+
this.overlayRenderer.drawBox(this.mousePosition.x, this.mousePosition.y, event.clientX, event.clientY);
|
|
75
|
+
}
|
|
76
|
+
else if (event.buttons === 1) {
|
|
77
|
+
this.turnOnMultiSelect(event);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
Object.defineProperty(this, "endMultiSelect", {
|
|
82
|
+
enumerable: true,
|
|
83
|
+
configurable: true,
|
|
84
|
+
writable: true,
|
|
85
|
+
value: (event) => {
|
|
86
|
+
if (!this.isMultiSelecting) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.isMultiSelecting = false;
|
|
90
|
+
this.overlayRenderer.clear();
|
|
91
|
+
const endWorldPosition = this.nvlInstance.getMousePosition(event);
|
|
92
|
+
const { nodes, rels } = this.nvlInstance.getHitsInBox(this.startWorldPosition.x, this.startWorldPosition.y, endWorldPosition.x, endWorldPosition.y, ['node', 'relationship']);
|
|
93
|
+
if (this.options.selectOnRelease) {
|
|
94
|
+
this.nvlInstance.updateElementsInGraph(nodes.map((node) => ({ id: node.id, selected: true })), rels.map((rel) => ({ id: rel.id, selected: true })));
|
|
95
|
+
}
|
|
96
|
+
this.callCallbackIfRegistered('onMultiSelect', { nodes, rels });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
this.mousePosition = { x: 0, y: 0 };
|
|
100
|
+
this.startWorldPosition = { x: 0, y: 0 };
|
|
101
|
+
this.overlayRenderer = new OverlayRenderer(this.containerInstance);
|
|
102
|
+
this.isMultiSelecting = false;
|
|
103
|
+
this.options = options;
|
|
104
|
+
this.addEventListener('mousedown', this.handleMouseDown, true);
|
|
105
|
+
this.addEventListener('mousemove', this.handleDrag, true);
|
|
106
|
+
this.addEventListener('mouseup', this.endMultiSelect, true);
|
|
107
|
+
this.addEventListener('mouseleave', this.endMultiSelect, true);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Removes all related event listeners and the overlay renderer for the box.
|
|
111
|
+
*/
|
|
112
|
+
destroy() {
|
|
113
|
+
this.removeEventListener('mousedown', this.handleMouseDown, true);
|
|
114
|
+
this.removeEventListener('mousemove', this.handleDrag, true);
|
|
115
|
+
this.removeEventListener('mouseup', this.endMultiSelect, true);
|
|
116
|
+
this.removeEventListener('mouseleave', this.endMultiSelect, true);
|
|
117
|
+
this.overlayRenderer.destroy();
|
|
118
|
+
}
|
|
119
|
+
turnOnMultiSelect(event) {
|
|
120
|
+
this.mousePosition = { x: event.clientX, y: event.clientY };
|
|
121
|
+
this.startWorldPosition = this.nvlInstance.getMousePosition(event);
|
|
122
|
+
const hits = this.nvlInstance.getHits(event, ['node'], { hitNodeMarginWidth: NODE_EDGE_WIDTH });
|
|
123
|
+
if (hits.nvlTargets.nodes.length > 0) {
|
|
124
|
+
this.isMultiSelecting = false;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
this.isMultiSelecting = true;
|
|
128
|
+
if (this.options.selectOnRelease) {
|
|
129
|
+
this.nvlInstance.deselectAll();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|