dragon-graph-lib 0.1.1 → 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.
@@ -4,15 +4,33 @@ function to_slug(string)
4
4
  return string.replace(/[^a-z0-9_-]+/ig, "-").toLowerCase();
5
5
  }
6
6
 
7
+ /**
8
+ * Connection socket
9
+ *
10
+ * This is what allows nodes to be connected between each other,
11
+ * it provides simple type checking but it can be customized for more complex cases.
12
+ */
7
13
  export class Socket
8
14
  {
15
+ /**
16
+ * @param {string} name Socket type name
17
+ * @param {boolean} multi_input (For input sockets) whether it should accept multiple incoming connections
18
+ */
9
19
  constructor(name, multi_input=false)
10
20
  {
21
+ /**
22
+ * Socket type name
23
+ * @member {string}
24
+ */
11
25
  this.name = name;
12
26
  this.multi_input = multi_input;
13
27
  this.accepts_incoming = new Set();
14
28
  }
15
29
 
30
+ /**
31
+ * Allows incoming connections from the given type
32
+ * @param {Socket|string} type Type name or Socket instance
33
+ */
16
34
  accept(type)
17
35
  {
18
36
  let type_name = type instanceof Socket ? type.name : type;
@@ -26,16 +44,41 @@ export class Socket
26
44
  return elem;
27
45
  }
28
46
 
47
+ /**
48
+ * Checks whether an output sockets allows a connection to the given input socket
49
+ *
50
+ * The default implementation defers to the input socket
51
+ *
52
+ * @param {Socket} input_socket
53
+ * @returns {boolean}
54
+ */
29
55
  output_can_connect(input_socket)
30
56
  {
31
57
  return input_socket.input_can_connect(this);
32
58
  }
33
59
 
60
+ /**
61
+ * Checks whether an input sockets allows a connection from the given output socket
62
+ *
63
+ * The default implementation checks types based on name (same or added with accept())
64
+ *
65
+ * @param {Socket} output_socket
66
+ * @returns {boolean}
67
+ */
34
68
  input_can_connect(output_socket)
35
69
  {
36
70
  return output_socket.name == this.name || this.accepts_incoming.has(output_socket.name);
37
71
  }
38
72
 
73
+ /**
74
+ * Checks whether two sockets allow connections between them
75
+ *
76
+ * Equivalent to output_socket.output_can_connect(input_socket)
77
+ *
78
+ * @param {Socket} output_socket
79
+ * @param {Socket} input_socket
80
+ * @returns {boolean}
81
+ */
39
82
  static can_connect(output_socket, input_socket)
40
83
  {
41
84
  return output_socket.output_can_connect(input_socket);
@@ -76,14 +119,27 @@ class VisualElement
76
119
  }
77
120
  }
78
121
 
122
+ /**
123
+ * Connection between two sockets
124
+ */
79
125
  export class Connection extends VisualElement
80
126
  {
81
- margin = 10;
82
-
127
+ /**
128
+ * @param {NodeOutput} output Output connector
129
+ * @param {NodeInput} input Input connector
130
+ */
83
131
  constructor(output, input)
84
132
  {
85
133
  super();
134
+ /**
135
+ * Output connector
136
+ * @member {NodeOutput}
137
+ */
86
138
  this.output = output;
139
+ /**
140
+ * Input connector
141
+ * @member {NodeInput}
142
+ */
87
143
  this.input = input;
88
144
  }
89
145
 
@@ -103,7 +159,6 @@ export class Connection extends VisualElement
103
159
  static path_d(x1, y1, x2, y2)
104
160
  {
105
161
  let w = x2 - x1;
106
- let h = y2 - y1;
107
162
 
108
163
  return `M ${x1} ${y1}
109
164
  C ${x1 + w / 3} ${y1}
@@ -113,29 +168,55 @@ export class Connection extends VisualElement
113
168
  }
114
169
  }
115
170
 
116
- class NodeIO extends VisualElement
171
+ /**
172
+ * Base connector class
173
+ */
174
+ class Connector extends VisualElement
117
175
  {
118
176
  constructor(node, name, label, socket)
119
177
  {
120
178
  super();
179
+ /**
180
+ * Node this connector is part of
181
+ * @member {Node}
182
+ */
121
183
  this.node = node;
184
+ /**
185
+ * Name of the connector within the node
186
+ * @member {string}
187
+ */
122
188
  this.name = name;
189
+ /**
190
+ * User-visible label / title
191
+ * @member {NodeOutput}
192
+ */
123
193
  this.label = label;
194
+ /**
195
+ * Associated socket
196
+ * @member {Socket?}
197
+ */
124
198
  this.socket = socket;
199
+ /**
200
+ * Active connections
201
+ * @member {Connection[]}
202
+ */
125
203
  this.connections = [];
126
204
  this.dom_items = {};
127
205
  }
128
206
 
129
- update_dom()
130
- {
131
- this.dom_items.label.textContent = this.label;
132
- }
133
-
207
+ /**
208
+ * Adds a connection to the connector
209
+ * @param {Connection} connection
210
+ */
134
211
  connect(connection)
135
212
  {
136
213
  this.connections.push(connection);
137
214
  }
138
215
 
216
+ /**
217
+ * Removes a connection from the connector
218
+ * @param {Connection} connection
219
+ */
139
220
  disconnect(connection)
140
221
  {
141
222
  let index = this.connections.indexOf(connection);
@@ -144,12 +225,26 @@ class NodeIO extends VisualElement
144
225
  }
145
226
  }
146
227
 
147
-
148
- export class NodeInput extends NodeIO
228
+ /**
229
+ * Input connector
230
+ * @extends Connector
231
+ */
232
+ export class NodeInput extends Connector
149
233
  {
234
+ /**
235
+ * @param {Node} node Node owning this connector
236
+ * @param {string} name Name of the connector within the node
237
+ * @param {string} label User-visible label / title
238
+ * @param {Socket?} socket Optional socket for incoming connectors
239
+ * @param {HTMLInputElement?} control Optional control to set the value without a connection
240
+ */
150
241
  constructor(node, name, label, socket, control)
151
242
  {
152
243
  super(node, name, label, socket);
244
+ /**
245
+ * Optional control to set the value without a connection
246
+ * @member {HTMLInputElement?}
247
+ */
153
248
  this.control = control;
154
249
  }
155
250
 
@@ -164,13 +259,10 @@ export class NodeInput extends NodeIO
164
259
  container.classList.add("dgl-socketed");
165
260
  }
166
261
 
167
- this.dom_items.label = document.createElement("span");
168
- this.dom_items.label.textContent = this.name;
169
- this.dom_items.label.classList.add("dgl-label", "dgl-input-label");
170
- container.appendChild(this.dom_items.label);
171
-
172
262
  if ( this.control )
173
- 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));
174
266
 
175
267
  return container;
176
268
  }
@@ -187,17 +279,32 @@ export class NodeInput extends NodeIO
187
279
  this.connections.push(connection);
188
280
 
189
281
  if ( this.control )
190
- this.control.style.visibility = "hidden";
282
+ this.control.hide();
191
283
  }
192
284
 
285
+ /**
286
+ * Sets the control value
287
+ * @param value
288
+ */
193
289
  set_value(value)
194
290
  {
195
291
  if ( this.control )
196
292
  {
197
- this.control.value = value;
293
+ this.control.set_value(value);
198
294
  }
199
295
  }
200
296
 
297
+ /**
298
+ * Gets the input value
299
+ *
300
+ * If there's input connections, the value is based on those.
301
+ * Otherwise the value comes from the control.
302
+ *
303
+ * If there is a socket on this connector with multi_input, the returned value
304
+ * is always an array.
305
+ *
306
+ * @return {any}
307
+ */
201
308
  get_value()
202
309
  {
203
310
  if ( this.connections.length )
@@ -214,7 +321,7 @@ export class NodeInput extends NodeIO
214
321
  return null;
215
322
  }
216
323
 
217
- let value = this.control.value
324
+ let value = this.control.get_value();
218
325
  if ( this.socket && this.socket.multi_input )
219
326
  return [value];
220
327
  return value;
@@ -224,12 +331,22 @@ export class NodeInput extends NodeIO
224
331
  {
225
332
  super.disconnect(connection);
226
333
  if ( this.control && this.connections.length == 0 )
227
- this.control.style.visibility = "visible";
334
+ this.control.show();
228
335
  }
229
336
  }
230
337
 
231
- export class NodeOutput extends NodeIO
338
+ /**
339
+ * Output connector
340
+ * @extends Connector
341
+ */
342
+ export class NodeOutput extends Connector
232
343
  {
344
+ /**
345
+ * @param {Node} node Node owning this connector
346
+ * @param {string} name Name of the connector within the node
347
+ * @param {string} label User-visible label / title
348
+ * @param {Socket} socket Socket for outgoing connectors
349
+ */
233
350
  constructor(node, name, label, socket)
234
351
  {
235
352
  super(node, name, label, socket);
@@ -250,19 +367,33 @@ export class NodeOutput extends NodeIO
250
367
  return container;
251
368
  }
252
369
 
370
+ /**
371
+ * Clears the stored value.
372
+ *
373
+ * Will force re-evaluation of the node on get_value()
374
+ */
253
375
  clear_value()
254
376
  {
255
377
  this.value = undefined;
256
378
  }
257
379
 
380
+ /**
381
+ * Sets value for outgoing connections
382
+ * @param value
383
+ */
258
384
  set_value(value)
259
385
  {
260
386
  this.value = value;
261
387
  }
262
388
 
389
+ /**
390
+ * Gets the output value
391
+ *
392
+ * Used by outgoing connections. Might cause the node to be evaluated
393
+ */
263
394
  get_value()
264
395
  {
265
- if ( this.value == undefined )
396
+ if ( this.value === undefined || this.node.dirty )
266
397
  this.node.evaluate();
267
398
  return this.value;
268
399
  }
@@ -274,34 +405,248 @@ function force_default(ev)
274
405
  ev.stopPropagation();
275
406
  }
276
407
 
277
- export function NodeControl(type, attrs={})
408
+ /**
409
+ * Custom control interface
410
+ */
411
+ export class NodeControl
412
+ {
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
278
520
  {
279
- let elem = document.createElement("input");
280
- elem.type = type;
281
- elem.classList.add("dgl-control");
282
- for ( let [k, v] of Object.entries(attrs) )
283
- elem.setAttribute(k, v);
284
- elem.addEventListener("mousedown", force_default);
285
- elem.addEventListener("mouseup", force_default);
286
- elem.addEventListener("mousemove", force_default);
287
- elem.addEventListener("wheel", force_default);
288
- return elem;
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
+ }
289
602
  }
290
603
 
604
+ /**
605
+ * Main node class
606
+ */
291
607
  export class Node extends VisualElement
292
608
  {
609
+ /**
610
+ * @param {string} title Node title, shown to the user
611
+ * @param {NodeType?} type Type controlling the node
612
+ */
293
613
  constructor(title, type)
294
614
  {
295
615
  super();
296
616
  this.title = title;
617
+ /**
618
+ * Registered outputs
619
+ * @member {Object.<string, NodeOutput>}
620
+ */
297
621
  this.outputs = {};
622
+ /**
623
+ * Registered inputs
624
+ * @member {Object.<string, NodeInput>}
625
+ */
298
626
  this.inputs = {};
627
+ /**
628
+ * Element that can be used to preview the node effect on the editor
629
+ * @member {HTMLElement}
630
+ */
299
631
  this.preview = null;
300
632
  this.type = type;
633
+ /**
634
+ * X position in the editor
635
+ * @member {number}
636
+ */
301
637
  this.x = 0;
638
+ /**
639
+ * Y position in the editor
640
+ * @member {number}
641
+ */
302
642
  this.y = 0;
643
+ this.dirty = true;
303
644
  }
304
645
 
646
+ /**
647
+ * Sets control values for inputs
648
+ * @param {object} data Object with input names as keys and associated values
649
+ */
305
650
  set_input_value(data)
306
651
  {
307
652
  for ( let [k, v] of Object.entries(this.inputs) )
@@ -311,6 +656,10 @@ export class Node extends VisualElement
311
656
  }
312
657
  }
313
658
 
659
+ /**
660
+ * Gets values from the inputs
661
+ * @returns {object} Object with input names as keys and associated values
662
+ */
314
663
  get_input_value()
315
664
  {
316
665
  let data = {};
@@ -336,11 +685,30 @@ export class Node extends VisualElement
336
685
  return connection;
337
686
  }
338
687
 
688
+ /**
689
+ * Adds a new input
690
+ *
691
+ * The resulting input will be accessible from this.inputs[name]
692
+ *
693
+ * @param {string} name Name of the connector within the node
694
+ * @param {string} label User-visible label / title
695
+ * @param {Socket?} socket Optional socket for incoming connectors
696
+ * @param {HTMLInputElement?} control Optional control to set the value without a connection
697
+ */
339
698
  add_input(name, title, socket, control)
340
699
  {
341
700
  this.inputs[name] = new NodeInput(this, name, title, socket, control);
342
701
  }
343
702
 
703
+ /**
704
+ * Adds a new output
705
+ *
706
+ * The resulting input will be accessible from this.outputs[name]
707
+ *
708
+ * @param {string} name Name of the connector within the node
709
+ * @param {string} label User-visible label / title
710
+ * @param {Socket} socket Socket for outgoing connectors
711
+ */
344
712
  add_output(name, title, socket)
345
713
  {
346
714
  this.outputs[name] = new NodeOutput(this, name, title, socket);
@@ -348,7 +716,8 @@ export class Node extends VisualElement
348
716
 
349
717
  update_dom()
350
718
  {
351
- this.dom.style.transform = `translate(${this.x}px, ${this.y}px`;
719
+ this.dom.style.left = `${this.x}px`;
720
+ this.dom.style.top = `${this.y}px`;
352
721
  }
353
722
 
354
723
  build_dom()
@@ -374,10 +743,39 @@ export class Node extends VisualElement
374
743
 
375
744
  evaluate()
376
745
  {
746
+ this.dirty = false;
377
747
  if ( this.type )
378
748
  this.type.evaluate_node(this);
379
749
  }
380
750
 
751
+ /**
752
+ * Returns a list of all input connections
753
+ * @returns {Connection[]}
754
+ */
755
+ input_connections()
756
+ {
757
+ return Object.values(this.inputs)
758
+ .map(c => c.connections)
759
+ .reduce((a, b) => a.concat(b), [])
760
+ ;
761
+ }
762
+
763
+ /**
764
+ * Returns a list of all output connections
765
+ * @returns {Connection[]}
766
+ */
767
+ all_connections()
768
+ {
769
+ return Object.values(this.outputs)
770
+ .map(c => c.connections)
771
+ .reduce((a, b) => a.concat(b), [])
772
+ ;
773
+ }
774
+
775
+ /**
776
+ * Returns a list of all connections (both input and output)
777
+ * @returns {Connection[]}
778
+ */
381
779
  all_connections()
382
780
  {
383
781
  return Object.values(this.inputs).concat(Object.values(this.outputs))
@@ -387,13 +785,32 @@ export class Node extends VisualElement
387
785
  }
388
786
  }
389
787
 
788
+ /**
789
+ * Node type
790
+ *
791
+ * This class is responsible for creating nodes, and adding logic for output values
792
+ */
390
793
  export class NodeType
391
794
  {
795
+ /**
796
+ * @param {string} name Type name
797
+ */
392
798
  constructor(name)
393
799
  {
800
+ /**
801
+ * Type name
802
+ * @member {string}
803
+ */
394
804
  this.name = name;
395
805
  }
396
806
 
807
+ /**
808
+ * Creates and sets up a node for this type
809
+ *
810
+ * Custom types should override populate_node() to add connectors or otherwise customize the node
811
+ *
812
+ * @returns Node
813
+ */
397
814
  create_node()
398
815
  {
399
816
  let node = new Node(this.name, this);
@@ -401,23 +818,43 @@ export class NodeType
401
818
  return node;
402
819
  }
403
820
 
404
- populate_node(node)
821
+ /**
822
+ * Allows custom types to add connectors or otherwise customize the node
823
+ * @protected
824
+ * @param {Node} node
825
+ */
826
+ populate_node(_node)
405
827
  {
406
828
  }
407
829
 
408
- update_node(node)
409
- {
410
-
411
- }
412
-
413
- evaluate_node(node)
830
+ /**
831
+ * Evaluates node logic
832
+ *
833
+ * Generally you should get input values with `node.inputs[name].get_value()`
834
+ * And set output values with `node.outputs[name].set_value()`.
835
+ *
836
+ * You can also alter the `node.preview` to add custom HTML to the node
837
+ * based on these values
838
+ *
839
+ * @param {Node} node
840
+ */
841
+ evaluate_node(_node)
414
842
  {
415
843
 
416
844
  }
417
845
  }
418
846
 
847
+ /**
848
+ * Context menu for the node editor
849
+ */
419
850
  export class ContextMenu extends VisualElement
420
851
  {
852
+ /**
853
+ * @param {object|NodeType[]} actions If an object,
854
+ * keys will be items in the menu while values are the performed actions.
855
+ * Possible values are callbacks, NodeType instances (which will create new nodes),
856
+ * Or other actions definitions for submenus
857
+ */
421
858
  constructor(actions)
422
859
  {
423
860
  super();
@@ -426,11 +863,17 @@ export class ContextMenu extends VisualElement
426
863
 
427
864
  to_dom(editor)
428
865
  {
429
- this.dom = this.make_menu("dgl-menu", this.actions, editor);
430
- this.dom.style.display = "none";
866
+ this.dom = this.build(editor);
431
867
  return this.dom;
432
868
  }
433
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
+
434
877
  make_action(title, action, editor)
435
878
  {
436
879
  let onclick = action;
@@ -486,6 +929,11 @@ export class ContextMenu extends VisualElement
486
929
  return item;
487
930
  }
488
931
 
932
+ /**
933
+ * Opens the menu at the given point
934
+ * @param {number} x
935
+ * @param {number} y
936
+ */
489
937
  show(x, y)
490
938
  {
491
939
  this.dom.style.display = "";
@@ -493,19 +941,44 @@ export class ContextMenu extends VisualElement
493
941
  this.dom.style.top = y + "px";
494
942
  }
495
943
 
944
+ /**
945
+ * Closes the menu
946
+ */
496
947
  close()
497
948
  {
498
949
  this.dom.style.display = "none";
499
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
+ }
500
963
  }
501
964
 
965
+ /**
966
+ * Node Editor
967
+ */
502
968
  export class Editor
503
969
  {
970
+
971
+ /**
972
+ * @param {HTMLElement} container Parent element in the DOM
973
+ * @param {object|NodeType[]} Actions for the context menu (See ContextMenu)
974
+ */
504
975
  constructor(container, context_menu={})
505
976
  {
506
977
  this.nodes = [];
507
978
 
508
979
  this.container = container;
980
+ if ( !container.hasAttribute("tabindex") )
981
+ container.setAttribute("tabindex", "0");
509
982
 
510
983
  this.area = container.appendChild(document.createElement("div"));
511
984
  container.classList.add("dgl-editor");
@@ -531,6 +1004,12 @@ export class Editor
531
1004
  this.container.addEventListener("mousedown", this._on_area_mouse_down.bind(this));
532
1005
  this.container.addEventListener("mousemove", this._on_area_mouse_move.bind(this));
533
1006
  this.container.addEventListener("mouseup", this._on_area_mouse_up.bind(this));
1007
+
1008
+ this.container.addEventListener("touchstart", this._on_area_mouse_down.bind(this));
1009
+ this.container.addEventListener("touchmove", this._on_area_mouse_move.bind(this));
1010
+ this.container.addEventListener("touchend", this._on_area_mouse_up.bind(this));
1011
+ this.container.addEventListener("touchcancel", this._on_area_mouse_up.bind(this));
1012
+
534
1013
  this.container.addEventListener("wheel", this._on_area_wheel.bind(this));
535
1014
  this.container.addEventListener("contextmenu", this._on_area_menu.bind(this));
536
1015
 
@@ -543,8 +1022,6 @@ export class Editor
543
1022
  "Duplicate": () => {
544
1023
  if ( this.selected_node ) {
545
1024
  let node = this.create_node(this.selected_node.type, this.selected_node.get_input_value());
546
- node.x = this.selected_node.x;
547
- node.y = this.selected_node.y;
548
1025
  this._select_node(node);
549
1026
 
550
1027
  }
@@ -555,10 +1032,12 @@ export class Editor
555
1032
  this.container.appendChild(this.context_menu.to_dom(this));
556
1033
  }
557
1034
 
558
- create_node(type, data={})
1035
+ /**
1036
+ * Adds a new node to the graph
1037
+ * @param {Node} node
1038
+ */
1039
+ add_node(node)
559
1040
  {
560
- let node = type.create_node();
561
- node.set_input_value(data);
562
1041
  node.x = this.cursor_x;
563
1042
  node.y = this.cursor_y;
564
1043
  this.nodes.push(node);
@@ -566,11 +1045,57 @@ export class Editor
566
1045
  this.area.appendChild(node_dom);
567
1046
 
568
1047
  node_dom.addEventListener("mousedown", ev => this._on_node_mouse_down(ev, node));
1048
+ node_dom.addEventListener("touchstart", ev => this._on_node_mouse_down(ev, node));
569
1049
  node_dom.addEventListener("contextmenu", ev => this._on_node_menu(ev, node));
1050
+ for ( let input of Object.values(node.inputs) )
1051
+ {
1052
+ if ( input.control )
1053
+ {
1054
+ input.control.on_change = () => {
1055
+ node.dirty = true;
1056
+ this._refresh_graph();
1057
+ };
1058
+ }
1059
+ }
1060
+ }
1061
+
1062
+ /**
1063
+ * Removes a node from the graph
1064
+ * @param {Node} node
1065
+ */
1066
+ remove_node(node)
1067
+ {
1068
+ for ( let conn of node.all_connections() )
1069
+ conn.disconnect();
570
1070
 
1071
+ let ind = this.nodes.indexOf(node);
1072
+ if ( ind != -1 )
1073
+ this.nodes.splice(ind, 1);
1074
+ node.destroy();
1075
+
1076
+ this._refresh_graph();
1077
+ }
1078
+
1079
+ /**
1080
+ * Creates a new node from the given type
1081
+ * @param {NodeType} type
1082
+ * @param {object} data Optional data to populate node inputs
1083
+ * @returns {Node} The created node
1084
+ */
1085
+ create_node(type, data={})
1086
+ {
1087
+ let node = type.create_node();
1088
+ this.add_node(node);
1089
+ node.set_input_value(data);
1090
+ this._refresh_graph();
571
1091
  return node;
572
1092
  }
573
1093
 
1094
+ /**
1095
+ * Automatically lays out the graph
1096
+ *
1097
+ * It assumes all nodes and connections are set up
1098
+ */
574
1099
  auto_layout()
575
1100
  {
576
1101
  setTimeout(() => this._on_auto_layout());
@@ -663,8 +1188,60 @@ export class Editor
663
1188
  }
664
1189
 
665
1190
  this._refresh_graph();
1191
+ this.fit_view();
666
1192
  }
667
1193
 
1194
+ /**
1195
+ * Pan and zoom to fit all the nodes
1196
+ * @param {number} margin Margin to leave around the area
1197
+ */
1198
+ fit_view(margin=20)
1199
+ {
1200
+ if ( this.nodes.length == 0 )
1201
+ return;
1202
+
1203
+ let left = Infinity;
1204
+ let right = -Infinity;
1205
+ let top = Infinity;
1206
+ let bottom = -Infinity;
1207
+
1208
+ for ( let node of this.nodes )
1209
+ {
1210
+ let rect = node.dom.getBoundingClientRect();
1211
+ if ( rect.left < left ) left = rect.left;
1212
+ if ( rect.right > right ) right = rect.right;
1213
+ if ( rect.top < top ) top = rect.top;
1214
+ if ( rect.bottom > bottom ) bottom = rect.bottom;
1215
+ }
1216
+
1217
+ let crect = this.container.getBoundingClientRect();
1218
+
1219
+ left += -margin - crect.left;
1220
+ right += margin - crect.left;
1221
+ top += -margin - crect.top;
1222
+ bottom += margin - crect.top;
1223
+
1224
+ let width = right - left;
1225
+ let height = bottom - top;
1226
+ let x_scale = crect.width / width;
1227
+ let y_scale = crect.height / height;
1228
+ let scale = Math.min(x_scale, y_scale);
1229
+ this.scale = scale;
1230
+
1231
+ this.offset_x = (crect.width - width * scale) / 2 - left + margin;
1232
+ this.offset_y = (crect.height - height * scale) / 2 - top + margin;
1233
+ this._apply_transform();
1234
+
1235
+ }
1236
+
1237
+ /**
1238
+ * Adds a connection to the graph
1239
+ * @param {Node} output_node Node the connection is coming from
1240
+ * @param {string} output_name Name of the output connector within output_node
1241
+ * @param {Node} input_node Node the connection is going to
1242
+ * @param {string} input_name Name of the input connector within input_node
1243
+ * @return {Connection?} Connection (Or null if the connection is impossible)
1244
+ */
668
1245
  connect(output_node, output_name, input_node, input_name)
669
1246
  {
670
1247
  let conn = output_node.connect(output_name, input_node, input_name);
@@ -680,6 +1257,10 @@ export class Editor
680
1257
  return conn;
681
1258
  }
682
1259
 
1260
+ /**
1261
+ * Removes a connection from the graph
1262
+ * @param {Connection} connection
1263
+ */
683
1264
  disconnect(connection)
684
1265
  {
685
1266
  connection.disconnect();
@@ -699,18 +1280,27 @@ export class Editor
699
1280
  this.node_menu.close();
700
1281
  }
701
1282
 
1283
+ _touch_mouse_pos(ev)
1284
+ {
1285
+ if ( ev.changedTouches )
1286
+ return ev.changedTouches[0];
1287
+ return ev;
1288
+ }
1289
+
702
1290
  _on_area_mouse_down(ev)
703
1291
  {
1292
+ this.container.focus();
704
1293
  this._close_menus();
705
1294
  if ( ev.button == 0 )
706
1295
  {
707
1296
  this._select_node(null);
708
1297
  }
709
- else if ( ev.button == 1 )
1298
+ else if ( ev.button == 1 || ev.changedTouches )
710
1299
  {
711
1300
  let rect = this.container.getBoundingClientRect();
712
- this.drag_start_x = ev.clientX - rect.left;
713
- this.drag_start_y = ev.clientY - rect.top;
1301
+ let evp = this._touch_mouse_pos(ev);
1302
+ this.drag_start_x = evp.clientX - rect.left;
1303
+ this.drag_start_y = evp.clientY - rect.top;
714
1304
  this.drag_start_ox = this.offset_x;
715
1305
  this.drag_start_oy = this.offset_y;
716
1306
  this.drag = "pan";
@@ -736,8 +1326,9 @@ export class Editor
736
1326
  _on_area_mouse_move(ev)
737
1327
  {
738
1328
  let rect = this.container.getBoundingClientRect();
739
- let drag_x = ev.clientX - rect.left;
740
- let drag_y = ev.clientY - rect.top;
1329
+ let evp = this._touch_mouse_pos(ev);
1330
+ let drag_x = evp.clientX - rect.left;
1331
+ let drag_y = evp.clientY - rect.top;
741
1332
  this.cursor_x = (drag_x - this.offset_x) / this.scale;
742
1333
  this.cursor_y = (drag_y - this.offset_y) / this.scale;
743
1334
 
@@ -757,7 +1348,8 @@ export class Editor
757
1348
  let y = this.drag_start_oy + delta_y / this.scale;
758
1349
  this.selected_node.x = x;
759
1350
  this.selected_node.y = y;
760
- this.selected_node.dom.style.transform = `translate(${x}px, ${y}px`;
1351
+ this.selected_node.dom.style.left = `${x}px`;
1352
+ this.selected_node.dom.style.top = `${y}px`;
761
1353
  for ( let connection of this.selected_node.all_connections() )
762
1354
  this._update_connection(connection);
763
1355
  }
@@ -802,7 +1394,8 @@ export class Editor
802
1394
 
803
1395
  _get_dest_connector(ev)
804
1396
  {
805
- let elem = document.elementFromPoint(ev.clientX, ev.clientY);
1397
+ let evp = this._touch_mouse_pos(ev);
1398
+ let elem = document.elementFromPoint(evp.clientX, evp.clientY);
806
1399
  if ( !elem.classList.contains("dgl-socket") )
807
1400
  return null;
808
1401
 
@@ -844,6 +1437,9 @@ export class Editor
844
1437
  ev.preventDefault();
845
1438
  ev.stopPropagation();
846
1439
 
1440
+ if ( !this.container.contains(document.activeElement) )
1441
+ return;
1442
+
847
1443
  let rect = this.container.getBoundingClientRect();
848
1444
  let mouse_x = ev.clientX - rect.left;
849
1445
  let mouse_y = ev.clientY - rect.top;
@@ -869,15 +1465,17 @@ export class Editor
869
1465
 
870
1466
  _on_node_mouse_down(ev, node)
871
1467
  {
1468
+ // this.container.focus();
872
1469
  this._close_menus();
873
- if ( ev.button == 0 )
1470
+ if ( ev.button == 0 || ev.changedTouches )
874
1471
  {
875
1472
  ev.stopPropagation();
876
1473
  this._select_node(node);
877
1474
  this.drag = "node";
878
1475
  let rect = this.container.getBoundingClientRect();
879
- this.drag_start_x = ev.clientX - rect.left;
880
- this.drag_start_y = ev.clientY - rect.top;
1476
+ let evp = this._touch_mouse_pos(ev);
1477
+ this.drag_start_x = evp.clientX - rect.left;
1478
+ this.drag_start_y = evp.clientY - rect.top;
881
1479
  this.drag_start_ox = node.x;
882
1480
  this.drag_start_oy = node.y;
883
1481
  if ( ev.target.classList.contains("dgl-socket") )