@elicdavis/node-flow 0.1.17 → 0.1.18

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@elicdavis/node-flow",
3
3
  "description": "build node graphs",
4
4
  "author": "Eli C Davis",
5
- "version": "0.1.17",
5
+ "version": "0.1.18",
6
6
  "license": "MIT",
7
7
  "keywords": [
8
8
  "nodes",
package/src/node.ts CHANGED
@@ -1334,6 +1334,9 @@ export class FlowNode {
1334
1334
 
1335
1335
  project(this.#portOffsets.input, this.#inputPortPositions);
1336
1336
  project(this.#portOffsets.output, this.#outputPortPositions);
1337
+
1338
+ this.#widgetPositions.Clear();
1339
+
1337
1340
  return true;
1338
1341
  }
1339
1342
  }
@@ -448,7 +448,9 @@ export class NodeSubsystem {
448
448
  }
449
449
 
450
450
  if (this.#widgetCurrentlyClicking !== null) {
451
- this.#widgetCurrentlyClicking.ClickEnd();
451
+ if (this.#widgetHovering === this.#widgetCurrentlyClicking) {
452
+ this.#widgetCurrentlyClicking.ClickEnd();
453
+ }
452
454
  this.#widgetCurrentlyClicking = null;
453
455
  }
454
456
 
package/src/popup.ts CHANGED
@@ -25,6 +25,8 @@ export class Popup {
25
25
 
26
26
  #popup: HTMLDivElement | null;
27
27
 
28
+ #keyHandler: ((e: KeyboardEvent) => void) | null;
29
+
28
30
  constructor(config: PopupConfig) {
29
31
  this.#title = config.title;
30
32
  this.#options = config.options;
@@ -32,16 +34,28 @@ export class Popup {
32
34
  this.#onClose = config.onClose;
33
35
  this.#buttonCSS = config.buttonCSS === undefined ? "flex-grow: 1; margin-right: 8px;" : config.buttonCSS;
34
36
  this.#popup = null;
37
+ this.#keyHandler = null;
35
38
  }
36
39
 
37
40
  Hide(): void {
38
41
  if (this.#popup === null) {
39
42
  return;
40
43
  }
44
+ if (this.#keyHandler !== null) {
45
+ document.removeEventListener("keydown", this.#keyHandler, true);
46
+ this.#keyHandler = null;
47
+ }
41
48
  document.body.removeChild(this.#popup);
42
49
  this.#popup = null;
43
50
  }
44
51
 
52
+ #close(option: string | null): void {
53
+ this.Hide();
54
+ if (this.#onClose !== undefined) {
55
+ this.#onClose(option);
56
+ }
57
+ }
58
+
45
59
  Show(): void {
46
60
  this.#popup = document.createElement('div');
47
61
  this.#popup.style.cssText = 'position:absolute;top:0;width:100%;display:flex;height:100%;justify-content:center;align-items:center;background-color:#00000070;';
@@ -68,14 +82,21 @@ export class Popup {
68
82
  const button = document.createElement("button");
69
83
  button.style.cssText = this.#buttonCSS;
70
84
  button.textContent = this.#options[i];
71
- button.onclick = () => {
72
- this.Hide();
73
- if (this.#onClose !== undefined) {
74
- this.#onClose(this.#options[i]);
75
- }
76
- };
85
+ button.onclick = () => this.#close(this.#options[i]);
77
86
  buttonContainer.appendChild(button);
78
87
  }
88
+
89
+ this.#keyHandler = (e: KeyboardEvent): void => {
90
+ if (e.key === "Enter") {
91
+ e.preventDefault();
92
+ this.#close(this.#options.length > 0 ? this.#options[0] : null);
93
+ } else if (e.key === "Escape") {
94
+ e.preventDefault();
95
+ this.#close(null);
96
+ }
97
+ e.stopPropagation();
98
+ };
99
+ document.addEventListener("keydown", this.#keyHandler, true);
79
100
  }
80
101
 
81
102
  }
package/src/theme.ts CHANGED
@@ -60,7 +60,7 @@ export const Theme = {
60
60
  DisabledFontColor: "#4a585c",
61
61
  },
62
62
  Minimap: {
63
- BackgroundColor: "rgba(7, 33, 42, 0.85)",
63
+ BackgroundColor: "rgba(10, 16, 18, 0.85)",
64
64
  BorderColor: "#1c1c1c",
65
65
  BorderRadius: 4,
66
66
  Padding: 6,
@@ -62,9 +62,10 @@ export class NumberWidget {
62
62
  },
63
63
  text: { color: Theme.Widget.FontColor },
64
64
  }));
65
- this.#callback = config?.callback;
66
65
  this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, 0));
67
66
 
67
+ this.#callback = config?.callback;
68
+
68
69
  if (this.#nodeProperty !== undefined) {
69
70
  this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
70
71
  this.Set(newVal);
@@ -60,8 +60,10 @@ export class StringWidget {
60
60
  text: { color: Theme.Widget.FontColor },
61
61
  }));
62
62
 
63
- this.#callback = config?.callback;
64
63
  this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, ""));
64
+
65
+ this.#callback = config?.callback;
66
+
65
67
  if (this.#nodeProperty !== undefined) {
66
68
  this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
67
69
  this.Set(newVal);
@@ -159,8 +159,10 @@ export class ToggleWidget {
159
159
  lightColor: config?.disabledStyle?.lightColor === undefined ? "#004400" : config?.enabledStyle?.lightColor,
160
160
  });
161
161
 
162
- this.#callback = config?.callback;
163
162
  this.Set(startingWidgetValue(node, this.#nodeProperty, config?.value, false));
163
+
164
+ this.#callback = config?.callback;
165
+
164
166
  if (this.#nodeProperty !== undefined) {
165
167
  this.#node.addPropertyChangeListener(this.#nodeProperty, (oldVal, newVal) => {
166
168
  this.Set(newVal);
@@ -0,0 +1,104 @@
1
+ import { strict as assert } from 'assert';
2
+ import { Popup } from '../src/popup';
3
+
4
+ interface StubElement {
5
+ tagName: string;
6
+ style: { cssText: string };
7
+ textContent: string;
8
+ children: Array<StubElement>;
9
+ onclick?: () => void;
10
+ appendChild(child: StubElement): StubElement;
11
+ append(child: StubElement): void;
12
+ removeChild(child: StubElement): void;
13
+ }
14
+
15
+ function element(tagName: string): StubElement {
16
+ return {
17
+ tagName,
18
+ style: { cssText: '' },
19
+ textContent: '',
20
+ children: [],
21
+ appendChild(child) { this.children.push(child); return child; },
22
+ append(child) { this.children.push(child); },
23
+ removeChild(child) { this.children = this.children.filter((c) => c !== child); },
24
+ };
25
+ }
26
+
27
+ function withDocument(run: (fire: (key: string) => void, body: StubElement) => void): void {
28
+ const listeners: Array<(e: unknown) => void> = [];
29
+ const body = element('body');
30
+
31
+ const g = globalThis as any;
32
+ const previous = g.document;
33
+ g.document = {
34
+ body,
35
+ createElement: element,
36
+ addEventListener: (_: string, fn: (e: unknown) => void) => listeners.push(fn),
37
+ removeEventListener: (_: string, fn: (e: unknown) => void) => {
38
+ const at = listeners.indexOf(fn);
39
+ if (at >= 0) listeners.splice(at, 1);
40
+ },
41
+ };
42
+
43
+ const fire = (key: string) => {
44
+ const event = { key, preventDefault() {}, stopPropagation() {} };
45
+ [...listeners].forEach((fn) => fn(event));
46
+ };
47
+
48
+ try {
49
+ run(fire, body);
50
+ } finally {
51
+ g.document = previous;
52
+ }
53
+ }
54
+
55
+ describe('popup', () => {
56
+ it('confirms with the first option when enter is pressed', () => {
57
+ withDocument((fire) => {
58
+ let closedWith: string | null | undefined;
59
+ new Popup({
60
+ title: 'Set Number',
61
+ options: ['Set', 'Cancel'],
62
+ onClose: (button) => { closedWith = button; },
63
+ }).Show();
64
+
65
+ fire('Enter');
66
+
67
+ assert.equal(closedWith, 'Set', 'enter has to commit, not discard what was typed');
68
+ });
69
+ });
70
+
71
+ it('cancels when escape is pressed', () => {
72
+ withDocument((fire) => {
73
+ let called = false;
74
+ let closedWith: string | null | undefined;
75
+ new Popup({
76
+ title: 'Set Number',
77
+ options: ['Set', 'Cancel'],
78
+ onClose: (button) => { called = true; closedWith = button; },
79
+ }).Show();
80
+
81
+ fire('Escape');
82
+
83
+ assert.equal(called, true);
84
+ assert.equal(closedWith, null);
85
+ });
86
+ });
87
+
88
+ it('stops listening once it has closed', () => {
89
+ withDocument((fire, body) => {
90
+ let closes = 0;
91
+ new Popup({
92
+ title: 'Set Number',
93
+ options: ['Set'],
94
+ onClose: () => { closes++; },
95
+ }).Show();
96
+
97
+ fire('Enter');
98
+ fire('Enter');
99
+
100
+ assert.equal(closes, 1);
101
+ assert.equal(body.children.length, 0, 'and takes itself off the page');
102
+ });
103
+ });
104
+ });
@@ -0,0 +1,126 @@
1
+ import { strict as assert } from 'assert';
2
+ import { NodeSubsystem } from '../src/nodes/subsystem';
3
+ import { PassSubsystem } from '../src/pass/subsystem';
4
+ import { FlowNode } from '../src/node';
5
+ import { Camera } from '../src/camera';
6
+ import { Box } from '../src/types/box';
7
+ import { Vector2 } from '../src/types/vector2';
8
+
9
+ function stubCtx(): CanvasRenderingContext2D {
10
+ const ctx: any = {
11
+ canvas: { width: 800, height: 600 },
12
+ lineWidth: 1,
13
+ font: '',
14
+ textAlign: 'left',
15
+ textBaseline: 'top',
16
+ fillStyle: '',
17
+ strokeStyle: '',
18
+ globalAlpha: 1,
19
+ measureText: () => ({ width: 40 }),
20
+ save: () => { }, restore: () => { }, translate: () => { }, scale: () => { },
21
+ beginPath: () => { }, closePath: () => { }, roundRect: () => { }, rect: () => { },
22
+ moveTo: () => { }, lineTo: () => { }, arc: () => { }, ellipse: () => { },
23
+ bezierCurveTo: () => { }, quadraticCurveTo: () => { },
24
+ fill: () => { }, stroke: () => { }, fillText: () => { }, strokeText: () => { },
25
+ clip: () => { }, drawImage: () => { }, setLineDash: () => { },
26
+ createLinearGradient: () => ({ addColorStop: () => { } }),
27
+ };
28
+ return ctx as CanvasRenderingContext2D;
29
+ }
30
+
31
+ function recordingWidget() {
32
+ let clicks = 0;
33
+ let drawnAt: Box | null = null;
34
+ const widget = {
35
+ Size: (): Vector2 => ({ x: 100, y: 20 }),
36
+ Draw: (_ctx: CanvasRenderingContext2D, position: Vector2, scale: number): Box => {
37
+ drawnAt = { Position: { x: position.x, y: position.y }, Size: { x: 100 * scale, y: 20 * scale } };
38
+ return drawnAt;
39
+ },
40
+ ClickStart: (): void => { },
41
+ ClickEnd: (): void => { clicks++; },
42
+ };
43
+ return {
44
+ widget,
45
+ clicks: () => clicks,
46
+ center: (): Vector2 => {
47
+ assert.ok(drawnAt !== null, 'widget was never drawn');
48
+ const b = drawnAt as Box;
49
+ return { x: b.Position.x + b.Size.x / 2, y: b.Position.y + b.Size.y / 2 };
50
+ },
51
+ };
52
+ }
53
+
54
+ function scene() {
55
+ const graph = new NodeSubsystem(new PassSubsystem());
56
+ const node = new FlowNode({ title: 'test', position: { x: 100, y: 100 } });
57
+ const rec = recordingWidget();
58
+ node.addWidget(rec.widget as any);
59
+ graph.addNode(node);
60
+
61
+ const camera = new Camera();
62
+ const ctx = stubCtx();
63
+ return { graph, camera, ctx, rec };
64
+ }
65
+
66
+ describe('clicking a widget', () => {
67
+ it('opens it when the press and release are both on it', () => {
68
+ const { graph, camera, ctx, rec } = scene();
69
+ graph.render(ctx, camera, undefined);
70
+ const on = rec.center();
71
+
72
+ graph.render(ctx, camera, on);
73
+ graph.clickStart(on, camera, false);
74
+ graph.render(ctx, camera, on);
75
+ graph.clickEnd();
76
+
77
+ assert.equal(rec.clicks(), 1);
78
+ });
79
+
80
+ it('leaves it alone when the gesture drags away before releasing', () => {
81
+ const { graph, camera, ctx, rec } = scene();
82
+ graph.render(ctx, camera, undefined);
83
+ const on = rec.center();
84
+
85
+ graph.render(ctx, camera, on);
86
+ graph.clickStart(on, camera, false);
87
+ graph.render(ctx, camera, { x: on.x + 400, y: on.y + 300 });
88
+ graph.clickEnd();
89
+
90
+ assert.equal(rec.clicks(), 0);
91
+ });
92
+ });
93
+
94
+ describe('a node panned off screen', () => {
95
+ it('stops hit-testing as a widget where it used to be drawn', () => {
96
+ const graph = new NodeSubsystem(new PassSubsystem());
97
+ const node = new FlowNode({
98
+ title: 'test',
99
+ position: { x: 100, y: 100 },
100
+ inputs: [{ name: 'In', type: 'float64' }],
101
+ });
102
+ const rec = recordingWidget();
103
+ node.addWidget(rec.widget as any);
104
+ graph.addNode(node);
105
+
106
+ const camera = new Camera();
107
+ const ctx = stubCtx();
108
+
109
+ graph.render(ctx, camera, undefined);
110
+ const on = rec.center();
111
+
112
+ camera.position.x = -50000;
113
+ graph.render(ctx, camera, undefined);
114
+
115
+ assert.equal(
116
+ node.inBounds(ctx, camera, on).Widget,
117
+ undefined,
118
+ 'a node that was not drawn has no widget under any screen point',
119
+ );
120
+
121
+ graph.clickStart(on, camera, false);
122
+ graph.clickEnd();
123
+
124
+ assert.equal(rec.clicks(), 0);
125
+ });
126
+ });
@@ -0,0 +1,48 @@
1
+ import { strict as assert } from 'assert';
2
+ import { NumberWidget } from '../src/widgets/number';
3
+ import { StringWidget } from '../src/widgets/string';
4
+ import { ToggleWidget } from '../src/widgets/toggle';
5
+ import { SliderWidget } from '../src/widgets/slider';
6
+ import { FlowNode } from '../src/node';
7
+
8
+ function stubNode(): FlowNode {
9
+ const properties = new Map<string, unknown>();
10
+ return {
11
+ getProperty: (name: string) => properties.get(name),
12
+ setProperty: (name: string, value: unknown) => { properties.set(name, value); },
13
+ addPropertyChangeListener: () => { },
14
+ } as unknown as FlowNode;
15
+ }
16
+
17
+ describe('widget construction', () => {
18
+ it('does not fire a number callback for its initial value', () => {
19
+ let fired = 0;
20
+ new NumberWidget(stubNode(), { value: 42, callback: () => { fired++; } });
21
+ assert.equal(fired, 0);
22
+ });
23
+
24
+ it('does not fire a string callback for its initial value', () => {
25
+ let fired = 0;
26
+ new StringWidget(stubNode(), { value: 'hello', callback: () => { fired++; } });
27
+ assert.equal(fired, 0);
28
+ });
29
+
30
+ it('does not fire a toggle callback for its initial value', () => {
31
+ let fired = 0;
32
+ new ToggleWidget(stubNode(), { value: true, callback: () => { fired++; } });
33
+ assert.equal(fired, 0);
34
+ });
35
+
36
+ it('does not fire a slider callback for its initial value', () => {
37
+ let fired = 0;
38
+ new SliderWidget(stubNode(), { value: 7, min: 0, max: 10, change: () => { fired++; } });
39
+ assert.equal(fired, 0);
40
+ });
41
+
42
+ it('still fires the number callback on a real change', () => {
43
+ const values: Array<number> = [];
44
+ const widget = new NumberWidget(stubNode(), { value: 42, callback: (v) => { values.push(v); } });
45
+ widget.Set(43);
46
+ assert.deepEqual(values, [43]);
47
+ });
48
+ });