dragon-graph-lib 0.1.2 → 0.1.3

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dragon-graph-lib",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "JS Library to create node-based editors",
5
5
  "homepage": "https://gitlab.com/mattbas/dragon-graph-lib",
6
6
  "bugs": {
@@ -205,11 +205,6 @@ class Connector extends VisualElement
205
205
  this.dom_items = {};
206
206
  }
207
207
 
208
- update_dom()
209
- {
210
- this.dom_items.label.textContent = this.label;
211
- }
212
-
213
208
  /**
214
209
  * Adds a connection to the connector
215
210
  * @param {Connection} connection
@@ -265,13 +260,10 @@ class NodeInput extends Connector
265
260
  container.classList.add("dgl-socketed");
266
261
  }
267
262
 
268
- this.dom_items.label = document.createElement("span");
269
- this.dom_items.label.textContent = this.name;
270
- this.dom_items.label.classList.add("dgl-label", "dgl-input-label");
271
- container.appendChild(this.dom_items.label);
272
-
273
263
  if ( this.control )
274
- container.appendChild(this.control);
264
+ container.appendChild(this.control.to_dom(this.label, this.name));
265
+ else
266
+ container.appendChild(NodeControl.make_label(this.label));
275
267
 
276
268
  return container;
277
269
  }
@@ -288,7 +280,7 @@ class NodeInput extends Connector
288
280
  this.connections.push(connection);
289
281
 
290
282
  if ( this.control )
291
- this.control.style.visibility = "hidden";
283
+ this.control.hide();
292
284
  }
293
285
 
294
286
  /**
@@ -299,7 +291,7 @@ class NodeInput extends Connector
299
291
  {
300
292
  if ( this.control )
301
293
  {
302
- this.control.value = value;
294
+ this.control.set_value(value);
303
295
  }
304
296
  }
305
297
 
@@ -330,7 +322,7 @@ class NodeInput extends Connector
330
322
  return null;
331
323
  }
332
324
 
333
- let value = this.control.value
325
+ let value = this.control.get_value();
334
326
  if ( this.socket && this.socket.multi_input )
335
327
  return [value];
336
328
  return value;
@@ -340,7 +332,7 @@ class NodeInput extends Connector
340
332
  {
341
333
  super.disconnect(connection);
342
334
  if ( this.control && this.connections.length == 0 )
343
- this.control.style.visibility = "visible";
335
+ this.control.show();
344
336
  }
345
337
  }
346
338
 
@@ -402,7 +394,7 @@ class NodeOutput extends Connector
402
394
  */
403
395
  get_value()
404
396
  {
405
- if ( this.value == undefined )
397
+ if ( this.value === undefined || this.node.dirty )
406
398
  this.node.evaluate();
407
399
  return this.value;
408
400
  }
@@ -415,23 +407,199 @@ function force_default(ev)
415
407
  }
416
408
 
417
409
  /**
418
- * Creates a node control
419
- * @param {string} type Input type
420
- * @param {object} attrs Input attributes as an object
421
- * @returns {HTMLInputElement}
410
+ * Custom control interface
422
411
  */
423
- function NodeControl(type, attrs={})
412
+ class NodeControl
424
413
  {
425
- let elem = document.createElement("input");
426
- elem.type = type;
427
- elem.classList.add("dgl-control");
428
- for ( let [k, v] of Object.entries(attrs) )
429
- elem.setAttribute(k, v);
430
- elem.addEventListener("mousedown", force_default);
431
- elem.addEventListener("mouseup", force_default);
432
- elem.addEventListener("mousemove", force_default);
433
- elem.addEventListener("wheel", force_default);
434
- return elem;
414
+ /**
415
+ * @param {object} config Control config
416
+ * @param {any} config.default Control default value
417
+ */
418
+ constructor(config={})
419
+ {
420
+ this.on_change = null;
421
+ this.config = config;
422
+ }
423
+
424
+ to_dom(label, name)
425
+ {
426
+ this.element = document.createElement("div");
427
+ this.element.classList.add("dgl-control-parent");
428
+ if ( label.length )
429
+ this.element.appendChild(NodeControl.make_label(label));
430
+ this.setup_dom(this.element, name);
431
+ if ( "default" in this.config )
432
+ this.set_value(this.config.default);
433
+ return this.element;
434
+ }
435
+
436
+ /**
437
+ * Sets up the DOM
438
+ * @param {HTMLElement} parent Element to add additional content to
439
+ * @param {name} name Control name
440
+ */
441
+ setup_dom(_parent, _name)
442
+ {
443
+ }
444
+
445
+ /**
446
+ * Dispatches the change event to update the graph
447
+ */
448
+ notify_change()
449
+ {
450
+ this.on_change();
451
+ }
452
+
453
+ /**
454
+ * Override to return the control value
455
+ * @returns {any} value
456
+ */
457
+ get_value()
458
+ {
459
+ }
460
+
461
+ /**
462
+ * Override to return set a value to the control
463
+ * @param {any} value
464
+ */
465
+ set_value(_value)
466
+ {
467
+ }
468
+
469
+ /**
470
+ * Ensures an element uses the default mouse interactions instead of
471
+ * propagating to the node editor
472
+ */
473
+ use_default_events(element)
474
+ {
475
+ element.addEventListener("mousedown", force_default);
476
+ element.addEventListener("mouseup", force_default);
477
+ element.addEventListener("mousemove", force_default);
478
+ element.addEventListener("wheel", force_default);
479
+ }
480
+
481
+ /**
482
+ * Creates a label element
483
+ * @param {string} label Text to display
484
+ */
485
+ static make_label(label)
486
+ {
487
+ let elem = document.createElement("span");
488
+ elem.textContent = label;
489
+ elem.classList.add("dgl-label", "dgl-input-label");
490
+ return elem;
491
+ }
492
+
493
+ set_visibility(value)
494
+ {
495
+ for ( let ch of this.element.children )
496
+ if ( !ch.classList.contains("dgl-label") )
497
+ ch.style.visibility = value;
498
+ }
499
+
500
+ /**
501
+ * Hides the control when an input connection is attached
502
+ */
503
+ hide()
504
+ {
505
+ this.set_visibility("hidden");
506
+ }
507
+
508
+ /**
509
+ * Shows the control when an input connection is detached
510
+ */
511
+ show()
512
+ {
513
+ this.set_visibility("visible");
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Control for simple input elements
519
+ */
520
+ class InputControl extends NodeControl
521
+ {
522
+ /**
523
+ * @param {object} config Control config
524
+ * @param {string} config.type Input type
525
+ * @param {object} config.attrs Input attributes
526
+ * @param {any} config.default Default value
527
+ */
528
+ constructor(config={type: "text"})
529
+ {
530
+ super(config);
531
+ }
532
+
533
+ setup_dom(parent, name)
534
+ {
535
+ this.input = document.createElement("input");
536
+ parent.appendChild(this.input);
537
+ this.input.type = this.config.type;
538
+ this.input.classList.add("dgl-control");
539
+ for ( let [k, v] of Object.entries(this.config.attrs ?? {}) )
540
+ this.input.setAttribute(k, v);
541
+ this.use_default_events(this.input);
542
+ this.input.name = name;
543
+ this.input.addEventListener("input", () => this.notify_change());
544
+ }
545
+
546
+ get_value()
547
+ {
548
+ if ( this.config.type == "checkbox" )
549
+ return this.input.checked;
550
+ return this.input.value;
551
+ }
552
+
553
+ set_value(value)
554
+ {
555
+ if ( this.config.type == "checkbox" )
556
+ this.input.checked = value;
557
+ else
558
+ this.input.value = value;
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Drop-down Control
564
+ */
565
+ class DropDownControl extends NodeControl
566
+ {
567
+
568
+ /**
569
+ * @param {object} config Control config
570
+ * @param {Array.<string, string>} config.options Drop down options ([value, display])
571
+ * @param {any} config.default Default value
572
+ */
573
+ constructor(config={options: []})
574
+ {
575
+ super(config);
576
+ }
577
+
578
+ setup_dom(parent, name)
579
+ {
580
+ this.input = document.createElement("select");
581
+ for ( let [value, display] of this.config.options )
582
+ {
583
+ let option = document.createElement("option");
584
+ this.input.appendChild(option);
585
+ option.value = value;
586
+ option.textContent = display ?? value;
587
+ }
588
+ this.input.name = name;
589
+ parent.appendChild(this.input);
590
+ this.use_default_events(this.input);
591
+ this.input.addEventListener("input", () => this.notify_change());
592
+ }
593
+
594
+ get_value()
595
+ {
596
+ return this.input.value;
597
+ }
598
+
599
+ set_value(value)
600
+ {
601
+ this.input.value = value;
602
+ }
435
603
  }
436
604
 
437
605
  /**
@@ -473,6 +641,7 @@ class Node extends VisualElement
473
641
  * @member {number}
474
642
  */
475
643
  this.y = 0;
644
+ this.dirty = true;
476
645
  }
477
646
 
478
647
  /**
@@ -575,6 +744,7 @@ class Node extends VisualElement
575
744
 
576
745
  evaluate()
577
746
  {
747
+ this.dirty = false;
578
748
  if ( this.type )
579
749
  this.type.evaluate_node(this);
580
750
  }
@@ -694,11 +864,17 @@ class ContextMenu extends VisualElement
694
864
 
695
865
  to_dom(editor)
696
866
  {
697
- this.dom = this.make_menu("dgl-menu", this.actions, editor);
698
- this.dom.style.display = "none";
867
+ this.dom = this.build(editor);
699
868
  return this.dom;
700
869
  }
701
870
 
871
+ build(editor)
872
+ {
873
+ let dom = this.make_menu("dgl-menu", this.actions, editor);
874
+ dom.style.display = "none";
875
+ return dom;
876
+ }
877
+
702
878
  make_action(title, action, editor)
703
879
  {
704
880
  let onclick = action;
@@ -773,6 +949,18 @@ class ContextMenu extends VisualElement
773
949
  {
774
950
  this.dom.style.display = "none";
775
951
  }
952
+
953
+ /**
954
+ * Rebuilds the DOM element
955
+ */
956
+ rebuild(editor)
957
+ {
958
+ let parent = this.dom.parentNode;
959
+ let old_e = this.dom;
960
+ let new_e = this.build(editor);
961
+ parent.replaceChild(new_e, old_e);
962
+ this.dom = new_e;
963
+ }
776
964
  }
777
965
 
778
966
  /**
@@ -845,7 +1033,6 @@ class Editor
845
1033
  this.container.appendChild(this.context_menu.to_dom(this));
846
1034
  }
847
1035
 
848
-
849
1036
  /**
850
1037
  * Adds a new node to the graph
851
1038
  * @param {Node} node
@@ -865,7 +1052,10 @@ class Editor
865
1052
  {
866
1053
  if ( input.control )
867
1054
  {
868
- input.control.addEventListener("change", () => this._refresh_graph());
1055
+ input.control.on_change = () => {
1056
+ node.dirty = true;
1057
+ this._refresh_graph();
1058
+ };
869
1059
  }
870
1060
  }
871
1061
  }
@@ -896,8 +1086,9 @@ class Editor
896
1086
  create_node(type, data={})
897
1087
  {
898
1088
  let node = type.create_node();
899
- node.set_input_value(data);
900
1089
  this.add_node(node);
1090
+ node.set_input_value(data);
1091
+ this._refresh_graph();
901
1092
  return node;
902
1093
  }
903
1094
 
@@ -1333,6 +1524,8 @@ Connection,
1333
1524
  NodeInput,
1334
1525
  NodeOutput,
1335
1526
  NodeControl,
1527
+ InputControl,
1528
+ DropDownControl,
1336
1529
  Node,
1337
1530
  NodeType,
1338
1531
  ContextMenu,
@@ -51,7 +51,7 @@
51
51
  width: fit-content;
52
52
  display: flex;
53
53
  flex-flow: column;
54
- gap: 12px;
54
+ gap: var(--dgl-node-padding);
55
55
  cursor: pointer;
56
56
  user-select: none;
57
57
  border: 1px solid var(--dgl-node-text);
@@ -80,6 +80,7 @@
80
80
  stroke-width: var( --dgl-connection-width);
81
81
  fill: none;
82
82
  }
83
+
83
84
  .dgl-preview {
84
85
  max-width: 20ch;
85
86
  }
@@ -100,6 +101,10 @@
100
101
  position: relative;
101
102
  }
102
103
 
104
+ .dgl-input-label {
105
+ margin-right: var(--dgl-node-padding);
106
+ }
107
+
103
108
  .dgl-output {
104
109
  justify-content: end;
105
110
  left: calc(var(--dgl-node-padding) + var(--dgl-socket-size) / 2);
@@ -204,11 +204,6 @@ class Connector extends VisualElement
204
204
  this.dom_items = {};
205
205
  }
206
206
 
207
- update_dom()
208
- {
209
- this.dom_items.label.textContent = this.label;
210
- }
211
-
212
207
  /**
213
208
  * Adds a connection to the connector
214
209
  * @param {Connection} connection
@@ -264,13 +259,10 @@ export class NodeInput extends Connector
264
259
  container.classList.add("dgl-socketed");
265
260
  }
266
261
 
267
- this.dom_items.label = document.createElement("span");
268
- this.dom_items.label.textContent = this.name;
269
- this.dom_items.label.classList.add("dgl-label", "dgl-input-label");
270
- container.appendChild(this.dom_items.label);
271
-
272
262
  if ( this.control )
273
- container.appendChild(this.control);
263
+ container.appendChild(this.control.to_dom(this.label, this.name));
264
+ else
265
+ container.appendChild(NodeControl.make_label(this.label));
274
266
 
275
267
  return container;
276
268
  }
@@ -287,7 +279,7 @@ export class NodeInput extends Connector
287
279
  this.connections.push(connection);
288
280
 
289
281
  if ( this.control )
290
- this.control.style.visibility = "hidden";
282
+ this.control.hide();
291
283
  }
292
284
 
293
285
  /**
@@ -298,7 +290,7 @@ export class NodeInput extends Connector
298
290
  {
299
291
  if ( this.control )
300
292
  {
301
- this.control.value = value;
293
+ this.control.set_value(value);
302
294
  }
303
295
  }
304
296
 
@@ -329,7 +321,7 @@ export class NodeInput extends Connector
329
321
  return null;
330
322
  }
331
323
 
332
- let value = this.control.value
324
+ let value = this.control.get_value();
333
325
  if ( this.socket && this.socket.multi_input )
334
326
  return [value];
335
327
  return value;
@@ -339,7 +331,7 @@ export class NodeInput extends Connector
339
331
  {
340
332
  super.disconnect(connection);
341
333
  if ( this.control && this.connections.length == 0 )
342
- this.control.style.visibility = "visible";
334
+ this.control.show();
343
335
  }
344
336
  }
345
337
 
@@ -401,7 +393,7 @@ export class NodeOutput extends Connector
401
393
  */
402
394
  get_value()
403
395
  {
404
- if ( this.value == undefined )
396
+ if ( this.value === undefined || this.node.dirty )
405
397
  this.node.evaluate();
406
398
  return this.value;
407
399
  }
@@ -414,23 +406,199 @@ function force_default(ev)
414
406
  }
415
407
 
416
408
  /**
417
- * Creates a node control
418
- * @param {string} type Input type
419
- * @param {object} attrs Input attributes as an object
420
- * @returns {HTMLInputElement}
409
+ * Custom control interface
421
410
  */
422
- export function NodeControl(type, attrs={})
411
+ export class NodeControl
423
412
  {
424
- let elem = document.createElement("input");
425
- elem.type = type;
426
- elem.classList.add("dgl-control");
427
- for ( let [k, v] of Object.entries(attrs) )
428
- elem.setAttribute(k, v);
429
- elem.addEventListener("mousedown", force_default);
430
- elem.addEventListener("mouseup", force_default);
431
- elem.addEventListener("mousemove", force_default);
432
- elem.addEventListener("wheel", force_default);
433
- return elem;
413
+ /**
414
+ * @param {object} config Control config
415
+ * @param {any} config.default Control default value
416
+ */
417
+ constructor(config={})
418
+ {
419
+ this.on_change = null;
420
+ this.config = config;
421
+ }
422
+
423
+ to_dom(label, name)
424
+ {
425
+ this.element = document.createElement("div");
426
+ this.element.classList.add("dgl-control-parent");
427
+ if ( label.length )
428
+ this.element.appendChild(NodeControl.make_label(label));
429
+ this.setup_dom(this.element, name);
430
+ if ( "default" in this.config )
431
+ this.set_value(this.config.default);
432
+ return this.element;
433
+ }
434
+
435
+ /**
436
+ * Sets up the DOM
437
+ * @param {HTMLElement} parent Element to add additional content to
438
+ * @param {name} name Control name
439
+ */
440
+ setup_dom(_parent, _name)
441
+ {
442
+ }
443
+
444
+ /**
445
+ * Dispatches the change event to update the graph
446
+ */
447
+ notify_change()
448
+ {
449
+ this.on_change();
450
+ }
451
+
452
+ /**
453
+ * Override to return the control value
454
+ * @returns {any} value
455
+ */
456
+ get_value()
457
+ {
458
+ }
459
+
460
+ /**
461
+ * Override to return set a value to the control
462
+ * @param {any} value
463
+ */
464
+ set_value(_value)
465
+ {
466
+ }
467
+
468
+ /**
469
+ * Ensures an element uses the default mouse interactions instead of
470
+ * propagating to the node editor
471
+ */
472
+ use_default_events(element)
473
+ {
474
+ element.addEventListener("mousedown", force_default);
475
+ element.addEventListener("mouseup", force_default);
476
+ element.addEventListener("mousemove", force_default);
477
+ element.addEventListener("wheel", force_default);
478
+ }
479
+
480
+ /**
481
+ * Creates a label element
482
+ * @param {string} label Text to display
483
+ */
484
+ static make_label(label)
485
+ {
486
+ let elem = document.createElement("span");
487
+ elem.textContent = label;
488
+ elem.classList.add("dgl-label", "dgl-input-label");
489
+ return elem;
490
+ }
491
+
492
+ set_visibility(value)
493
+ {
494
+ for ( let ch of this.element.children )
495
+ if ( !ch.classList.contains("dgl-label") )
496
+ ch.style.visibility = value;
497
+ }
498
+
499
+ /**
500
+ * Hides the control when an input connection is attached
501
+ */
502
+ hide()
503
+ {
504
+ this.set_visibility("hidden");
505
+ }
506
+
507
+ /**
508
+ * Shows the control when an input connection is detached
509
+ */
510
+ show()
511
+ {
512
+ this.set_visibility("visible");
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Control for simple input elements
518
+ */
519
+ export class InputControl extends NodeControl
520
+ {
521
+ /**
522
+ * @param {object} config Control config
523
+ * @param {string} config.type Input type
524
+ * @param {object} config.attrs Input attributes
525
+ * @param {any} config.default Default value
526
+ */
527
+ constructor(config={type: "text"})
528
+ {
529
+ super(config);
530
+ }
531
+
532
+ setup_dom(parent, name)
533
+ {
534
+ this.input = document.createElement("input");
535
+ parent.appendChild(this.input);
536
+ this.input.type = this.config.type;
537
+ this.input.classList.add("dgl-control");
538
+ for ( let [k, v] of Object.entries(this.config.attrs ?? {}) )
539
+ this.input.setAttribute(k, v);
540
+ this.use_default_events(this.input);
541
+ this.input.name = name;
542
+ this.input.addEventListener("input", () => this.notify_change());
543
+ }
544
+
545
+ get_value()
546
+ {
547
+ if ( this.config.type == "checkbox" )
548
+ return this.input.checked;
549
+ return this.input.value;
550
+ }
551
+
552
+ set_value(value)
553
+ {
554
+ if ( this.config.type == "checkbox" )
555
+ this.input.checked = value;
556
+ else
557
+ this.input.value = value;
558
+ }
559
+ }
560
+
561
+ /**
562
+ * Drop-down Control
563
+ */
564
+ export class DropDownControl extends NodeControl
565
+ {
566
+
567
+ /**
568
+ * @param {object} config Control config
569
+ * @param {Array.<string, string>} config.options Drop down options ([value, display])
570
+ * @param {any} config.default Default value
571
+ */
572
+ constructor(config={options: []})
573
+ {
574
+ super(config);
575
+ }
576
+
577
+ setup_dom(parent, name)
578
+ {
579
+ this.input = document.createElement("select");
580
+ for ( let [value, display] of this.config.options )
581
+ {
582
+ let option = document.createElement("option");
583
+ this.input.appendChild(option);
584
+ option.value = value;
585
+ option.textContent = display ?? value;
586
+ }
587
+ this.input.name = name;
588
+ parent.appendChild(this.input);
589
+ this.use_default_events(this.input);
590
+ this.input.addEventListener("input", () => this.notify_change());
591
+ }
592
+
593
+ get_value()
594
+ {
595
+ return this.input.value;
596
+ }
597
+
598
+ set_value(value)
599
+ {
600
+ this.input.value = value;
601
+ }
434
602
  }
435
603
 
436
604
  /**
@@ -472,6 +640,7 @@ export class Node extends VisualElement
472
640
  * @member {number}
473
641
  */
474
642
  this.y = 0;
643
+ this.dirty = true;
475
644
  }
476
645
 
477
646
  /**
@@ -574,6 +743,7 @@ export class Node extends VisualElement
574
743
 
575
744
  evaluate()
576
745
  {
746
+ this.dirty = false;
577
747
  if ( this.type )
578
748
  this.type.evaluate_node(this);
579
749
  }
@@ -693,11 +863,17 @@ export class ContextMenu extends VisualElement
693
863
 
694
864
  to_dom(editor)
695
865
  {
696
- this.dom = this.make_menu("dgl-menu", this.actions, editor);
697
- this.dom.style.display = "none";
866
+ this.dom = this.build(editor);
698
867
  return this.dom;
699
868
  }
700
869
 
870
+ build(editor)
871
+ {
872
+ let dom = this.make_menu("dgl-menu", this.actions, editor);
873
+ dom.style.display = "none";
874
+ return dom;
875
+ }
876
+
701
877
  make_action(title, action, editor)
702
878
  {
703
879
  let onclick = action;
@@ -772,6 +948,18 @@ export class ContextMenu extends VisualElement
772
948
  {
773
949
  this.dom.style.display = "none";
774
950
  }
951
+
952
+ /**
953
+ * Rebuilds the DOM element
954
+ */
955
+ rebuild(editor)
956
+ {
957
+ let parent = this.dom.parentNode;
958
+ let old_e = this.dom;
959
+ let new_e = this.build(editor);
960
+ parent.replaceChild(new_e, old_e);
961
+ this.dom = new_e;
962
+ }
775
963
  }
776
964
 
777
965
  /**
@@ -844,7 +1032,6 @@ export class Editor
844
1032
  this.container.appendChild(this.context_menu.to_dom(this));
845
1033
  }
846
1034
 
847
-
848
1035
  /**
849
1036
  * Adds a new node to the graph
850
1037
  * @param {Node} node
@@ -864,7 +1051,10 @@ export class Editor
864
1051
  {
865
1052
  if ( input.control )
866
1053
  {
867
- input.control.addEventListener("change", () => this._refresh_graph());
1054
+ input.control.on_change = () => {
1055
+ node.dirty = true;
1056
+ this._refresh_graph();
1057
+ };
868
1058
  }
869
1059
  }
870
1060
  }
@@ -895,8 +1085,9 @@ export class Editor
895
1085
  create_node(type, data={})
896
1086
  {
897
1087
  let node = type.create_node();
898
- node.set_input_value(data);
899
1088
  this.add_node(node);
1089
+ node.set_input_value(data);
1090
+ this._refresh_graph();
900
1091
  return node;
901
1092
  }
902
1093