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