dragon-graph-lib 0.1.0 → 0.1.2

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.
@@ -5,15 +5,33 @@ function to_slug(string)
5
5
  return string.replace(/[^a-z0-9_-]+/ig, "-").toLowerCase();
6
6
  }
7
7
 
8
+ /**
9
+ * Connection socket
10
+ *
11
+ * This is what allows nodes to be connected between each other,
12
+ * it provides simple type checking but it can be customized for more complex cases.
13
+ */
8
14
  class Socket
9
15
  {
16
+ /**
17
+ * @param {string} name Socket type name
18
+ * @param {boolean} multi_input (For input sockets) whether it should accept multiple incoming connections
19
+ */
10
20
  constructor(name, multi_input=false)
11
21
  {
22
+ /**
23
+ * Socket type name
24
+ * @member {string}
25
+ */
12
26
  this.name = name;
13
27
  this.multi_input = multi_input;
14
28
  this.accepts_incoming = new Set();
15
29
  }
16
30
 
31
+ /**
32
+ * Allows incoming connections from the given type
33
+ * @param {Socket|string} type Type name or Socket instance
34
+ */
17
35
  accept(type)
18
36
  {
19
37
  let type_name = type instanceof Socket ? type.name : type;
@@ -27,16 +45,41 @@ class Socket
27
45
  return elem;
28
46
  }
29
47
 
48
+ /**
49
+ * Checks whether an output sockets allows a connection to the given input socket
50
+ *
51
+ * The default implementation defers to the input socket
52
+ *
53
+ * @param {Socket} input_socket
54
+ * @returns {boolean}
55
+ */
30
56
  output_can_connect(input_socket)
31
57
  {
32
58
  return input_socket.input_can_connect(this);
33
59
  }
34
60
 
61
+ /**
62
+ * Checks whether an input sockets allows a connection from the given output socket
63
+ *
64
+ * The default implementation checks types based on name (same or added with accept())
65
+ *
66
+ * @param {Socket} output_socket
67
+ * @returns {boolean}
68
+ */
35
69
  input_can_connect(output_socket)
36
70
  {
37
71
  return output_socket.name == this.name || this.accepts_incoming.has(output_socket.name);
38
72
  }
39
73
 
74
+ /**
75
+ * Checks whether two sockets allow connections between them
76
+ *
77
+ * Equivalent to output_socket.output_can_connect(input_socket)
78
+ *
79
+ * @param {Socket} output_socket
80
+ * @param {Socket} input_socket
81
+ * @returns {boolean}
82
+ */
40
83
  static can_connect(output_socket, input_socket)
41
84
  {
42
85
  return output_socket.output_can_connect(input_socket);
@@ -77,14 +120,27 @@ class VisualElement
77
120
  }
78
121
  }
79
122
 
123
+ /**
124
+ * Connection between two sockets
125
+ */
80
126
  class Connection extends VisualElement
81
127
  {
82
- margin = 10;
83
-
128
+ /**
129
+ * @param {NodeOutput} output Output connector
130
+ * @param {NodeInput} input Input connector
131
+ */
84
132
  constructor(output, input)
85
133
  {
86
134
  super();
135
+ /**
136
+ * Output connector
137
+ * @member {NodeOutput}
138
+ */
87
139
  this.output = output;
140
+ /**
141
+ * Input connector
142
+ * @member {NodeInput}
143
+ */
88
144
  this.input = input;
89
145
  }
90
146
 
@@ -104,7 +160,6 @@ class Connection extends VisualElement
104
160
  static path_d(x1, y1, x2, y2)
105
161
  {
106
162
  let w = x2 - x1;
107
- let h = y2 - y1;
108
163
 
109
164
  return `M ${x1} ${y1}
110
165
  C ${x1 + w / 3} ${y1}
@@ -114,15 +169,38 @@ class Connection extends VisualElement
114
169
  }
115
170
  }
116
171
 
117
- class NodeIO extends VisualElement
172
+ /**
173
+ * Base connector class
174
+ */
175
+ class Connector extends VisualElement
118
176
  {
119
177
  constructor(node, name, label, socket)
120
178
  {
121
179
  super();
180
+ /**
181
+ * Node this connector is part of
182
+ * @member {Node}
183
+ */
122
184
  this.node = node;
185
+ /**
186
+ * Name of the connector within the node
187
+ * @member {string}
188
+ */
123
189
  this.name = name;
190
+ /**
191
+ * User-visible label / title
192
+ * @member {NodeOutput}
193
+ */
124
194
  this.label = label;
195
+ /**
196
+ * Associated socket
197
+ * @member {Socket?}
198
+ */
125
199
  this.socket = socket;
200
+ /**
201
+ * Active connections
202
+ * @member {Connection[]}
203
+ */
126
204
  this.connections = [];
127
205
  this.dom_items = {};
128
206
  }
@@ -132,11 +210,19 @@ class NodeIO extends VisualElement
132
210
  this.dom_items.label.textContent = this.label;
133
211
  }
134
212
 
213
+ /**
214
+ * Adds a connection to the connector
215
+ * @param {Connection} connection
216
+ */
135
217
  connect(connection)
136
218
  {
137
219
  this.connections.push(connection);
138
220
  }
139
221
 
222
+ /**
223
+ * Removes a connection from the connector
224
+ * @param {Connection} connection
225
+ */
140
226
  disconnect(connection)
141
227
  {
142
228
  let index = this.connections.indexOf(connection);
@@ -145,12 +231,26 @@ class NodeIO extends VisualElement
145
231
  }
146
232
  }
147
233
 
148
-
149
- class NodeInput extends NodeIO
234
+ /**
235
+ * Input connector
236
+ * @extends Connector
237
+ */
238
+ class NodeInput extends Connector
150
239
  {
240
+ /**
241
+ * @param {Node} node Node owning this connector
242
+ * @param {string} name Name of the connector within the node
243
+ * @param {string} label User-visible label / title
244
+ * @param {Socket?} socket Optional socket for incoming connectors
245
+ * @param {HTMLInputElement?} control Optional control to set the value without a connection
246
+ */
151
247
  constructor(node, name, label, socket, control)
152
248
  {
153
249
  super(node, name, label, socket);
250
+ /**
251
+ * Optional control to set the value without a connection
252
+ * @member {HTMLInputElement?}
253
+ */
154
254
  this.control = control;
155
255
  }
156
256
 
@@ -191,6 +291,10 @@ class NodeInput extends NodeIO
191
291
  this.control.style.visibility = "hidden";
192
292
  }
193
293
 
294
+ /**
295
+ * Sets the control value
296
+ * @param value
297
+ */
194
298
  set_value(value)
195
299
  {
196
300
  if ( this.control )
@@ -199,6 +303,17 @@ class NodeInput extends NodeIO
199
303
  }
200
304
  }
201
305
 
306
+ /**
307
+ * Gets the input value
308
+ *
309
+ * If there's input connections, the value is based on those.
310
+ * Otherwise the value comes from the control.
311
+ *
312
+ * If there is a socket on this connector with multi_input, the returned value
313
+ * is always an array.
314
+ *
315
+ * @return {any}
316
+ */
202
317
  get_value()
203
318
  {
204
319
  if ( this.connections.length )
@@ -229,8 +344,18 @@ class NodeInput extends NodeIO
229
344
  }
230
345
  }
231
346
 
232
- class NodeOutput extends NodeIO
347
+ /**
348
+ * Output connector
349
+ * @extends Connector
350
+ */
351
+ class NodeOutput extends Connector
233
352
  {
353
+ /**
354
+ * @param {Node} node Node owning this connector
355
+ * @param {string} name Name of the connector within the node
356
+ * @param {string} label User-visible label / title
357
+ * @param {Socket} socket Socket for outgoing connectors
358
+ */
234
359
  constructor(node, name, label, socket)
235
360
  {
236
361
  super(node, name, label, socket);
@@ -251,16 +376,30 @@ class NodeOutput extends NodeIO
251
376
  return container;
252
377
  }
253
378
 
379
+ /**
380
+ * Clears the stored value.
381
+ *
382
+ * Will force re-evaluation of the node on get_value()
383
+ */
254
384
  clear_value()
255
385
  {
256
386
  this.value = undefined;
257
387
  }
258
388
 
389
+ /**
390
+ * Sets value for outgoing connections
391
+ * @param value
392
+ */
259
393
  set_value(value)
260
394
  {
261
395
  this.value = value;
262
396
  }
263
397
 
398
+ /**
399
+ * Gets the output value
400
+ *
401
+ * Used by outgoing connections. Might cause the node to be evaluated
402
+ */
264
403
  get_value()
265
404
  {
266
405
  if ( this.value == undefined )
@@ -275,6 +414,12 @@ function force_default(ev)
275
414
  ev.stopPropagation();
276
415
  }
277
416
 
417
+ /**
418
+ * Creates a node control
419
+ * @param {string} type Input type
420
+ * @param {object} attrs Input attributes as an object
421
+ * @returns {HTMLInputElement}
422
+ */
278
423
  function NodeControl(type, attrs={})
279
424
  {
280
425
  let elem = document.createElement("input");
@@ -289,20 +434,51 @@ function NodeControl(type, attrs={})
289
434
  return elem;
290
435
  }
291
436
 
437
+ /**
438
+ * Main node class
439
+ */
292
440
  class Node extends VisualElement
293
441
  {
442
+ /**
443
+ * @param {string} title Node title, shown to the user
444
+ * @param {NodeType?} type Type controlling the node
445
+ */
294
446
  constructor(title, type)
295
447
  {
296
448
  super();
297
449
  this.title = title;
450
+ /**
451
+ * Registered outputs
452
+ * @member {Object.<string, NodeOutput>}
453
+ */
298
454
  this.outputs = {};
455
+ /**
456
+ * Registered inputs
457
+ * @member {Object.<string, NodeInput>}
458
+ */
299
459
  this.inputs = {};
460
+ /**
461
+ * Element that can be used to preview the node effect on the editor
462
+ * @member {HTMLElement}
463
+ */
300
464
  this.preview = null;
301
465
  this.type = type;
466
+ /**
467
+ * X position in the editor
468
+ * @member {number}
469
+ */
302
470
  this.x = 0;
471
+ /**
472
+ * Y position in the editor
473
+ * @member {number}
474
+ */
303
475
  this.y = 0;
304
476
  }
305
477
 
478
+ /**
479
+ * Sets control values for inputs
480
+ * @param {object} data Object with input names as keys and associated values
481
+ */
306
482
  set_input_value(data)
307
483
  {
308
484
  for ( let [k, v] of Object.entries(this.inputs) )
@@ -312,6 +488,10 @@ class Node extends VisualElement
312
488
  }
313
489
  }
314
490
 
491
+ /**
492
+ * Gets values from the inputs
493
+ * @returns {object} Object with input names as keys and associated values
494
+ */
315
495
  get_input_value()
316
496
  {
317
497
  let data = {};
@@ -337,11 +517,30 @@ class Node extends VisualElement
337
517
  return connection;
338
518
  }
339
519
 
520
+ /**
521
+ * Adds a new input
522
+ *
523
+ * The resulting input will be accessible from this.inputs[name]
524
+ *
525
+ * @param {string} name Name of the connector within the node
526
+ * @param {string} label User-visible label / title
527
+ * @param {Socket?} socket Optional socket for incoming connectors
528
+ * @param {HTMLInputElement?} control Optional control to set the value without a connection
529
+ */
340
530
  add_input(name, title, socket, control)
341
531
  {
342
532
  this.inputs[name] = new NodeInput(this, name, title, socket, control);
343
533
  }
344
534
 
535
+ /**
536
+ * Adds a new output
537
+ *
538
+ * The resulting input will be accessible from this.outputs[name]
539
+ *
540
+ * @param {string} name Name of the connector within the node
541
+ * @param {string} label User-visible label / title
542
+ * @param {Socket} socket Socket for outgoing connectors
543
+ */
345
544
  add_output(name, title, socket)
346
545
  {
347
546
  this.outputs[name] = new NodeOutput(this, name, title, socket);
@@ -349,7 +548,8 @@ class Node extends VisualElement
349
548
 
350
549
  update_dom()
351
550
  {
352
- this.dom.style.transform = `translate(${this.x}px, ${this.y}px`;
551
+ this.dom.style.left = `${this.x}px`;
552
+ this.dom.style.top = `${this.y}px`;
353
553
  }
354
554
 
355
555
  build_dom()
@@ -379,6 +579,34 @@ class Node extends VisualElement
379
579
  this.type.evaluate_node(this);
380
580
  }
381
581
 
582
+ /**
583
+ * Returns a list of all input connections
584
+ * @returns {Connection[]}
585
+ */
586
+ input_connections()
587
+ {
588
+ return Object.values(this.inputs)
589
+ .map(c => c.connections)
590
+ .reduce((a, b) => a.concat(b), [])
591
+ ;
592
+ }
593
+
594
+ /**
595
+ * Returns a list of all output connections
596
+ * @returns {Connection[]}
597
+ */
598
+ all_connections()
599
+ {
600
+ return Object.values(this.outputs)
601
+ .map(c => c.connections)
602
+ .reduce((a, b) => a.concat(b), [])
603
+ ;
604
+ }
605
+
606
+ /**
607
+ * Returns a list of all connections (both input and output)
608
+ * @returns {Connection[]}
609
+ */
382
610
  all_connections()
383
611
  {
384
612
  return Object.values(this.inputs).concat(Object.values(this.outputs))
@@ -388,13 +616,32 @@ class Node extends VisualElement
388
616
  }
389
617
  }
390
618
 
619
+ /**
620
+ * Node type
621
+ *
622
+ * This class is responsible for creating nodes, and adding logic for output values
623
+ */
391
624
  class NodeType
392
625
  {
626
+ /**
627
+ * @param {string} name Type name
628
+ */
393
629
  constructor(name)
394
630
  {
631
+ /**
632
+ * Type name
633
+ * @member {string}
634
+ */
395
635
  this.name = name;
396
636
  }
397
637
 
638
+ /**
639
+ * Creates and sets up a node for this type
640
+ *
641
+ * Custom types should override populate_node() to add connectors or otherwise customize the node
642
+ *
643
+ * @returns Node
644
+ */
398
645
  create_node()
399
646
  {
400
647
  let node = new Node(this.name, this);
@@ -402,23 +649,43 @@ class NodeType
402
649
  return node;
403
650
  }
404
651
 
405
- populate_node(node)
652
+ /**
653
+ * Allows custom types to add connectors or otherwise customize the node
654
+ * @protected
655
+ * @param {Node} node
656
+ */
657
+ populate_node(_node)
406
658
  {
407
659
  }
408
660
 
409
- update_node(node)
410
- {
411
-
412
- }
413
-
414
- evaluate_node(node)
661
+ /**
662
+ * Evaluates node logic
663
+ *
664
+ * Generally you should get input values with `node.inputs[name].get_value()`
665
+ * And set output values with `node.outputs[name].set_value()`.
666
+ *
667
+ * You can also alter the `node.preview` to add custom HTML to the node
668
+ * based on these values
669
+ *
670
+ * @param {Node} node
671
+ */
672
+ evaluate_node(_node)
415
673
  {
416
674
 
417
675
  }
418
676
  }
419
677
 
678
+ /**
679
+ * Context menu for the node editor
680
+ */
420
681
  class ContextMenu extends VisualElement
421
682
  {
683
+ /**
684
+ * @param {object|NodeType[]} actions If an object,
685
+ * keys will be items in the menu while values are the performed actions.
686
+ * Possible values are callbacks, NodeType instances (which will create new nodes),
687
+ * Or other actions definitions for submenus
688
+ */
422
689
  constructor(actions)
423
690
  {
424
691
  super();
@@ -487,6 +754,11 @@ class ContextMenu extends VisualElement
487
754
  return item;
488
755
  }
489
756
 
757
+ /**
758
+ * Opens the menu at the given point
759
+ * @param {number} x
760
+ * @param {number} y
761
+ */
490
762
  show(x, y)
491
763
  {
492
764
  this.dom.style.display = "";
@@ -494,19 +766,32 @@ class ContextMenu extends VisualElement
494
766
  this.dom.style.top = y + "px";
495
767
  }
496
768
 
769
+ /**
770
+ * Closes the menu
771
+ */
497
772
  close()
498
773
  {
499
774
  this.dom.style.display = "none";
500
775
  }
501
776
  }
502
777
 
778
+ /**
779
+ * Node Editor
780
+ */
503
781
  class Editor
504
782
  {
783
+
784
+ /**
785
+ * @param {HTMLElement} container Parent element in the DOM
786
+ * @param {object|NodeType[]} Actions for the context menu (See ContextMenu)
787
+ */
505
788
  constructor(container, context_menu={})
506
789
  {
507
790
  this.nodes = [];
508
791
 
509
792
  this.container = container;
793
+ if ( !container.hasAttribute("tabindex") )
794
+ container.setAttribute("tabindex", "0");
510
795
 
511
796
  this.area = container.appendChild(document.createElement("div"));
512
797
  container.classList.add("dgl-editor");
@@ -532,6 +817,12 @@ class Editor
532
817
  this.container.addEventListener("mousedown", this._on_area_mouse_down.bind(this));
533
818
  this.container.addEventListener("mousemove", this._on_area_mouse_move.bind(this));
534
819
  this.container.addEventListener("mouseup", this._on_area_mouse_up.bind(this));
820
+
821
+ this.container.addEventListener("touchstart", this._on_area_mouse_down.bind(this));
822
+ this.container.addEventListener("touchmove", this._on_area_mouse_move.bind(this));
823
+ this.container.addEventListener("touchend", this._on_area_mouse_up.bind(this));
824
+ this.container.addEventListener("touchcancel", this._on_area_mouse_up.bind(this));
825
+
535
826
  this.container.addEventListener("wheel", this._on_area_wheel.bind(this));
536
827
  this.container.addEventListener("contextmenu", this._on_area_menu.bind(this));
537
828
 
@@ -544,8 +835,6 @@ class Editor
544
835
  "Duplicate": () => {
545
836
  if ( this.selected_node ) {
546
837
  let node = this.create_node(this.selected_node.type, this.selected_node.get_input_value());
547
- node.x = this.selected_node.x;
548
- node.y = this.selected_node.y;
549
838
  this._select_node(node);
550
839
 
551
840
  }
@@ -556,10 +845,13 @@ class Editor
556
845
  this.container.appendChild(this.context_menu.to_dom(this));
557
846
  }
558
847
 
559
- create_node(type, data={})
848
+
849
+ /**
850
+ * Adds a new node to the graph
851
+ * @param {Node} node
852
+ */
853
+ add_node(node)
560
854
  {
561
- let node = type.create_node();
562
- node.set_input_value(data);
563
855
  node.x = this.cursor_x;
564
856
  node.y = this.cursor_y;
565
857
  this.nodes.push(node);
@@ -567,11 +859,53 @@ class Editor
567
859
  this.area.appendChild(node_dom);
568
860
 
569
861
  node_dom.addEventListener("mousedown", ev => this._on_node_mouse_down(ev, node));
862
+ node_dom.addEventListener("touchstart", ev => this._on_node_mouse_down(ev, node));
570
863
  node_dom.addEventListener("contextmenu", ev => this._on_node_menu(ev, node));
864
+ for ( let input of Object.values(node.inputs) )
865
+ {
866
+ if ( input.control )
867
+ {
868
+ input.control.addEventListener("change", () => this._refresh_graph());
869
+ }
870
+ }
871
+ }
571
872
 
873
+ /**
874
+ * Removes a node from the graph
875
+ * @param {Node} node
876
+ */
877
+ remove_node(node)
878
+ {
879
+ for ( let conn of node.all_connections() )
880
+ conn.disconnect();
881
+
882
+ let ind = this.nodes.indexOf(node);
883
+ if ( ind != -1 )
884
+ this.nodes.splice(ind, 1);
885
+ node.destroy();
886
+
887
+ this._refresh_graph();
888
+ }
889
+
890
+ /**
891
+ * Creates a new node from the given type
892
+ * @param {NodeType} type
893
+ * @param {object} data Optional data to populate node inputs
894
+ * @returns {Node} The created node
895
+ */
896
+ create_node(type, data={})
897
+ {
898
+ let node = type.create_node();
899
+ node.set_input_value(data);
900
+ this.add_node(node);
572
901
  return node;
573
902
  }
574
903
 
904
+ /**
905
+ * Automatically lays out the graph
906
+ *
907
+ * It assumes all nodes and connections are set up
908
+ */
575
909
  auto_layout()
576
910
  {
577
911
  setTimeout(() => this._on_auto_layout());
@@ -664,8 +998,60 @@ class Editor
664
998
  }
665
999
 
666
1000
  this._refresh_graph();
1001
+ this.fit_view();
1002
+ }
1003
+
1004
+ /**
1005
+ * Pan and zoom to fit all the nodes
1006
+ * @param {number} margin Margin to leave around the area
1007
+ */
1008
+ fit_view(margin=20)
1009
+ {
1010
+ if ( this.nodes.length == 0 )
1011
+ return;
1012
+
1013
+ let left = Infinity;
1014
+ let right = -Infinity;
1015
+ let top = Infinity;
1016
+ let bottom = -Infinity;
1017
+
1018
+ for ( let node of this.nodes )
1019
+ {
1020
+ let rect = node.dom.getBoundingClientRect();
1021
+ if ( rect.left < left ) left = rect.left;
1022
+ if ( rect.right > right ) right = rect.right;
1023
+ if ( rect.top < top ) top = rect.top;
1024
+ if ( rect.bottom > bottom ) bottom = rect.bottom;
1025
+ }
1026
+
1027
+ let crect = this.container.getBoundingClientRect();
1028
+
1029
+ left += -margin - crect.left;
1030
+ right += margin - crect.left;
1031
+ top += -margin - crect.top;
1032
+ bottom += margin - crect.top;
1033
+
1034
+ let width = right - left;
1035
+ let height = bottom - top;
1036
+ let x_scale = crect.width / width;
1037
+ let y_scale = crect.height / height;
1038
+ let scale = Math.min(x_scale, y_scale);
1039
+ this.scale = scale;
1040
+
1041
+ this.offset_x = (crect.width - width * scale) / 2 - left + margin;
1042
+ this.offset_y = (crect.height - height * scale) / 2 - top + margin;
1043
+ this._apply_transform();
1044
+
667
1045
  }
668
1046
 
1047
+ /**
1048
+ * Adds a connection to the graph
1049
+ * @param {Node} output_node Node the connection is coming from
1050
+ * @param {string} output_name Name of the output connector within output_node
1051
+ * @param {Node} input_node Node the connection is going to
1052
+ * @param {string} input_name Name of the input connector within input_node
1053
+ * @return {Connection?} Connection (Or null if the connection is impossible)
1054
+ */
669
1055
  connect(output_node, output_name, input_node, input_name)
670
1056
  {
671
1057
  let conn = output_node.connect(output_name, input_node, input_name);
@@ -681,6 +1067,10 @@ class Editor
681
1067
  return conn;
682
1068
  }
683
1069
 
1070
+ /**
1071
+ * Removes a connection from the graph
1072
+ * @param {Connection} connection
1073
+ */
684
1074
  disconnect(connection)
685
1075
  {
686
1076
  connection.disconnect();
@@ -700,18 +1090,27 @@ class Editor
700
1090
  this.node_menu.close();
701
1091
  }
702
1092
 
1093
+ _touch_mouse_pos(ev)
1094
+ {
1095
+ if ( ev.changedTouches )
1096
+ return ev.changedTouches[0];
1097
+ return ev;
1098
+ }
1099
+
703
1100
  _on_area_mouse_down(ev)
704
1101
  {
1102
+ this.container.focus();
705
1103
  this._close_menus();
706
1104
  if ( ev.button == 0 )
707
1105
  {
708
1106
  this._select_node(null);
709
1107
  }
710
- else if ( ev.button == 1 )
1108
+ else if ( ev.button == 1 || ev.changedTouches )
711
1109
  {
712
1110
  let rect = this.container.getBoundingClientRect();
713
- this.drag_start_x = ev.clientX - rect.left;
714
- this.drag_start_y = ev.clientY - rect.top;
1111
+ let evp = this._touch_mouse_pos(ev);
1112
+ this.drag_start_x = evp.clientX - rect.left;
1113
+ this.drag_start_y = evp.clientY - rect.top;
715
1114
  this.drag_start_ox = this.offset_x;
716
1115
  this.drag_start_oy = this.offset_y;
717
1116
  this.drag = "pan";
@@ -737,8 +1136,9 @@ class Editor
737
1136
  _on_area_mouse_move(ev)
738
1137
  {
739
1138
  let rect = this.container.getBoundingClientRect();
740
- let drag_x = ev.clientX - rect.left;
741
- let drag_y = ev.clientY - rect.top;
1139
+ let evp = this._touch_mouse_pos(ev);
1140
+ let drag_x = evp.clientX - rect.left;
1141
+ let drag_y = evp.clientY - rect.top;
742
1142
  this.cursor_x = (drag_x - this.offset_x) / this.scale;
743
1143
  this.cursor_y = (drag_y - this.offset_y) / this.scale;
744
1144
 
@@ -758,7 +1158,8 @@ class Editor
758
1158
  let y = this.drag_start_oy + delta_y / this.scale;
759
1159
  this.selected_node.x = x;
760
1160
  this.selected_node.y = y;
761
- this.selected_node.dom.style.transform = `translate(${x}px, ${y}px`;
1161
+ this.selected_node.dom.style.left = `${x}px`;
1162
+ this.selected_node.dom.style.top = `${y}px`;
762
1163
  for ( let connection of this.selected_node.all_connections() )
763
1164
  this._update_connection(connection);
764
1165
  }
@@ -803,7 +1204,8 @@ class Editor
803
1204
 
804
1205
  _get_dest_connector(ev)
805
1206
  {
806
- let elem = document.elementFromPoint(ev.clientX, ev.clientY);
1207
+ let evp = this._touch_mouse_pos(ev);
1208
+ let elem = document.elementFromPoint(evp.clientX, evp.clientY);
807
1209
  if ( !elem.classList.contains("dgl-socket") )
808
1210
  return null;
809
1211
 
@@ -845,6 +1247,9 @@ class Editor
845
1247
  ev.preventDefault();
846
1248
  ev.stopPropagation();
847
1249
 
1250
+ if ( !this.container.contains(document.activeElement) )
1251
+ return;
1252
+
848
1253
  let rect = this.container.getBoundingClientRect();
849
1254
  let mouse_x = ev.clientX - rect.left;
850
1255
  let mouse_y = ev.clientY - rect.top;
@@ -870,15 +1275,17 @@ class Editor
870
1275
 
871
1276
  _on_node_mouse_down(ev, node)
872
1277
  {
1278
+ // this.container.focus();
873
1279
  this._close_menus();
874
- if ( ev.button == 0 )
1280
+ if ( ev.button == 0 || ev.changedTouches )
875
1281
  {
876
1282
  ev.stopPropagation();
877
1283
  this._select_node(node);
878
1284
  this.drag = "node";
879
1285
  let rect = this.container.getBoundingClientRect();
880
- this.drag_start_x = ev.clientX - rect.left;
881
- this.drag_start_y = ev.clientY - rect.top;
1286
+ let evp = this._touch_mouse_pos(ev);
1287
+ this.drag_start_x = evp.clientX - rect.left;
1288
+ this.drag_start_y = evp.clientY - rect.top;
882
1289
  this.drag_start_ox = node.x;
883
1290
  this.drag_start_oy = node.y;
884
1291
  if ( ev.target.classList.contains("dgl-socket") )