@neo4j-nvl/interaction-handlers 0.3.7 → 0.3.8-15dd54ce

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 CHANGED
@@ -2,6 +2,43 @@
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
+ ## [0.3.8] - 2025-03-18
6
+
7
+ This release contains a fix for the [NVL result transformer](https://neo4j.com/docs/api/nvl/current/functions/_neo4j_nvl_base.nvlResultTransformer.html).
8
+
9
+ ### Fixed
10
+
11
+ * NVL result transformer to be able to handle deeply nested records
12
+
13
+ ## [0.3.7] - 2025-02-28
14
+
15
+ ### Added
16
+
17
+ * Export zoom functionalities as a named function
18
+ * Add a `positions` prop to the react wrapper
19
+ * Color custimzation options
20
+ * Aria attributes for NVL canvas and option to disable them
21
+ * Option to disable web workers
22
+
23
+ ### Changed
24
+
25
+ * Change default threshold when relationship captions are displayed
26
+ * update default selection color and default node color to be aligned with the Neo4j Design Library
27
+
28
+ ### Deprecated
29
+
30
+ * `selectedBorderColor` - use `styling.selectedBorderColor` instead
31
+ * `nodeDefaultBorderColor` - use `styling.nodeDefaultBorderColor` instead
32
+
33
+ ### Fixed
34
+
35
+ * Fix icon colors not being inverted for dark node colors
36
+ * fix bug where selected border color was not processed if a default border color was not provided in options
37
+ * Address several warnings during initialization
38
+ * Address multi-select callback inconsistencies
39
+ * Canvas renderer performance improvements
40
+ * ensure properties provided via `nvlOptions` are also updated on prop change in React wrappers
41
+
5
42
  ## [0.3.6] - 2025-02-21
6
43
 
7
44
  NVL 0.3.6 improves the API docs and adds a result transformer for the Neo4j JavaScript Driver, as well as small improvements and bug fixes.
@@ -43,7 +43,7 @@ describe('DragNodeInteraction', () => {
43
43
  const container = myNVL.getContainer();
44
44
  container.dispatchEvent(mouseDownEvent);
45
45
  container.dispatchEvent(mouseMoveEvent);
46
- container.dispatchEvent(mouseUpEvent);
46
+ document.body.dispatchEvent(mouseUpEvent);
47
47
  return new Promise((resolve) => {
48
48
  expect(dragStartCallbackMock).toHaveBeenCalledTimes(1);
49
49
  expect(dragStartCallbackMock).toHaveBeenCalledWith([{ id: '0', x: 10, y: 10 }], mouseMoveEvent);
@@ -79,7 +79,7 @@ describe('DragNodeInteraction', () => {
79
79
  const container = myNVL.getContainer();
80
80
  container.dispatchEvent(mouseDownEvent);
81
81
  container.dispatchEvent(mouseMoveEvent);
82
- container.dispatchEvent(mouseUpEvent);
82
+ document.body.dispatchEvent(mouseUpEvent);
83
83
  return new Promise((resolve) => {
84
84
  expect(dragStartCallbackMock).toHaveBeenCalledTimes(1);
85
85
  expect(dragStartCallbackMock).toHaveBeenCalledWith([
@@ -99,6 +99,34 @@ describe('DragNodeInteraction', () => {
99
99
  resolve();
100
100
  });
101
101
  });
102
+ test('dragging should not be invoked when the mouse is not on a node', () => {
103
+ dragNodeInteraction.updateCallback('onDragStart', dragStartCallbackMock);
104
+ dragNodeInteraction.updateCallback('onDrag', dragCallbackMock);
105
+ dragNodeInteraction.updateCallback('onDragEnd', dragEndCallbackMock);
106
+ const mouseDownEvent = new MouseEvent('mousedown', {
107
+ clientX: 100,
108
+ clientY: 100
109
+ });
110
+ const mouseMoveEvent = new MouseEvent('mousemove', {
111
+ buttons: 1,
112
+ clientX: 150,
113
+ clientY: 150
114
+ });
115
+ const mouseUpEvent = new MouseEvent('mouseup', {
116
+ clientX: 150,
117
+ clientY: 150
118
+ });
119
+ const container = myNVL.getContainer();
120
+ container.dispatchEvent(mouseDownEvent);
121
+ container.dispatchEvent(mouseMoveEvent);
122
+ document.body.dispatchEvent(mouseUpEvent);
123
+ return new Promise((resolve) => {
124
+ expect(dragStartCallbackMock).toHaveBeenCalledTimes(0);
125
+ expect(dragCallbackMock).toHaveBeenCalledTimes(0);
126
+ expect(dragEndCallbackMock).toHaveBeenCalledTimes(0);
127
+ resolve();
128
+ });
129
+ });
102
130
  test('dragging should not be invoked when the mouse is moved less than the drag threshold', () => {
103
131
  dragNodeInteraction.updateCallback('onDragStart', dragStartCallbackMock);
104
132
  dragNodeInteraction.updateCallback('onDrag', dragCallbackMock);
@@ -120,7 +148,7 @@ describe('DragNodeInteraction', () => {
120
148
  const container = myNVL.getContainer();
121
149
  container.dispatchEvent(mouseDownEvent);
122
150
  container.dispatchEvent(mouseMoveEvent);
123
- container.dispatchEvent(mouseUpEvent);
151
+ document.body.dispatchEvent(mouseUpEvent);
124
152
  return new Promise((resolve) => {
125
153
  expect(dragStartCallbackMock).toHaveBeenCalledTimes(0);
126
154
  expect(dragCallbackMock).toHaveBeenCalledTimes(0);
@@ -128,4 +156,36 @@ describe('DragNodeInteraction', () => {
128
156
  resolve();
129
157
  });
130
158
  });
159
+ test('dragging outside the container should stop dragging but keep selection disabled until mouse release', () => {
160
+ dragNodeInteraction.updateCallback('onDragStart', dragStartCallbackMock);
161
+ dragNodeInteraction.updateCallback('onDrag', dragCallbackMock);
162
+ dragNodeInteraction.updateCallback('onDragEnd', dragEndCallbackMock);
163
+ const mouseDownEvent = new MouseEvent('mousedown', {
164
+ clientX: 10,
165
+ clientY: 10
166
+ });
167
+ const mouseMoveEvent = new MouseEvent('mousemove', {
168
+ buttons: 1,
169
+ clientX: 20,
170
+ clientY: 20
171
+ });
172
+ const mouseLeaveEvent = new MouseEvent('mouseleave', {
173
+ clientX: 1,
174
+ clientY: 1
175
+ });
176
+ const bodyMouseUpEvent = new MouseEvent('mouseup');
177
+ const container = myNVL.getContainer();
178
+ container.dispatchEvent(mouseDownEvent);
179
+ container.dispatchEvent(mouseMoveEvent);
180
+ container.dispatchEvent(mouseLeaveEvent);
181
+ return new Promise((resolve) => {
182
+ expect(dragStartCallbackMock).toHaveBeenCalledTimes(1);
183
+ expect(dragCallbackMock).toHaveBeenCalledTimes(1);
184
+ expect(document.body.style.getPropertyValue('user-select')).toBe('none');
185
+ document.body.dispatchEvent(bodyMouseUpEvent);
186
+ expect(document.body.style.getPropertyValue('user-select')).toBe('');
187
+ expect(dragEndCallbackMock).toHaveBeenCalledTimes(1);
188
+ resolve();
189
+ });
190
+ });
131
191
  });
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom';
@@ -0,0 +1,115 @@
1
+ import NVL from '@neo4j-nvl/base';
2
+ import '@testing-library/jest-dom';
3
+ import { PanInteraction } from '../interaction-handlers/pan-interaction';
4
+ jest.mock('@neo4j-nvl/layout-workers');
5
+ describe('PanInteraction', () => {
6
+ let panInteraction;
7
+ let myNVL;
8
+ const panCallbackMock = jest.fn();
9
+ beforeEach(() => {
10
+ myNVL = new NVL(document.createElement('div'), [
11
+ { id: '0', x: 10, y: 10 },
12
+ { id: '1', x: 200, y: 200 }
13
+ ], [{ id: '10', from: '0', to: '1' }], { disableWebGL: true, initialZoom: 1, layout: 'free' });
14
+ panInteraction = new PanInteraction(myNVL);
15
+ });
16
+ afterEach(() => {
17
+ panInteraction.destroy();
18
+ myNVL.destroy();
19
+ panCallbackMock.mockReset();
20
+ });
21
+ test('performing a simple pan operation should invoke the expected callback', () => {
22
+ panInteraction.updateCallback('onPan', panCallbackMock);
23
+ const mouseDownEvent = new MouseEvent('mousedown', {
24
+ clientX: 100,
25
+ clientY: 100
26
+ });
27
+ const mouseMoveEvent = new MouseEvent('mousemove', {
28
+ buttons: 1,
29
+ clientX: 120,
30
+ clientY: 120
31
+ });
32
+ const mouseUpEvent = new MouseEvent('mouseup', {
33
+ clientX: 120,
34
+ clientY: 120
35
+ });
36
+ const container = myNVL.getContainer();
37
+ const initialPan = myNVL.getPan();
38
+ container.dispatchEvent(mouseDownEvent);
39
+ container.dispatchEvent(mouseMoveEvent);
40
+ container.dispatchEvent(mouseUpEvent);
41
+ return new Promise((resolve) => {
42
+ expect(panCallbackMock).toHaveBeenCalledTimes(1);
43
+ expect(panCallbackMock).toHaveBeenCalledWith({
44
+ x: initialPan.x - 20,
45
+ y: initialPan.y - 20
46
+ }, mouseMoveEvent);
47
+ const newPan = myNVL.getPan();
48
+ expect(newPan).not.toEqual(initialPan);
49
+ resolve();
50
+ });
51
+ });
52
+ test('panning should not work when clicking on nodes by default', () => {
53
+ panInteraction.updateCallback('onPan', panCallbackMock);
54
+ const mouseDownEvent = new MouseEvent('mousedown', {
55
+ clientX: 10,
56
+ clientY: 10
57
+ });
58
+ const mouseMoveEvent = new MouseEvent('mousemove', {
59
+ buttons: 1,
60
+ clientX: 30,
61
+ clientY: 30
62
+ });
63
+ const container = myNVL.getContainer();
64
+ container.dispatchEvent(mouseDownEvent);
65
+ container.dispatchEvent(mouseMoveEvent);
66
+ return new Promise((resolve) => {
67
+ expect(panCallbackMock).not.toHaveBeenCalled();
68
+ resolve();
69
+ });
70
+ });
71
+ test('should update targets to allow panning on nodes', () => {
72
+ panInteraction.updateCallback('onPan', panCallbackMock);
73
+ panInteraction.updateTargets(['node'], false);
74
+ const mouseDownEvent = new MouseEvent('mousedown', {
75
+ clientX: 10,
76
+ clientY: 10
77
+ });
78
+ const mouseMoveEvent = new MouseEvent('mousemove', {
79
+ buttons: 1,
80
+ clientX: 50,
81
+ clientY: 50
82
+ });
83
+ const container = myNVL.getContainer();
84
+ const initialPan = myNVL.getPan();
85
+ container.dispatchEvent(mouseDownEvent);
86
+ container.dispatchEvent(mouseMoveEvent);
87
+ return new Promise((resolve) => {
88
+ expect(panCallbackMock).toHaveBeenCalledTimes(1);
89
+ expect(panCallbackMock).toHaveBeenCalledWith({
90
+ x: initialPan.x - 40,
91
+ y: initialPan.y - 40
92
+ }, mouseMoveEvent);
93
+ resolve();
94
+ });
95
+ });
96
+ test('panning outside the container should disable text selection until mouse release', () => {
97
+ panInteraction.updateCallback('onPan', panCallbackMock);
98
+ const mouseDownEvent = new MouseEvent('mousedown', {
99
+ clientX: 100,
100
+ clientY: 100
101
+ });
102
+ const mouseMoveEvent = new MouseEvent('mousemove', {
103
+ buttons: 1,
104
+ clientX: 120,
105
+ clientY: 120
106
+ });
107
+ const bodyMouseUpEvent = new MouseEvent('mouseup');
108
+ const container = myNVL.getContainer();
109
+ container.dispatchEvent(mouseDownEvent);
110
+ container.dispatchEvent(mouseMoveEvent);
111
+ expect(document.body.style.getPropertyValue('user-select')).toBe('none');
112
+ document.body.dispatchEvent(bodyMouseUpEvent);
113
+ expect(document.body.style.getPropertyValue('user-select')).toBe('');
114
+ });
115
+ });
@@ -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 };
@@ -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 handleMouseUp;
47
+ private handleBodyMouseUp;
48
+ private resetState;
48
49
  /**
49
50
  * Removes all related event listeners from the container.
50
51
  */
@@ -28,7 +28,6 @@ export class DragNodeInteraction extends BaseInteraction {
28
28
  this.moveSelectedNodes = false;
29
29
  this.addEventListener('mousedown', this.handleMouseDown);
30
30
  this.addEventListener('mousemove', this.handleMouseMove);
31
- this.addEventListener('mouseup', this.handleMouseUp);
32
31
  }
33
32
  handleMouseDown = (event) => {
34
33
  this.mousePosition = { x: event.clientX, y: event.clientY };
@@ -62,6 +61,7 @@ export class DragNodeInteraction extends BaseInteraction {
62
61
  return;
63
62
  }
64
63
  if (!this.isDragging) {
64
+ this.toggleGlobalTextSelection(false, this.handleBodyMouseUp);
65
65
  if (this.moveSelectedNodes) {
66
66
  this.callCallbackIfRegistered('onDragStart', this.selectedNodes, evt);
67
67
  }
@@ -89,15 +89,21 @@ export class DragNodeInteraction extends BaseInteraction {
89
89
  this.callCallbackIfRegistered('onDrag', [this.mouseDownNode.data], evt);
90
90
  }
91
91
  };
92
- handleMouseUp = (evt) => {
93
- if (this.isDragging) {
92
+ handleBodyMouseUp = (evt) => {
93
+ this.toggleGlobalTextSelection(true, this.handleBodyMouseUp);
94
+ if (this.isDragging && this.mouseDownNode !== null) {
94
95
  if (this.moveSelectedNodes) {
95
96
  this.callCallbackIfRegistered('onDragEnd', this.selectedNodes, evt);
96
97
  }
97
98
  else {
98
- this.callCallbackIfRegistered('onDragEnd', [this.mouseDownNode?.data], evt);
99
+ this.callCallbackIfRegistered('onDragEnd', [this.mouseDownNode.data], evt);
99
100
  }
100
101
  }
102
+ if (this.isDragging) {
103
+ this.resetState();
104
+ }
105
+ };
106
+ resetState = () => {
101
107
  this.isDragging = false;
102
108
  this.mouseDownNode = null;
103
109
  this.isDrawing = false;
@@ -108,8 +114,8 @@ export class DragNodeInteraction extends BaseInteraction {
108
114
  * Removes all related event listeners from the container.
109
115
  */
110
116
  destroy = () => {
117
+ this.toggleGlobalTextSelection(true, this.handleBodyMouseUp);
111
118
  this.removeEventListener('mousedown', this.handleMouseDown);
112
119
  this.removeEventListener('mousemove', this.handleMouseMove);
113
- this.removeEventListener('mouseup', this.handleMouseUp);
114
120
  };
115
121
  }
@@ -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
@@ -10,6 +10,7 @@ export class PanInteraction extends BaseInteraction {
10
10
  mousePosition;
11
11
  targets;
12
12
  shouldPan;
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
@@ -61,6 +62,10 @@ export class PanInteraction extends BaseInteraction {
61
62
  if (!this.shouldPan || evt.buttons !== 1) {
62
63
  return;
63
64
  }
65
+ if (!this.isPanning) {
66
+ this.toggleGlobalTextSelection(false, this.handleMouseUp);
67
+ this.isPanning = true;
68
+ }
64
69
  const zoom = this.nvlInstance.getScale();
65
70
  const { x, y } = this.nvlInstance.getPan();
66
71
  const dx = ((evt.clientX - this.mousePosition.x) / zoom) * window.devicePixelRatio;
@@ -70,12 +75,17 @@ export class PanInteraction extends BaseInteraction {
70
75
  this.mousePosition = { x: evt.clientX, y: evt.clientY };
71
76
  };
72
77
  handleMouseUp = () => {
78
+ if (this.isPanning) {
79
+ this.toggleGlobalTextSelection(true, this.handleMouseUp);
80
+ this.isPanning = false;
81
+ }
73
82
  this.shouldPan = false;
74
83
  };
75
84
  /**
76
85
  * Removes the related event listeners from the canvas.
77
86
  */
78
87
  destroy() {
88
+ this.toggleGlobalTextSelection(true, this.handleMouseUp);
79
89
  this.removeEventListener('mousedown', this.handleMouseDown, true);
80
90
  this.removeEventListener('mousemove', this.handleMouseMove, true);
81
91
  this.removeEventListener('mouseup', this.handleMouseUp, true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neo4j-nvl/interaction-handlers",
3
- "version": "0.3.7",
3
+ "version": "0.3.8-15dd54ce",
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.7",
30
+ "@neo4j-nvl/base": "0.3.8-15dd54ce",
31
31
  "concaveman": "^1.2.1",
32
32
  "lodash": "4.17.21"
33
33
  },
@@ -36,5 +36,6 @@
36
36
  "@testing-library/react": "^13.4.0",
37
37
  "@types/concaveman": "1.1.6",
38
38
  "@types/lodash": "4.14.202"
39
- }
39
+ },
40
+ "stableVersion": "0.3.8"
40
41
  }