linny-r 2.0.9 → 2.0.11

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.
@@ -75,6 +75,11 @@ class ModalDialog {
75
75
  // Make this modal dialog invisible.
76
76
  this.modal.style.display = 'none';
77
77
  }
78
+
79
+ get showing() {
80
+ // Return TRUE iff this modal dialog is visible.
81
+ return this.modal.style.display === 'block';
82
+ }
78
83
 
79
84
  } // END of class ModalDialog
80
85
 
@@ -85,15 +90,19 @@ class ModalDialog {
85
90
  // such that fields[name] is the entity property name that corresponds
86
91
  // with the DOM input element for that property. For example, for the
87
92
  // process group properties dialog, fields['LB'] = 'lower_bound' to
88
- // indicate that the DOM element having id="process_LB" corresponds to
93
+ // indicate that the DOM element having id="process-LB" corresponds to
89
94
  // the property `p.lower_bound` of process `p`.
90
95
  class GroupPropertiesDialog extends ModalDialog {
91
96
  constructor(id, fields) {
92
97
  super(id);
98
+ // `fields` is the object that relates HTML elements to properties.
93
99
  this.fields = fields;
94
100
  // `group` holds the entities (all of the same type) that should be
95
101
  // updated when the OK-button of the dialog is clicked.
96
102
  this.group = [];
103
+ // `selected_ds` is the dataset that was selected in the Finder when
104
+ // opening this dialog, or the first dataset in the group list.
105
+ this.selected_ds = null;
97
106
  // `initial_values` is a "dictionary" with (field name, value) entries
98
107
  // that hold the initial values of the group-editable properties.
99
108
  this.initial = {};
@@ -123,28 +132,58 @@ class GroupPropertiesDialog extends ModalDialog {
123
132
  e.addEventListener('keydown', fnc);
124
133
  }
125
134
  }
135
+ const spe = this.element('prefix');
136
+ if(spe) {
137
+ spe.addEventListener('keydown', fnc);
138
+ document.getElementById('dsg-add-modif-btn').addEventListener(
139
+ 'click', () => UI.modals.datasetgroup.promptForSelector('Add'));
140
+ document.getElementById('dsg-rename-modif-btn').addEventListener(
141
+ 'click', () => UI.modals.datasetgroup.promptForSelector('Rename'));
142
+ document.getElementById('dsg-edit-modif-btn').addEventListener(
143
+ 'click', () => UI.modals.datasetgroup.editExpression());
144
+ document.getElementById('dsg-delete-modif-btn').addEventListener(
145
+ 'click', () => UI.modals.datasetgroup.deleteModifier());
146
+ this.selector_modal = new ModalDialog('group-selector');
147
+ this.selector_modal.ok.addEventListener(
148
+ 'click', () => UI.modals.datasetgroup.selectorAction());
149
+ this.selector_modal.cancel.addEventListener(
150
+ 'click', () => UI.modals.datasetgroup.selector_modal.hide());
151
+ }
126
152
  }
127
153
 
128
154
  resetFields() {
129
155
  // Remove all class names from fields that relate to their "same"
130
156
  // and "changed status, and reset group-related properties.
131
- for(let name in this.fields) if(this.initial.hasOwnProperty(name)) {
132
- const cl = this.element(name).classList;
133
- while(cl.length > 0 && cl.item(cl.length - 1).indexOf('same-') >= 0) {
134
- cl.remove(cl.item(cl.length - 1));
157
+ function stripClassList(e) {
158
+ if(e) {
159
+ const cl = e.classList;
160
+ while(cl.length > 0 && cl.item(cl.length - 1).indexOf('same-') >= 0) {
161
+ cl.remove(cl.item(cl.length - 1));
162
+ }
135
163
  }
136
164
  }
165
+ for(let name in this.fields) if(this.initial.hasOwnProperty(name)) {
166
+ stripClassList(this.element(name));
167
+ }
168
+ stripClassList(this.element('prefix'));
137
169
  this.element('group').innerText = '';
138
- let e = this.element('name');
139
- if(e) e.disabled = false;
140
- e = this.element('actor');
141
- if(e) e.disabled = false;
142
- e = this.element('cluster');
143
- if(e) e.disabled = false;
170
+ for(const id of ['name', 'actor', 'cluster']) {
171
+ const e = this.element(id);
172
+ if(e) e.disabled = false;
173
+ }
174
+ const e = this.element('io');
175
+ if(e) e.style.display = 'block';
144
176
  this.group.length = 0;
145
177
  this.initial = {};
146
178
  this.same = {};
147
179
  this.changed = {};
180
+ this.shared_prefix = '';
181
+ this.selectors = {};
182
+ this.selected_selector = '';
183
+ this.default_selectors = [];
184
+ this.new_defsel = false;
185
+ this.same_defsel = true;
186
+ this.last_time_clicked = 0;
148
187
  }
149
188
 
150
189
  setFields(obj) {
@@ -196,23 +235,212 @@ class GroupPropertiesDialog extends ModalDialog {
196
235
  this.same[name] = same;
197
236
  }
198
237
  }
238
+ // For the dataset group dialog, more fields must be determined.
239
+ if(obj instanceof Dataset) {
240
+ // Determine the longest prefix shared by ALL datasets in the group.
241
+ this.shared_prefix = UI.sharedPrefix(obj.name, obj.name);
242
+ for(const ds of this.group) {
243
+ const sp = UI.sharedPrefix(obj.name, ds.name);
244
+ if(sp && this.shared_prefix.startsWith(sp)) {
245
+ this.shared_prefix = sp;
246
+ } else if(!sp.startsWith(this.shared_prefix)) {
247
+ this.shared_prefix = '';
248
+ break;
249
+ }
250
+ }
251
+ this.element('prefix').value = this.shared_prefix;
252
+ // Determine the set of all dataset modifier selectors while counting
253
+ // the number of occurrences of each selector and checking whether
254
+ // the modifier expressions are identical.
255
+ // NOTE: Here, too, take `obj` as the reference object.
256
+ this.selectors = {};
257
+ this.selected_selector = '';
258
+ this.default_selectors = [];
259
+ this.new_defsel = false;
260
+ this.same_defsel = true;
261
+ if(obj.default_selector) {
262
+ this.default_selectors.push(UI.nameToID(obj.default_selector));
263
+ }
264
+ for(const k of Object.keys(obj.modifiers)) {
265
+ const dsm = obj.modifiers[k];
266
+ this.selectors[k] = {
267
+ count: 1,
268
+ sel: dsm.selector,
269
+ expr: dsm.expression.text,
270
+ same_x: true,
271
+ new_s: false,
272
+ new_x: false,
273
+ deleted: false
274
+ };
275
+ }
276
+ // Then iterate over all datasets, excluding `obj`.
277
+ for(const ds of this.group) if(ds !== obj) {
278
+ const defsel = UI.nameToID(ds.default_selector);
279
+ if(this.default_selectors.indexOf(defsel) < 0) this.same_defsel = false;
280
+ if(defsel) addDistinct(defsel, this.default_selectors);
281
+ for(const k of Object.keys(ds.modifiers)) {
282
+ const
283
+ dsm = ds.modifiers[k],
284
+ s = this.selectors[k];
285
+ if(s) {
286
+ s.count++;
287
+ s.same_x = s.same_x && dsm.expression.text === s.expr;
288
+ } else {
289
+ this.selectors[k] = {
290
+ count: 1,
291
+ sel: dsm.selector,
292
+ expr: dsm.expression.text,
293
+ same_x: true,
294
+ new_s: false,
295
+ new_x: false,
296
+ deleted: false
297
+ };
298
+ }
299
+ }
300
+ }
301
+ // Selectors are not "same" when they do not apply to all datasets
302
+ // in the group.
303
+ const n = this.group.length;
304
+ for(const k of Object.keys(this.selectors)) {
305
+ const s = this.selectors[k];
306
+ s.same_s = s.count === n;
307
+ }
308
+ this.updateModifierList();
309
+ }
310
+ }
311
+
312
+ updateModifierList() {
313
+ // Display the modifier set for a dataset group.
314
+ const
315
+ trl = [],
316
+ not = (x) => { return (x === false ? 'not-' : ''); },
317
+ mdef = (this.new_defsel !== false ? this.new_defsel :
318
+ (this.default_selectors.length ? this.default_selectors[0] : '')),
319
+ sdef = not(this.same_defsel),
320
+ cdef = not(this.new_defsel);
321
+ for(const k of Object.keys(this.selectors)) {
322
+ const
323
+ s = this.selectors[k],
324
+ ms = (s.new_s === false ? s.sel : s.new_s),
325
+ mx = (s.new_x === false ? s.expr : s.new_x),
326
+ wild = (ms.indexOf('*') >= 0 || ms.indexOf('?') >= 0),
327
+ clk = `" onclick="UI.modals.datasetgroup.selectGroupSelector(event, \'${k}\'`;
328
+ // Do not display deleted modifiers.
329
+ if(s.deleted) continue;
330
+ trl.push(['<tr id="dsgs-', k, '" class="dataset-modif',
331
+ (k === this.selected_selector ? ' sel-set' : ''),
332
+ '"><td class="dataset-selector',
333
+ ` ${not(s.same_s)}same-${not(s.new_s)}changed`,
334
+ (wild ? ' wildcard' : ''),
335
+ '" title="Shift-click to ', (s.defsel ? 'clear' : 'set as'),
336
+ ' default modifier', clk, ', false);">',
337
+ (k === mdef ||
338
+ (this.new_defsel === false && this.default_selectors.indexOf(k) >= 0) ?
339
+ `<img src="images/solve-${sdef}same-${cdef}changed.png" ` +
340
+ 'style="height: 14px; width: 14px; margin: 0 1px -3px -1px;">' : ''),
341
+ (wild ? wildcardFormat(ms, true) : ms),
342
+ '</td><td class="dataset-expression',
343
+ ` ${not(s.same_x)}same-${not(s.new_x)}changed`, clk,
344
+ ', true);">', mx, '</td></tr>'
345
+ ].join(''));
346
+ }
347
+ this.element('modif-table').innerHTML = trl.join('');
348
+ if(this.selected_selector) UI.scrollIntoView(
349
+ document.getElementById('dsg-' + this.selected_selector));
350
+ const btns = 'dsg-rename-modif dsg-edit-modif dsg-delete-modif';
351
+ if(this.selected_selector) {
352
+ UI.enableButtons(btns);
353
+ } else {
354
+ UI.disableButtons(btns);
355
+ }
199
356
  }
200
357
 
358
+ selectGroupSelector(event, id, x=true) {
359
+ // Select modifier selector, or when double-clicked, edit its expression when
360
+ // x = TRUE, or the name of the modifier when x = FALSE.
361
+ const edit = event.altKey || this.doubleClicked(id);
362
+ this.selected_selector = id;
363
+ if(edit) {
364
+ this.last_time_clicked = 0;
365
+ if(x) {
366
+ this.editExpression();
367
+ } else {
368
+ this.promptForSelector('Rename');
369
+ }
370
+ return;
371
+ }
372
+ if(event.shiftKey) {
373
+ // Toggle new default selector.
374
+ this.new_defsel = (this.new_defsel === id ? '' : id);
375
+ }
376
+ this.updateModifierList();
377
+ }
378
+
379
+ doubleClicked(s) {
380
+ const
381
+ now = Date.now(),
382
+ dt = now - this.last_time_clicked;
383
+ this.last_time_clicked = now;
384
+ if(s === this.selected_selector) {
385
+ // Consider click to be "double" if it occurred less than 300 ms ago.
386
+ if(dt < 300) {
387
+ this.last_time_clicked = 0;
388
+ return true;
389
+ }
390
+ }
391
+ this.clicked_selector = s;
392
+ return false;
393
+ }
394
+
395
+ enterKey() {
396
+ // Open "edit" dialog for the selected modifier.
397
+ const
398
+ tbl = this.element('modif-table'),
399
+ srl = tbl.getElementsByClassName('sel-set');
400
+ if(srl.length > 0) {
401
+ const r = tbl.rows[srl[0].rowIndex];
402
+ if(r) {
403
+ // Emulate a double-click on the second cell to edit the expression.
404
+ this.last_time_clicked = Date.now();
405
+ r.cells[1].dispatchEvent(new Event('click'));
406
+ }
407
+ }
408
+ }
409
+
410
+ upDownKey(dir) {
411
+ // Select row above or below the selected one (if possible).
412
+ const
413
+ tbl = this.element('modif-table'),
414
+ srl = tbl.getElementsByClassName('sel-set');
415
+ if(srl.length > 0) {
416
+ let r = tbl.rows[srl[0].rowIndex + dir];
417
+ while(r && r.style.display === 'none') {
418
+ r = (dir > 0 ? r.nextSibling : r.previousSibling);
419
+ }
420
+ if(r) {
421
+ UI.scrollIntoView(r);
422
+ // NOTE: Cell, not row, listens for onclick event.
423
+ r.cells[1].dispatchEvent(new Event('click'));
424
+ }
425
+ }
426
+ }
427
+
201
428
  show(attr, obj) {
202
429
  // Make dialog visible with same/changed status and disabled name,
203
430
  // actor and cluster fields.
204
431
  // NOTE: Cluster dialog is also used to *add* a new cluster, and in
205
- // that case no fields should be set
432
+ // that case no fields should be set.
206
433
  if(obj) this.setFields(obj);
207
434
  if(obj && this.group.length > 0) {
208
435
  this.element('group').innerText = `(N=${this.group.length})`;
209
- // Disable name, actor and cluster fields if they exist.
210
- let e = this.element('name');
211
- if(e) e.disabled = true;
212
- e = this.element('actor');
213
- if(e) e.disabled = true;
214
- e = this.element('cluster');
215
- if(e) e.disabled = true;
436
+ // Disable name, actor, and cluster fields if they exist.
437
+ for(const id of ['name', 'actor', 'cluster']) {
438
+ const e = this.element(id);
439
+ if(e) e.disabled = true;
440
+ }
441
+ // Hide io field if it exists.
442
+ const e = this.element('io');
443
+ if(e) e.style.display = 'none';
216
444
  // Set the right colors to reflect same and changed status.
217
445
  this.highlightModifiedFields();
218
446
  }
@@ -231,6 +459,7 @@ class GroupPropertiesDialog extends ModalDialog {
231
459
  // Set the CSS classes of fields so that they reflect their "same"
232
460
  // and "changed" status.
233
461
  if(this.group.length === 0) return;
462
+ const not = {false: 'not-', true: ''};
234
463
  for(let name in this.initial) if(this.initial.hasOwnProperty(name)) {
235
464
  const
236
465
  iv = this.initial[name],
@@ -238,7 +467,6 @@ class GroupPropertiesDialog extends ModalDialog {
238
467
  // for which `same[name]` is TRUE iff all entities had identical
239
468
  // values for the property identified by `name` when the dialog
240
469
  // was opened.
241
- not = {false: 'not-', true: ''},
242
470
  same = `${not[this.same[name]]}same`,
243
471
  el = this.element(name);
244
472
  let changed = false,
@@ -261,10 +489,20 @@ class GroupPropertiesDialog extends ModalDialog {
261
489
  // Compute current value as Boolean.
262
490
  const v = (type === 'box' ? state ==='checked' : state === 'eq');
263
491
  changed = (v !== iv);
492
+ // When array box for dataset group is (un)checked, the time aspects
493
+ // cover div must be hidden (shown).
494
+ if(name === 'array') {
495
+ this.element('no-time-msg').style.display = (v ? 'block' : 'none');
496
+ }
264
497
  }
265
498
  this.changed[name] = changed;
266
499
  el.className = `${type} ${state} ${same}-${not[changed]}changed`.trim();
267
500
  }
501
+ const spe = this.element('prefix');
502
+ if(spe) {
503
+ const changed = spe.value !== this.shared_prefix;
504
+ spe.className = `same-${not[changed]}changed`;
505
+ }
268
506
  }
269
507
 
270
508
  validLinkProperty(link, property, value=0) {
@@ -313,7 +551,105 @@ class GroupPropertiesDialog extends ModalDialog {
313
551
  }
314
552
  }
315
553
  }
554
+
555
+ promptForSelector(action) {
556
+ // Open the group selector modal for the specified action.
557
+ let ms = '',
558
+ md = this.selector_modal;
559
+ if(action === 'Rename') {
560
+ ms = this.selectors[this.selected_selector].sel;
561
+ }
562
+ md.element('action').innerText = action;
563
+ md.element('name').value = ms;
564
+ md.show('name');
565
+ }
566
+
567
+ selectorAction() {
568
+ // Perform the specified selector action.
569
+ const
570
+ md = this.selector_modal,
571
+ action = md.element('action').innerText,
572
+ ne = md.element('name'),
573
+ ms = MODEL.validSelector(ne.value);
574
+ if(!ms) {
575
+ ne.focus();
576
+ return;
577
+ }
578
+ const ok = (action === 'Add' ? this.addSelector(ms) : this.renameSelector(ms));
579
+ if(ok) {
580
+ md.hide();
581
+ this.updateModifierList();
582
+ }
583
+ }
584
+
585
+ addSelector(ms) {
586
+ // Create a new selector and adds it to the list.
587
+ const k = UI.nameToID(ms);
588
+ if(!this.selectors.hasOwnProperty(k)) {
589
+ this.selectors[k] = {
590
+ count: 1,
591
+ sel: ms,
592
+ expr: '',
593
+ same_x: true,
594
+ new_s: ms,
595
+ new_x: '',
596
+ deleted: false
597
+ };
598
+ }
599
+ this.selected_selector = k;
600
+ return true;
601
+ }
316
602
 
603
+ renameSelector(ms) {
604
+ // Record the new name for this selector as property `new_s`.
605
+ if(this.selected_selector) {
606
+ const sel = this.selectors[this.selected_selector];
607
+ // NOTES:
608
+ // (1) When renaming, the old name is be preserved.
609
+ // (2) Name changes do not affect the key of the selector.
610
+ // (3) When the new name is identical to the original, record this
611
+ // by setting `new_s` to FALSE.
612
+ sel.new_s = (ms === sel.sel ? false : ms);
613
+ }
614
+ return true;
615
+ }
616
+
617
+ editExpression() {
618
+ // Open the Expression editor for the selected expression.
619
+ const sel = this.selectors[this.selected_selector];
620
+ if(sel) {
621
+ const md = UI.modals.expression;
622
+ md.element('property').innerHTML = '(dataset group)' +
623
+ UI.OA_SEPARATOR + sel.sel;
624
+ md.element('text').value = sel.new_x || sel.expr;
625
+ document.getElementById('variable-obj').value = 0;
626
+ X_EDIT.updateVariableBar();
627
+ X_EDIT.clearStatusBar();
628
+ X_EDIT.showPrefix(this.shared_prefix);
629
+ md.show('text');
630
+ }
631
+ }
632
+
633
+ modifyExpression(x) {
634
+ // Record the new expression for the selected selector.
635
+ // NOTE: Expressions are compiled when changes are saved.
636
+ const sel = this.selectors[this.selected_selector];
637
+ // NOTE: When the new expression is identical to the original,
638
+ // record this by setting `new_x` to FALSE.
639
+ if(sel) sel.new_x = (x === sel.expr ? false : x);
640
+ this.updateModifierList();
641
+ }
642
+
643
+ deleteModifier() {
644
+ // Record that the selected modifier should be deleted.
645
+ const sel = this.selectors[this.selected_selector];
646
+ if(sel) {
647
+ sel.deleted = true;
648
+ this.selected_selector = '';
649
+ this.updateModifierList();
650
+ }
651
+ }
652
+
317
653
  } // END of class GroupPropertiesDialog
318
654
 
319
655
 
@@ -499,6 +835,17 @@ class GUIController extends Controller {
499
835
  'no-links': 'no_links'
500
836
  });
501
837
 
838
+ // The Dataset group modal.
839
+ this.modals.datasetgroup = new GroupPropertiesDialog('datasetgroup', {
840
+ 'default': 'default_value',
841
+ 'unit': 'scale_unit',
842
+ 'periodic': 'periodic',
843
+ 'array': 'array',
844
+ 'time-scale': 'time_scale',
845
+ 'time-unit': 'time_unit',
846
+ 'method': 'method'
847
+ });
848
+
502
849
  // Initially, no dialog being dragged or resized.
503
850
  this.dr_dialog = null;
504
851
 
@@ -810,6 +1157,11 @@ class GUIController extends Controller {
810
1157
  this.modals.product.element('io').addEventListener('click',
811
1158
  () => UI.toggleImportExportBox('product'));
812
1159
 
1160
+ this.modals.datasetgroup.ok.addEventListener('click',
1161
+ () => FINDER.updateDatasetGroupProperties());
1162
+ this.modals.datasetgroup.cancel.addEventListener('click',
1163
+ () => UI.modals.datasetgroup.hide());
1164
+
813
1165
  this.modals.link.ok.addEventListener('click',
814
1166
  () => UI.updateLinkProperties());
815
1167
  this.modals.link.cancel.addEventListener('click',
@@ -873,6 +1225,12 @@ class GUIController extends Controller {
873
1225
  // Ensure that all modal windows respond to ESCape
874
1226
  // (and more in general to other special keys).
875
1227
  document.addEventListener('keydown', (event) => UI.checkModals(event));
1228
+ // Ensure that all modal dialogs "swallow" mousedown events, as otherwise
1229
+ // these may alo be processed by the main window drawing canvas.
1230
+ for(const modal of document.getElementsByClassName('modal')) {
1231
+ modal.addEventListener('mousedown', (event) => event.stopPropagation());
1232
+ }
1233
+
876
1234
  }
877
1235
 
878
1236
  setConstraintUnderCursor(c) {
@@ -1575,18 +1933,19 @@ class GUIController extends Controller {
1575
1933
  // Button functionality
1576
1934
  //
1577
1935
 
1578
- enableButtons(btns) {
1936
+ enableButtons(btns, blink=false) {
1579
1937
  for(const btn of btns.trim().split(/\s+/)) {
1580
1938
  const b = document.getElementById(btn + '-btn');
1581
- b.classList.remove('disab', 'activ');
1939
+ b.classList.remove('disab', 'activ', 'blink');
1582
1940
  b.classList.add('enab');
1941
+ if(blink) b.classList.add('blink');
1583
1942
  }
1584
1943
  }
1585
1944
 
1586
1945
  disableButtons(btns) {
1587
1946
  for(const btn of btns.trim().split(/\s+/)) {
1588
1947
  const b = document.getElementById(btn + '-btn');
1589
- b.classList.remove('enab', 'activ', 'stay-activ');
1948
+ b.classList.remove('enab', 'activ', 'stay-activ', 'blink');
1590
1949
  b.classList.add('disab');
1591
1950
  }
1592
1951
  }
@@ -2154,9 +2513,9 @@ class GUIController extends Controller {
2154
2513
  // dragging its endpoint).
2155
2514
  } else if(this.constraining_node) {
2156
2515
  if(this.on_node && this.constraining_node.canConstrain(this.on_node)) {
2157
- // display constraint editor
2158
- CONSTRAINT_EDITOR.from_name.innerHTML = this.constraining_node.displayName;
2159
- CONSTRAINT_EDITOR.to_name.innerHTML = this.on_node.displayName;
2516
+ // Display constraint editor.
2517
+ CONSTRAINT_EDITOR.from_name.innerText = this.constraining_node.displayName;
2518
+ CONSTRAINT_EDITOR.to_name.innerText = this.on_node.displayName;
2160
2519
  CONSTRAINT_EDITOR.showDialog();
2161
2520
  }
2162
2521
  this.linking_node = null;
@@ -2280,17 +2639,11 @@ class GUIController extends Controller {
2280
2639
  // Handler for keyboard events
2281
2640
  //
2282
2641
 
2283
- checkModals(e) {
2284
- // Respond to Escape, Enter and shortcut keys.
2285
- const
2286
- ttype = e.target.type,
2287
- ttag = e.target.tagName,
2288
- modals = document.getElementsByClassName('modal');
2289
- // Modal dialogs: hide on ESC and move to next input on ENTER.
2642
+ get topModal() {
2643
+ // Return the topmost visible modal dialog, or NULL if none are showing.
2644
+ const modals = document.getElementsByClassName('modal');
2290
2645
  let maxz = 0,
2291
- topmod = null,
2292
- code = e.code,
2293
- alt = e.altKey;
2646
+ topmod = null;
2294
2647
  for(const m of modals) {
2295
2648
  const
2296
2649
  cs = window.getComputedStyle(m),
@@ -2300,6 +2653,18 @@ class GUIController extends Controller {
2300
2653
  maxz = z;
2301
2654
  }
2302
2655
  }
2656
+ return topmod;
2657
+ }
2658
+
2659
+ checkModals(e) {
2660
+ // Respond to Escape, Enter and shortcut keys.
2661
+ const
2662
+ ttype = e.target.type,
2663
+ ttag = e.target.tagName,
2664
+ code = e.code,
2665
+ alt = e.altKey,
2666
+ topmod = this.topModal;
2667
+ // Modal dialogs: hide on ESC and move to next input on ENTER.
2303
2668
  // NOTE: Consider only the top modal (if any is showing).
2304
2669
  if(code === 'Escape') {
2305
2670
  e.stopImmediatePropagation();
@@ -2312,15 +2677,16 @@ class GUIController extends Controller {
2312
2677
  while(i < inp.length && inp[i].disabled) i++;
2313
2678
  if(i < inp.length) {
2314
2679
  inp[i].focus();
2315
- } else if(['constraint-modal', 'boundline-data-modal',
2680
+ } else if(['datasetgroup-modal', 'constraint-modal', 'boundline-data-modal',
2316
2681
  'xp-clusters-modal'].indexOf(topmod.id) >= 0) {
2317
- // NOTE: Constraint modal, boundline data modal and "ignore clusters" modal must NOT close
2318
- // when Enter is pressed, but only de-focus the input field.
2682
+ // NOTE: These modals must NOT close when Enter is pressed, but only
2683
+ // de-focus the input field.
2319
2684
  e.target.blur();
2320
2685
  } else {
2321
2686
  const btns = topmod.getElementsByClassName('ok-btn');
2322
2687
  if(btns.length > 0) btns[0].dispatchEvent(new Event('click'));
2323
2688
  }
2689
+ if(topmod.id === 'datasetgroup-modal') UI.modals.datasetgroup.enterKey();
2324
2690
  } else if(this.dr_dialog_order.length > 0) {
2325
2691
  // Send ENTER key event to the top draggable dialog.
2326
2692
  const last = this.dr_dialog_order.length - 1;
@@ -2334,15 +2700,22 @@ class GUIController extends Controller {
2334
2700
  // Prevent backspace to be interpreted (by FireFox) as "go back in browser".
2335
2701
  e.preventDefault();
2336
2702
  } else if(ttag === 'BODY') {
2337
- // Constraint Editor accepts arrow keys.
2338
- if(topmod && topmod.id === 'constraint-modal') {
2339
- if(code.startsWith('Arrow')) {
2703
+ // Dataset group modal and Constraint Editor accept arrow keys.
2704
+ if(topmod) {
2705
+ if(topmod.id === 'constraint-modal' && code.startsWith('Arrow')) {
2340
2706
  e.preventDefault();
2341
2707
  CONSTRAINT_EDITOR.arrowKey(e);
2342
2708
  return;
2343
2709
  }
2710
+ if(topmod.id === 'datasetgroup-modal' &&
2711
+ (code === 'ArrowUp' || code === 'ArrowDown')) {
2712
+ e.preventDefault();
2713
+ // NOTE: Pass key direction as -1 for UP and +1 for DOWN.
2714
+ UI.modals.datasetgroup.upDownKey(e.keyCode - 39);
2715
+ return;
2716
+ }
2344
2717
  }
2345
- // Up and down arrow keys.
2718
+ // Lists in draggable dialogs respond to up and down arrow keys.
2346
2719
  if(code === 'ArrowUp' || code === 'ArrowDown') {
2347
2720
  e.preventDefault();
2348
2721
  // Send event to the top draggable dialog.