@neo4j-nvl/interaction-handlers 1.2.0 → 1.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/CHANGELOG.md +16 -0
- package/README.md +1 -1
- package/lib/__tests__/box-select-interaction.test.js +28 -2
- package/lib/__tests__/lasso-interaction.test.js +37 -1
- package/lib/__tests__/utils.test.d.ts +1 -0
- package/lib/__tests__/utils.test.js +19 -0
- package/lib/interaction-handlers/box-select-interaction.d.ts +3 -0
- package/lib/interaction-handlers/box-select-interaction.js +24 -9
- package/lib/interaction-handlers/keyboard-interaction.d.ts +4 -0
- package/lib/interaction-handlers/keyboard-interaction.js +19 -1
- package/lib/interaction-handlers/lasso-interaction.d.ts +4 -0
- package/lib/interaction-handlers/lasso-interaction.js +31 -9
- package/lib/interaction-handlers/utils.d.ts +2 -1
- package/lib/interaction-handlers/utils.js +6 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to NVL will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
4
4
|
|
|
5
|
+
## [1.2.1] - 2026-07-28
|
|
6
|
+
|
|
7
|
+
This 1.2.1 patch release contains several fixes and accessibility improvements.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
* Outline when the canvas is focused while using the keyboard interaction handlers.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
* Renamed links in README from Explore to Neo4j Bloom.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
* Aligned padding on svg and png export.
|
|
17
|
+
* Aligned start behaviour of box and lasso select interaction handlers.
|
|
18
|
+
* Fixed static image wrapper React component returning incorrect height when using SVG format.
|
|
19
|
+
* Fixed self-referencing relationship captions being misplaced on downward curves.
|
|
20
|
+
|
|
5
21
|
## [1.2.0] - 2026-05-27
|
|
6
22
|
|
|
7
23
|
This 1.2.0 release introduces a new keyboard interaction handler and SVG support for the static image wrapper React component. It also includes several performance improvements for clustering and text/icon rendering, as well as several bug fixes.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Neo4j Visualization Library
|
|
2
2
|
|
|
3
|
-
Welcome to the Neo4j Visualization Library, NVL for short. NVL is a collection of libraries that can be used to build custom graph visualizations like those used in [Neo4j Bloom
|
|
3
|
+
Welcome to the Neo4j Visualization Library, NVL for short. NVL is a collection of libraries that can be used to build custom graph visualizations like those used in [Neo4j Bloom](https://neo4j.com/product/bloom/). NVL is written in TypeScript and can be used in any JavaScript project. It is also available as a React component that can be used in React applications.
|
|
4
4
|
|
|
5
5
|
This module is a collection of decorator classes that can be used to add interaction events to an NVL instance. The decorators are applied to the NVL class. You can find more information about the NVL class in the [NVL base library](https://www.npmjs.com/package/@neo4j-nvl/base).
|
|
6
6
|
|
|
@@ -50,14 +50,15 @@ describe('BoxSelectInteraction', () => {
|
|
|
50
50
|
resolve();
|
|
51
51
|
});
|
|
52
52
|
});
|
|
53
|
-
test
|
|
53
|
+
test('will not start when node is below pointer when dragging', () => {
|
|
54
54
|
boxSelectInteraction.updateCallback('onBoxStarted', startCallback);
|
|
55
55
|
boxSelectInteraction.updateCallback('onBoxSelect', selectCallback);
|
|
56
56
|
expect(myNVL.getSelectedNodes()).toHaveLength(0);
|
|
57
57
|
const gesture = [
|
|
58
58
|
createMouseEvent('mousedown', 25, 25),
|
|
59
59
|
createMouseEvent('mousemove', 50, 50),
|
|
60
|
-
createMouseEvent('
|
|
60
|
+
createMouseEvent('mousemove', 200, 200),
|
|
61
|
+
createMouseEvent('mouseup', 200, 200)
|
|
61
62
|
];
|
|
62
63
|
const container = myNVL.getContainer();
|
|
63
64
|
gesture.forEach((e) => container.dispatchEvent(e));
|
|
@@ -67,6 +68,31 @@ describe('BoxSelectInteraction', () => {
|
|
|
67
68
|
resolve();
|
|
68
69
|
});
|
|
69
70
|
});
|
|
71
|
+
test('can start box select when only a relationship is under the pointer', () => {
|
|
72
|
+
boxSelectInteraction.updateCallback('onBoxStarted', startCallback);
|
|
73
|
+
boxSelectInteraction.updateCallback('onBoxSelect', selectCallback);
|
|
74
|
+
const gesture = [
|
|
75
|
+
createMouseEvent('mousedown', 100, 100),
|
|
76
|
+
createMouseEvent('mousemove', 150, 100),
|
|
77
|
+
createMouseEvent('mouseup', 150, 100)
|
|
78
|
+
];
|
|
79
|
+
const container = myNVL.getContainer();
|
|
80
|
+
gesture.forEach((e) => container.dispatchEvent(e));
|
|
81
|
+
return new Promise((resolve) => {
|
|
82
|
+
expect(startCallback).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(selectCallback).toHaveBeenCalledTimes(1);
|
|
84
|
+
resolve();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
test('does not fire callbacks when clicking directly on a relationship without dragging', () => {
|
|
88
|
+
boxSelectInteraction.updateCallback('onBoxStarted', startCallback);
|
|
89
|
+
boxSelectInteraction.updateCallback('onBoxSelect', selectCallback);
|
|
90
|
+
const container = myNVL.getContainer();
|
|
91
|
+
container.dispatchEvent(createMouseEvent('mousedown', 112, 112));
|
|
92
|
+
container.dispatchEvent(createMouseEvent('mouseup', 112, 112));
|
|
93
|
+
expect(startCallback).not.toHaveBeenCalled();
|
|
94
|
+
expect(selectCallback).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
70
96
|
test('box selection should disable text selection until mouse release', () => {
|
|
71
97
|
const container = myNVL.getContainer();
|
|
72
98
|
boxSelectInteraction.updateCallback('onBoxSelect', boxSelectCallbackMock);
|
|
@@ -78,12 +78,39 @@ describe('LassoInteraction', () => {
|
|
|
78
78
|
resolve();
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
|
+
test('does not fire started or select callbacks when movement stays below drag threshold', () => {
|
|
82
|
+
lassoInteraction.updateCallback('onLassoStarted', startCallback);
|
|
83
|
+
lassoInteraction.updateCallback('onLassoSelect', selectCallback);
|
|
84
|
+
const container = myNVL.getContainer();
|
|
85
|
+
container.dispatchEvent(createMouseEvent('mousedown', 100, 100));
|
|
86
|
+
container.dispatchEvent(createMouseEvent('mousemove', 100, 103));
|
|
87
|
+
container.dispatchEvent(createMouseEvent('mouseup', 100, 103));
|
|
88
|
+
expect(startCallback).not.toHaveBeenCalled();
|
|
89
|
+
expect(selectCallback).not.toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
test('can start lasso when only a relationship is under the pointer', () => {
|
|
92
|
+
lassoInteraction.updateCallback('onLassoStarted', startCallback);
|
|
93
|
+
lassoInteraction.updateCallback('onLassoSelect', selectCallback);
|
|
94
|
+
const gesture = [
|
|
95
|
+
createMouseEvent('mousedown', 100, 100),
|
|
96
|
+
createMouseEvent('mousemove', 150, 100),
|
|
97
|
+
createMouseEvent('mousemove', 50, 150),
|
|
98
|
+
createMouseEvent('mouseup', 100, 100)
|
|
99
|
+
];
|
|
100
|
+
const container = myNVL.getContainer();
|
|
101
|
+
gesture.forEach((e) => container.dispatchEvent(e));
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
expect(startCallback).toHaveBeenCalledTimes(1);
|
|
104
|
+
expect(selectCallback).toHaveBeenCalledTimes(1);
|
|
105
|
+
resolve();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
81
108
|
test('will not start when node is below pointer when dragging', () => {
|
|
82
109
|
lassoInteraction.updateCallback('onLassoStarted', startCallback);
|
|
83
110
|
lassoInteraction.updateCallback('onLassoSelect', selectCallback);
|
|
84
111
|
expect(myNVL.getSelectedNodes()).toHaveLength(0);
|
|
85
112
|
const gesture1 = [
|
|
86
|
-
createMouseEvent('mousedown',
|
|
113
|
+
createMouseEvent('mousedown', 25, 25),
|
|
87
114
|
createMouseEvent('mousemove', 50, 0),
|
|
88
115
|
createMouseEvent('mousemove', 20, 50),
|
|
89
116
|
createMouseEvent('mouseup', 0, 0)
|
|
@@ -95,6 +122,15 @@ describe('LassoInteraction', () => {
|
|
|
95
122
|
resolve();
|
|
96
123
|
});
|
|
97
124
|
});
|
|
125
|
+
test('does not fire callbacks when clicking directly on a relationship without dragging', () => {
|
|
126
|
+
lassoInteraction.updateCallback('onLassoStarted', startCallback);
|
|
127
|
+
lassoInteraction.updateCallback('onLassoSelect', selectCallback);
|
|
128
|
+
const container = myNVL.getContainer();
|
|
129
|
+
container.dispatchEvent(createMouseEvent('mousedown', 112, 112));
|
|
130
|
+
container.dispatchEvent(createMouseEvent('mouseup', 112, 112));
|
|
131
|
+
expect(startCallback).not.toHaveBeenCalled();
|
|
132
|
+
expect(selectCallback).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
98
134
|
test('test for line crossing utility function', () => {
|
|
99
135
|
const res1 = checkLinesCrossing([0, 0], [10, 10], [10, 0], [0, 10]);
|
|
100
136
|
expect(res1).toBeTruthy();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isDraggingDistance, isDraggingMovement } from '../interaction-handlers/utils';
|
|
2
|
+
describe('interaction utils', () => {
|
|
3
|
+
test('returns true when movement reaches threshold on either axis', () => {
|
|
4
|
+
expect(isDraggingDistance({ x: 0, y: 0 }, { x: 10, y: 0 })).toBe(true);
|
|
5
|
+
expect(isDraggingDistance({ x: 0, y: 0 }, { x: 0, y: 10 })).toBe(true);
|
|
6
|
+
});
|
|
7
|
+
test('returns true for diagonal movement whose combined movement reaches threshold', () => {
|
|
8
|
+
expect(isDraggingDistance({ x: 0, y: 0 }, { x: 2, y: 2 })).toBe(false);
|
|
9
|
+
expect(isDraggingDistance({ x: 0, y: 0 }, { x: 7, y: 7 })).toBe(true);
|
|
10
|
+
expect(isDraggingDistance({ x: 0, y: 0 }, { x: 8, y: 8 })).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
test('applies the same threshold logic to mouse events', () => {
|
|
13
|
+
expect(isDraggingMovement(new MouseEvent('mousemove', { clientX: 10, clientY: 0 }), { x: 0, y: 0 })).toBe(true);
|
|
14
|
+
expect(isDraggingMovement(new MouseEvent('mousemove', { clientX: 0, clientY: 10 }), { x: 0, y: 0 })).toBe(true);
|
|
15
|
+
expect(isDraggingMovement(new MouseEvent('mousemove', { clientX: 2, clientY: 2 }), { x: 0, y: 0 })).toBe(false);
|
|
16
|
+
expect(isDraggingMovement(new MouseEvent('mousemove', { clientX: 7, clientY: 7 }), { x: 0, y: 0 })).toBe(true);
|
|
17
|
+
expect(isDraggingMovement(new MouseEvent('mousemove', { clientX: 8, clientY: 8 }), { x: 0, y: 0 })).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -38,8 +38,10 @@ export type BoxSelectInteractionCallbacks = {
|
|
|
38
38
|
export declare class BoxSelectInteraction extends BaseInteraction<BoxSelectInteractionOptions> {
|
|
39
39
|
private mousePosition;
|
|
40
40
|
private startWorldPosition;
|
|
41
|
+
private startClientPosition;
|
|
41
42
|
private overlayRenderer;
|
|
42
43
|
private isBoxSelecting;
|
|
44
|
+
private boxSelectPending;
|
|
43
45
|
/**
|
|
44
46
|
* Creates a new instance of the multi-select interaction handler.
|
|
45
47
|
* @param nvl - The NVL instance to attach the interaction handler to.
|
|
@@ -54,5 +56,6 @@ export declare class BoxSelectInteraction extends BaseInteraction<BoxSelectInter
|
|
|
54
56
|
* Removes all related event listeners and the overlay renderer for the box.
|
|
55
57
|
*/
|
|
56
58
|
destroy(): void;
|
|
59
|
+
private activateBoxSelect;
|
|
57
60
|
private turnOnBoxSelect;
|
|
58
61
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NODE_EDGE_WIDTH } from '../constants';
|
|
2
2
|
import { OverlayRenderer } from '../overlay-renderer/overlay-renderer';
|
|
3
3
|
import { BaseInteraction } from './base';
|
|
4
|
-
import { getCanvasPosition, getWorldPosition } from './utils';
|
|
4
|
+
import { getCanvasPosition, getWorldPosition, isDraggingMovement } from './utils';
|
|
5
5
|
/**
|
|
6
6
|
* An interaction handler for multi-selecting nodes and relationships.
|
|
7
7
|
* When dragging the cursor, it draws a box on the scene and all nodes and relationships inside the box are selected.
|
|
@@ -11,8 +11,10 @@ import { getCanvasPosition, getWorldPosition } from './utils';
|
|
|
11
11
|
export class BoxSelectInteraction extends BaseInteraction {
|
|
12
12
|
mousePosition = { x: 0, y: 0 };
|
|
13
13
|
startWorldPosition = { x: 0, y: 0 };
|
|
14
|
+
startClientPosition = { x: 0, y: 0 };
|
|
14
15
|
overlayRenderer;
|
|
15
16
|
isBoxSelecting = false;
|
|
17
|
+
boxSelectPending = false;
|
|
16
18
|
/**
|
|
17
19
|
* Creates a new instance of the multi-select interaction handler.
|
|
18
20
|
* @param nvl - The NVL instance to attach the interaction handler to.
|
|
@@ -28,6 +30,7 @@ export class BoxSelectInteraction extends BaseInteraction {
|
|
|
28
30
|
handleMouseDown = (event) => {
|
|
29
31
|
if (event.button !== 0) {
|
|
30
32
|
this.isBoxSelecting = false;
|
|
33
|
+
this.boxSelectPending = false;
|
|
31
34
|
return;
|
|
32
35
|
}
|
|
33
36
|
this.turnOnBoxSelect(event);
|
|
@@ -36,9 +39,12 @@ export class BoxSelectInteraction extends BaseInteraction {
|
|
|
36
39
|
if (this.isBoxSelecting) {
|
|
37
40
|
const curPos = getCanvasPosition(this.containerInstance, event);
|
|
38
41
|
this.overlayRenderer.drawBox(this.mousePosition.x, this.mousePosition.y, curPos.x, curPos.y);
|
|
42
|
+
return;
|
|
39
43
|
}
|
|
40
|
-
|
|
41
|
-
this.
|
|
44
|
+
if (this.boxSelectPending && event.buttons === 1) {
|
|
45
|
+
if (isDraggingMovement(event, this.startClientPosition)) {
|
|
46
|
+
this.activateBoxSelect(event);
|
|
47
|
+
}
|
|
42
48
|
}
|
|
43
49
|
};
|
|
44
50
|
getHitsInBox = (startPos, endPos) => {
|
|
@@ -71,6 +77,7 @@ export class BoxSelectInteraction extends BaseInteraction {
|
|
|
71
77
|
};
|
|
72
78
|
endBoxSelect = (event) => {
|
|
73
79
|
if (!this.isBoxSelecting) {
|
|
80
|
+
this.boxSelectPending = false;
|
|
74
81
|
return;
|
|
75
82
|
}
|
|
76
83
|
this.isBoxSelecting = false;
|
|
@@ -94,20 +101,28 @@ export class BoxSelectInteraction extends BaseInteraction {
|
|
|
94
101
|
this.removeEventListener('mouseup', this.endBoxSelect, true);
|
|
95
102
|
this.overlayRenderer.destroy();
|
|
96
103
|
}
|
|
104
|
+
activateBoxSelect = (event) => {
|
|
105
|
+
this.boxSelectPending = false;
|
|
106
|
+
this.isBoxSelecting = true;
|
|
107
|
+
this.toggleGlobalTextSelection(false, this.endBoxSelect);
|
|
108
|
+
this.callCallbackIfRegistered('onBoxStarted', event);
|
|
109
|
+
if (this.currentOptions.selectOnRelease === true) {
|
|
110
|
+
this.nvlInstance.deselectAll();
|
|
111
|
+
}
|
|
112
|
+
const curPos = getCanvasPosition(this.containerInstance, event);
|
|
113
|
+
this.overlayRenderer.drawBox(this.mousePosition.x, this.mousePosition.y, curPos.x, curPos.y);
|
|
114
|
+
};
|
|
97
115
|
turnOnBoxSelect(event) {
|
|
98
116
|
this.mousePosition = getCanvasPosition(this.containerInstance, event);
|
|
99
117
|
this.startWorldPosition = getWorldPosition(this.nvlInstance, this.mousePosition);
|
|
100
118
|
const hits = this.nvlInstance.getHits(event, ['node'], { hitNodeMarginWidth: NODE_EDGE_WIDTH });
|
|
101
119
|
if (hits.nvlTargets.nodes.length > 0) {
|
|
102
120
|
this.isBoxSelecting = false;
|
|
121
|
+
this.boxSelectPending = false;
|
|
103
122
|
}
|
|
104
123
|
else {
|
|
105
|
-
this.
|
|
106
|
-
this.
|
|
107
|
-
this.callCallbackIfRegistered('onBoxStarted', event);
|
|
108
|
-
if (this.currentOptions.selectOnRelease === true) {
|
|
109
|
-
this.nvlInstance.deselectAll();
|
|
110
|
-
}
|
|
124
|
+
this.startClientPosition = { x: event.clientX, y: event.clientY };
|
|
125
|
+
this.boxSelectPending = true;
|
|
111
126
|
}
|
|
112
127
|
}
|
|
113
128
|
}
|
|
@@ -27,6 +27,8 @@ export type KeyboardInteractionOptions = {
|
|
|
27
27
|
exitGraphKey?: string[];
|
|
28
28
|
/** Key combo for opening the context menu on the focused element. Default: `['Shift', 'F10']` */
|
|
29
29
|
contextMenuKey?: string[];
|
|
30
|
+
/** The color of the outline shown when the canvas receives keyboard focus. Default: `'#30839D'` */
|
|
31
|
+
canvasOutlineColor?: string;
|
|
30
32
|
};
|
|
31
33
|
/**
|
|
32
34
|
* Callbacks for the keyboard interaction handler.
|
|
@@ -114,6 +116,8 @@ export declare class KeyboardInteraction extends BaseInteraction<KeyboardInterac
|
|
|
114
116
|
* Returns the currently focused element, if any.
|
|
115
117
|
*/
|
|
116
118
|
getFocused(): Node | Relationship | undefined;
|
|
119
|
+
private setCanvasOutline;
|
|
120
|
+
private clearCanvasOutline;
|
|
117
121
|
private handleFocusIn;
|
|
118
122
|
private handleFocusOut;
|
|
119
123
|
private handleKeyDown;
|
|
@@ -4,7 +4,8 @@ const DEFAULT_OPTIONS = {
|
|
|
4
4
|
navigateForwardKey: ['Tab'],
|
|
5
5
|
navigateBackwardKey: ['Shift', 'Tab'],
|
|
6
6
|
exitGraphKey: ['Escape'],
|
|
7
|
-
contextMenuKey: ['Shift', 'F10']
|
|
7
|
+
contextMenuKey: ['Shift', 'F10'],
|
|
8
|
+
canvasOutlineColor: '#30839D'
|
|
8
9
|
};
|
|
9
10
|
/**
|
|
10
11
|
* Checks whether a keyboard event matches a key combo array.
|
|
@@ -79,11 +80,25 @@ export class KeyboardInteraction extends BaseInteraction {
|
|
|
79
80
|
}
|
|
80
81
|
return this.nvlInstance.getRelationships().find((r) => r.id === this.focusedElementId);
|
|
81
82
|
}
|
|
83
|
+
setCanvasOutline() {
|
|
84
|
+
const container = this.containerInstance;
|
|
85
|
+
if (container.matches(':focus-visible')) {
|
|
86
|
+
container.style.outline = `2px solid ${this.resolvedOptions.canvasOutlineColor}`;
|
|
87
|
+
container.style.outlineOffset = '-2px';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
clearCanvasOutline() {
|
|
91
|
+
const container = this.containerInstance;
|
|
92
|
+
container.style.outline = 'none';
|
|
93
|
+
container.style.outlineOffset = '';
|
|
94
|
+
}
|
|
82
95
|
handleFocusIn = (event) => {
|
|
96
|
+
this.setCanvasOutline();
|
|
83
97
|
this.callCallbackIfRegistered('onCanvasFocus', event);
|
|
84
98
|
};
|
|
85
99
|
handleFocusOut = (event) => {
|
|
86
100
|
this.callCallbackIfRegistered('onCanvasBlur', event);
|
|
101
|
+
this.clearCanvasOutline();
|
|
87
102
|
};
|
|
88
103
|
handleKeyDown = (event) => {
|
|
89
104
|
const focusedElement = this.getFocused();
|
|
@@ -108,6 +123,7 @@ export class KeyboardInteraction extends BaseInteraction {
|
|
|
108
123
|
}
|
|
109
124
|
}
|
|
110
125
|
else if (matchesKeyCombo(event, exitGraphKey)) {
|
|
126
|
+
this.setCanvasOutline();
|
|
111
127
|
if (focusedElement !== undefined) {
|
|
112
128
|
event.preventDefault();
|
|
113
129
|
this.clearFocus(event);
|
|
@@ -150,6 +166,7 @@ export class KeyboardInteraction extends BaseInteraction {
|
|
|
150
166
|
// Notify about the newly focused element
|
|
151
167
|
const newElement = this.getFocused();
|
|
152
168
|
if (newElement !== undefined) {
|
|
169
|
+
this.clearCanvasOutline();
|
|
153
170
|
if (isNode) {
|
|
154
171
|
this.callCallbackIfRegistered('onNodeFocus', newElement, event);
|
|
155
172
|
}
|
|
@@ -179,6 +196,7 @@ export class KeyboardInteraction extends BaseInteraction {
|
|
|
179
196
|
this.removeEventListener('keyup', this.handleKeyUp);
|
|
180
197
|
this.removeEventListener('focusin', this.handleFocusIn);
|
|
181
198
|
this.removeEventListener('focusout', this.handleFocusOut);
|
|
199
|
+
this.clearCanvasOutline();
|
|
182
200
|
if (this.addedTabindex) {
|
|
183
201
|
this.containerInstance.removeAttribute('tabindex');
|
|
184
202
|
}
|
|
@@ -60,6 +60,9 @@ export declare const checkPointInside: (x: number, y: number, vs: Point[]) => bo
|
|
|
60
60
|
*/
|
|
61
61
|
export declare class LassoInteraction extends BaseInteraction<LassoInteractionOptions> {
|
|
62
62
|
private active;
|
|
63
|
+
private lassoPending;
|
|
64
|
+
private startClientPosition;
|
|
65
|
+
private pendingPointerCanvas;
|
|
63
66
|
private points;
|
|
64
67
|
private overlayRenderer;
|
|
65
68
|
/**
|
|
@@ -71,6 +74,7 @@ export declare class LassoInteraction extends BaseInteraction<LassoInteractionOp
|
|
|
71
74
|
private startLasso;
|
|
72
75
|
private handleMouseDown;
|
|
73
76
|
private handleDrag;
|
|
77
|
+
private activateLasso;
|
|
74
78
|
private handleMouseUp;
|
|
75
79
|
private getLassoItems;
|
|
76
80
|
private endLasso;
|
|
@@ -2,7 +2,7 @@ import concaveman from 'concaveman';
|
|
|
2
2
|
import { NODE_EDGE_WIDTH } from '../constants';
|
|
3
3
|
import { OverlayRenderer } from '../overlay-renderer/overlay-renderer';
|
|
4
4
|
import { BaseInteraction } from './base';
|
|
5
|
-
import { getCanvasPosition, getWorldPosition } from './utils';
|
|
5
|
+
import { getCanvasPosition, getWorldPosition, isDraggingMovement } from './utils';
|
|
6
6
|
const pointDist = 10;
|
|
7
7
|
const shapeShowTime = 500;
|
|
8
8
|
/**
|
|
@@ -73,6 +73,9 @@ export const checkPointInside = (x, y, vs) => {
|
|
|
73
73
|
*/
|
|
74
74
|
export class LassoInteraction extends BaseInteraction {
|
|
75
75
|
active = false;
|
|
76
|
+
lassoPending = false;
|
|
77
|
+
startClientPosition = { x: 0, y: 0 };
|
|
78
|
+
pendingPointerCanvas = { x: 0, y: 0 };
|
|
76
79
|
points = [];
|
|
77
80
|
overlayRenderer;
|
|
78
81
|
/**
|
|
@@ -91,19 +94,16 @@ export class LassoInteraction extends BaseInteraction {
|
|
|
91
94
|
const hits = this.nvlInstance.getHits(event, ['node'], { hitNodeMarginWidth: NODE_EDGE_WIDTH });
|
|
92
95
|
if (hits.nvlTargets.nodes.length > 0) {
|
|
93
96
|
this.active = false;
|
|
97
|
+
this.lassoPending = false;
|
|
94
98
|
}
|
|
95
99
|
else {
|
|
96
|
-
this.
|
|
97
|
-
this.
|
|
98
|
-
this.
|
|
99
|
-
this.callCallbackIfRegistered('onLassoStarted', event);
|
|
100
|
-
if (this.currentOptions.selectOnRelease === true) {
|
|
101
|
-
this.nvlInstance.deselectAll();
|
|
102
|
-
}
|
|
100
|
+
this.lassoPending = true;
|
|
101
|
+
this.startClientPosition = { x: event.clientX, y: event.clientY };
|
|
102
|
+
this.pendingPointerCanvas = getCanvasPosition(this.containerInstance, event);
|
|
103
103
|
}
|
|
104
104
|
};
|
|
105
105
|
handleMouseDown = (event) => {
|
|
106
|
-
if (event.button === 0 && !this.active) {
|
|
106
|
+
if (event.button === 0 && !this.active && !this.lassoPending) {
|
|
107
107
|
this.startLasso(event);
|
|
108
108
|
}
|
|
109
109
|
};
|
|
@@ -120,9 +120,31 @@ export class LassoInteraction extends BaseInteraction {
|
|
|
120
120
|
this.points.push(pos);
|
|
121
121
|
this.overlayRenderer.drawLasso(this.points, true, false);
|
|
122
122
|
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (this.lassoPending && event.buttons === 1) {
|
|
126
|
+
if (isDraggingMovement(event, this.startClientPosition)) {
|
|
127
|
+
this.activateLasso(event);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
activateLasso = (event) => {
|
|
132
|
+
this.lassoPending = false;
|
|
133
|
+
this.active = true;
|
|
134
|
+
const pos = getCanvasPosition(this.containerInstance, event);
|
|
135
|
+
this.points = [this.pendingPointerCanvas, pos];
|
|
136
|
+
this.toggleGlobalTextSelection(false, this.endLasso);
|
|
137
|
+
this.callCallbackIfRegistered('onLassoStarted', event);
|
|
138
|
+
if (this.currentOptions.selectOnRelease === true) {
|
|
139
|
+
this.nvlInstance.deselectAll();
|
|
123
140
|
}
|
|
141
|
+
this.overlayRenderer.drawLasso(this.points, true, false);
|
|
124
142
|
};
|
|
125
143
|
handleMouseUp = (event) => {
|
|
144
|
+
if (!this.active) {
|
|
145
|
+
this.lassoPending = false;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
126
148
|
this.points.push(getCanvasPosition(this.containerInstance, event));
|
|
127
149
|
this.endLasso(event);
|
|
128
150
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { NVL, Point } from '@neo4j-nvl/base';
|
|
2
2
|
export declare const generateUniqueId: (digit: number) => string;
|
|
3
|
-
export declare const
|
|
3
|
+
export declare const isDraggingDistance: (startPoint: Point, endPoint: Point) => boolean;
|
|
4
|
+
export declare const isDraggingMovement: (event: MouseEvent, point: Point) => boolean;
|
|
4
5
|
export declare const getCanvasPosition: (canvas: HTMLElement, mouseEvent: MouseEvent) => Point;
|
|
5
6
|
export declare const getCanvasCenterOffset: (canvas: HTMLElement, mouseEvent: MouseEvent) => Point;
|
|
6
7
|
export declare const getWorldPosition: (nvl: NVL, pos: Point) => Point;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { DRAG_THRESHOLD } from '../constants';
|
|
2
2
|
export const generateUniqueId = (digit) => Math.floor(Math.random() * Math.pow(10, digit)).toString();
|
|
3
|
-
export const
|
|
4
|
-
const diffX = Math.abs(
|
|
5
|
-
const diffY = Math.abs(
|
|
3
|
+
export const isDraggingDistance = (startPoint, endPoint) => {
|
|
4
|
+
const diffX = Math.abs(endPoint.x - startPoint.x);
|
|
5
|
+
const diffY = Math.abs(endPoint.y - startPoint.y);
|
|
6
6
|
if (diffX > DRAG_THRESHOLD || diffY > DRAG_THRESHOLD) {
|
|
7
7
|
return true;
|
|
8
8
|
}
|
|
9
9
|
const distanceSquared = Math.pow(diffX, 2) + Math.pow(diffY, 2);
|
|
10
10
|
return distanceSquared > DRAG_THRESHOLD;
|
|
11
11
|
};
|
|
12
|
+
export const isDraggingMovement = (event, point) => {
|
|
13
|
+
return isDraggingDistance(point, { x: event.clientX, y: event.clientY });
|
|
14
|
+
};
|
|
12
15
|
export const getCanvasPosition = (canvas, mouseEvent) => {
|
|
13
16
|
const rect = canvas.getBoundingClientRect();
|
|
14
17
|
const devicePixelRatio = window.devicePixelRatio || 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neo4j-nvl/interaction-handlers",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
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": "1.2.
|
|
30
|
+
"@neo4j-nvl/base": "1.2.1",
|
|
31
31
|
"concaveman": "1.2.1",
|
|
32
32
|
"lodash": "4.18.1"
|
|
33
33
|
},
|