linny-r 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linny-r",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Executable graphical language with WYSIWYG editor for MILP models",
5
5
  "main": "server.js",
6
6
  "scripts": {
package/static/index.html CHANGED
@@ -851,6 +851,26 @@ NOTE: Unit symbols are case-sensitive, so BTU ≠ Btu">
851
851
  </div>
852
852
  </div>
853
853
 
854
+ <!-- the CONFIRM LOAD modal asks to confirm to load model from repository -->
855
+ <div id="confirm-load-from-repo-modal" class="modal">
856
+ <div id="confirm-load-from-repo-dlg" class="inp-dlg">
857
+ <div class="dlg-title" style="background-color: #cc88b0">
858
+ Confirm load model
859
+ </div>
860
+ <div id="confirm-load-from-repo-msg">
861
+ Loading model
862
+ <span id="confirm-load-from-repo-mod-name"></span>
863
+ will discard changes you made to the current model. Continue?
864
+ </div>
865
+ <div id="confirm-load-from-repo-buttons">
866
+ <img class="ok-btn big-btn" src="images/ok.png">
867
+ Yes
868
+ <img class="cancel-btn big-btn" src="images/cancel.png">
869
+ No
870
+ </div>
871
+ </div>
872
+ </div>
873
+
854
874
  <!-- the CONFIRM DELETE modal asks to confirm to delete module from repository -->
855
875
  <div id="confirm-delete-from-repo-modal" class="modal">
856
876
  <div id="confirm-delete-from-repo-dlg" class="inp-dlg">
@@ -1924,10 +1924,6 @@ td.it {
1924
1924
  font-style: italic;
1925
1925
  }
1926
1926
 
1927
- td.series {
1928
-
1929
- }
1930
-
1931
1927
  td.io {
1932
1928
  width: 12px;
1933
1929
  }
@@ -2116,7 +2112,7 @@ td.array::before {
2116
2112
  td.series::before {
2117
2113
  content: ' \28B8';
2118
2114
  color: #b00080;
2119
- margin-left: -1px;
2115
+ margin-left: -4px;
2120
2116
  }
2121
2117
 
2122
2118
  td.outcome.modif::before {
@@ -2134,7 +2130,7 @@ td.array.modif::before {
2134
2130
  td.series.modif::before {
2135
2131
  content: ' \28B8\2045';
2136
2132
  color: #b00080;
2137
- margin-left: -1px;
2133
+ margin-left: -4px;
2138
2134
  }
2139
2135
 
2140
2136
  td.blackbox::before {
@@ -4036,6 +4032,11 @@ select.i-param {
4036
4032
  max-width: calc(100% - 15px);
4037
4033
  }
4038
4034
 
4035
+ #confirm-load-from-repo-dlg {
4036
+ width: 400px;
4037
+ height: 85px;
4038
+ }
4039
+
4039
4040
  #confirm-delete-from-repo-dlg {
4040
4041
  width: 270px;
4041
4042
  height: 120px;
@@ -4047,12 +4048,14 @@ select.i-param {
4047
4048
  font-weight: bold;
4048
4049
  }
4049
4050
 
4051
+ #confirm-load-from-repo-mod-name,
4050
4052
  #confirm-delete-from-repo-mod-name {
4051
4053
  word-break: keep-all;
4052
4054
  white-space: nowrap;
4053
4055
  font-family: monospace;
4054
4056
  }
4055
4057
 
4058
+ #confirm-load-from-repo-msg,
4056
4059
  #confirm-delete-from-repo-msg {
4057
4060
  height: calc(100% - 55px);
4058
4061
  }
@@ -4577,6 +4580,7 @@ div.call-stack-expr {
4577
4580
  height: min-content;
4578
4581
  }
4579
4582
 
4583
+ #confirm-load-from-repo-msg,
4580
4584
  #confirm-delete-from-repo-msg,
4581
4585
  #check-update-msg {
4582
4586
  width: calc(100% - 8px);
@@ -4585,6 +4589,7 @@ div.call-stack-expr {
4585
4589
  }
4586
4590
 
4587
4591
  #confirm-move-buttons,
4592
+ #confirm-load-from-repo-buttons,
4588
4593
  #confirm-delete-from-repo-buttons,
4589
4594
  #check-update-buttons {
4590
4595
  width: 100%;
@@ -4595,6 +4600,7 @@ div.call-stack-expr {
4595
4600
  }
4596
4601
 
4597
4602
  #confirm-move-buttons > img,
4603
+ #confirm-load-from-repo-buttons > img,
4598
4604
  #confirm-delete-from-repo-buttons > img,
4599
4605
  #check-update-buttons > img {
4600
4606
  float: none;
@@ -974,6 +974,9 @@ class SensitivityAnalysis {
974
974
  // Class ExperimentManager controls the collection of experiments of the model
975
975
  class ExperimentManager {
976
976
  constructor() {
977
+ // NOTE: the properties below are relevant only for the GUI
978
+ this.experiment_table = null;
979
+ this.focal_table = null;
977
980
  }
978
981
 
979
982
  reset() {
@@ -1005,6 +1008,7 @@ class ExperimentManager {
1005
1008
  selectExperiment(title) {
1006
1009
  const xi = MODEL.indexOfExperiment(title);
1007
1010
  this.selected_experiment = (xi < 0 ? null : MODEL.experiments[xi]);
1011
+ this.focal_table = this.experiment_table;
1008
1012
  this.updateDialog();
1009
1013
  }
1010
1014
 
@@ -4378,6 +4378,13 @@ class GUIController extends Controller {
4378
4378
  const btns = topmod.getElementsByClassName('ok-btn');
4379
4379
  if(btns.length > 0) btns[0].dispatchEvent(new Event('click'));
4380
4380
  }
4381
+ } else if(this.dr_dialog_order.length > 0) {
4382
+ // Send ENTER key event to the top draggable dialog
4383
+ const last = this.dr_dialog_order.length - 1;
4384
+ if(last >= 0) {
4385
+ const mgr = window[this.dr_dialog_order[last].dataset.manager];
4386
+ if(mgr && 'enterKey' in mgr) mgr.enterKey();
4387
+ }
4381
4388
  }
4382
4389
  } else if(e.keyCode === 8 &&
4383
4390
  ttype !== 'text' && ttype !== 'password' && ttype !== 'textarea') {
@@ -4392,7 +4399,18 @@ class GUIController extends Controller {
4392
4399
  return;
4393
4400
  }
4394
4401
  }
4395
- // end. home, Left and right arrow keys
4402
+ // Up and down arrow keys
4403
+ if([38, 40].indexOf(e.keyCode) >= 0) {
4404
+ e.preventDefault();
4405
+ // Send event to the top draggable dialog
4406
+ const last = this.dr_dialog_order.length - 1;
4407
+ if(last >= 0) {
4408
+ const mgr = window[this.dr_dialog_order[last].dataset.manager];
4409
+ // NOTE: pass key direction as -1 for UP and +1 for DOWN
4410
+ if(mgr && 'upDownKey' in mgr) mgr.upDownKey(e.keyCode - 39);
4411
+ }
4412
+ }
4413
+ // end, home, Left and right arrow keys
4396
4414
  if([35, 36, 37, 39].indexOf(e.keyCode) >= 0) e.preventDefault();
4397
4415
  if(e.keyCode === 35) {
4398
4416
  MODEL.t = MODEL.end_period - MODEL.start_period + 1;
@@ -8478,7 +8496,7 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
8478
8496
  document.getElementById('repo-include-btn').addEventListener(
8479
8497
  'click', () => REPOSITORY_BROWSER.includeModule());
8480
8498
  document.getElementById('repo-load-btn').addEventListener(
8481
- 'click', () => REPOSITORY_BROWSER.loadModuleAsModel());
8499
+ 'click', () => REPOSITORY_BROWSER.confirmLoadModuleAsModel());
8482
8500
  document.getElementById('repo-store-btn').addEventListener(
8483
8501
  'click', () => REPOSITORY_BROWSER.promptForStoring());
8484
8502
  document.getElementById('repo-black-box-btn').addEventListener(
@@ -8525,6 +8543,12 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
8525
8543
  this.include_modal.element('actor').addEventListener(
8526
8544
  'blur', () => REPOSITORY_BROWSER.updateActors());
8527
8545
 
8546
+ this.confirm_load_modal = new ModalDialog('confirm-load-from-repo');
8547
+ this.confirm_load_modal.ok.addEventListener(
8548
+ 'click', () => REPOSITORY_BROWSER.loadModuleAsModel());
8549
+ this.confirm_load_modal.cancel.addEventListener(
8550
+ 'click', () => REPOSITORY_BROWSER.confirm_load_modal.hide());
8551
+
8528
8552
  this.confirm_delete_modal = new ModalDialog('confirm-delete-from-repo');
8529
8553
  this.confirm_delete_modal.ok.addEventListener(
8530
8554
  'click', () => REPOSITORY_BROWSER.deleteFromRepository());
@@ -8536,6 +8560,31 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
8536
8560
  super.reset();
8537
8561
  this.last_time_selected = 0;
8538
8562
  }
8563
+
8564
+ enterKey() {
8565
+ // Open "edit properties" dialog for the selected entity
8566
+ const srl = this.modules_table.getElementsByClassName('sel-set');
8567
+ if(srl.length > 0) {
8568
+ const r = this.modules_table.rows[srl[0].rowIndex];
8569
+ if(r) {
8570
+ // Ensure that click will be interpreted as double-click
8571
+ this.last_time_selected = Date.now();
8572
+ r.dispatchEvent(new Event('click'));
8573
+ }
8574
+ }
8575
+ }
8576
+
8577
+ upDownKey(dir) {
8578
+ // Select row above or below the selected one (if possible)
8579
+ const srl = this.modules_table.getElementsByClassName('sel-set');
8580
+ if(srl.length > 0) {
8581
+ const r = this.modules_table.rows[srl[0].rowIndex + dir];
8582
+ if(r) {
8583
+ UI.scrollIntoView(r);
8584
+ r.dispatchEvent(new Event('click'));
8585
+ }
8586
+ }
8587
+ }
8539
8588
 
8540
8589
  get isLocalHost() {
8541
8590
  // Returns TRUE if first repository on the list is 'local host'
@@ -8718,7 +8767,7 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
8718
8767
  // Consider click to be "double" if it occurred less than 300 ms ago
8719
8768
  if(dt < 300) {
8720
8769
  this.last_time_selected = 0;
8721
- this.loadModuleAsModel();
8770
+ this.includeModule();
8722
8771
  return;
8723
8772
  }
8724
8773
  }
@@ -8967,6 +9016,7 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
8967
9016
 
8968
9017
  loadModuleAsModel() {
8969
9018
  // Loads selected module as model
9019
+ this.confirm_load_modal.hide();
8970
9020
  if(this.repository_index >= 0 && this.module_index >= 0) {
8971
9021
  // NOTE: when loading new model, the stay-on-top dialogs must be reset
8972
9022
  UI.hideStayOnTopDialogs();
@@ -8983,6 +9033,17 @@ class GUIRepositoryBrowser extends RepositoryBrowser {
8983
9033
  r.loadModule(this.module_index, true);
8984
9034
  }
8985
9035
  }
9036
+
9037
+ confirmLoadModuleAsModel() {
9038
+ // Prompts modeler to confirm loading the selected module as model
9039
+ if(this.repository_index >= 0 && this.module_index >= 0 &&
9040
+ document.getElementById('repo-load-btn').classList.contains('enab')) {
9041
+ const r = this.repositories[this.repository_index];
9042
+ this.confirm_load_modal.element('mod-name').innerText =
9043
+ r.module_names[this.module_index];
9044
+ this.confirm_load_modal.show();
9045
+ }
9046
+ }
8986
9047
 
8987
9048
  confirmDeleteFromRepository() {
8988
9049
  // Prompts modeler to confirm deletion of the selected module
@@ -9034,7 +9095,7 @@ class GUIDatasetManager extends DatasetManager {
9034
9095
  this.filter_text = document.getElementById('ds-filter-text');
9035
9096
  this.filter_text.addEventListener(
9036
9097
  'input', () => DATASET_MANAGER.changeFilter());
9037
- this.table = document.getElementById('dataset-table');
9098
+ this.dataset_table = document.getElementById('dataset-table');
9038
9099
  // Data properties pane
9039
9100
  this.properties = document.getElementById('dataset-properties');
9040
9101
  // Toggle buttons at bottom of dialog
@@ -9056,6 +9117,8 @@ class GUIDatasetManager extends DatasetManager {
9056
9117
  'click', () => DATASET_MANAGER.editExpression());
9057
9118
  document.getElementById('ds-delete-modif-btn').addEventListener(
9058
9119
  'click', () => DATASET_MANAGER.deleteModifier());
9120
+ // Modifier table
9121
+ this.modifier_table = document.getElementById('dataset-modif-table');
9059
9122
  // Modal dialogs
9060
9123
  this.new_modal = new ModalDialog('new-dataset');
9061
9124
  this.new_modal.ok.addEventListener(
@@ -9104,7 +9167,59 @@ class GUIDatasetManager extends DatasetManager {
9104
9167
  this.selected_modifier = null;
9105
9168
  this.edited_expression = null;
9106
9169
  this.filter_pattern = null;
9107
- this.last_time_selected = 0;
9170
+ this.clicked_object = null;
9171
+ this.last_time_clicked = 0;
9172
+ this.focal_table = null;
9173
+ }
9174
+
9175
+ doubleClicked(obj) {
9176
+ const
9177
+ now = Date.now(),
9178
+ dt = now - this.last_time_clicked;
9179
+ this.last_time_clicked = now;
9180
+ if(obj === this.clicked_object) {
9181
+ // Consider click to be "double" if it occurred less than 300 ms ago
9182
+ if(dt < 300) {
9183
+ this.last_time_clicked = 0;
9184
+ return true;
9185
+ }
9186
+ }
9187
+ this.clicked_object = obj;
9188
+ return false;
9189
+ }
9190
+
9191
+ enterKey() {
9192
+ // Open "edit" dialog for the selected dataset or modifier expression
9193
+ const srl = this.focal_table.getElementsByClassName('sel-set');
9194
+ if(srl.length > 0) {
9195
+ const r = this.focal_table.rows[srl[0].rowIndex];
9196
+ if(r) {
9197
+ const e = new Event('click');
9198
+ if(this.focal_table === this.dataset_table) {
9199
+ // Emulate Alt-click in the table to open the time series dialog
9200
+ e.altKey = true;
9201
+ r.dispatchEvent(e);
9202
+ } else if(this.focal_table === this.modifier_table) {
9203
+ // Emulate a double-click on the second cell to edit the expression
9204
+ this.last_time_clicked = Date.now();
9205
+ r.cells[1].dispatchEvent(e);
9206
+ }
9207
+ }
9208
+ }
9209
+ }
9210
+
9211
+ upDownKey(dir) {
9212
+ // Select row above or below the selected one (if possible)
9213
+ const srl = this.focal_table.getElementsByClassName('sel-set');
9214
+ if(srl.length > 0) {
9215
+ let r = this.focal_table.rows[srl[0].rowIndex + dir];
9216
+ if(r) {
9217
+ UI.scrollIntoView(r);
9218
+ // NOTE: cell, not row, listens for onclick event
9219
+ if(this.focal_table === this.modifier_table) r = r.cells[1];
9220
+ r.dispatchEvent(new Event('click'));
9221
+ }
9222
+ }
9108
9223
  }
9109
9224
 
9110
9225
  updateDialog() {
@@ -9148,10 +9263,10 @@ class GUIDatasetManager extends DatasetManager {
9148
9263
  '\', event.shiftKey);"><td', cls, '>', d.displayName,
9149
9264
  '</td></tr>'].join(''));
9150
9265
  }
9151
- this.table.innerHTML = dl.join('');
9266
+ this.dataset_table.innerHTML = dl.join('');
9152
9267
  const btns = 'ds-data ds-rename ds-clone ds-delete';
9153
9268
  if(sd) {
9154
- this.table.innerHTML = dl.join('');
9269
+ this.dataset_table.innerHTML = dl.join('');
9155
9270
  this.properties.style.display = 'block';
9156
9271
  document.getElementById('dataset-default').innerHTML =
9157
9272
  VM.sig4Dig(sd.default_value) +
@@ -9234,7 +9349,7 @@ class GUIDatasetManager extends DatasetManager {
9234
9349
  m.selector, '</td><td class="dataset-expression',
9235
9350
  clk, ');">', m.expression.text, '</td></tr>'].join(''));
9236
9351
  }
9237
- document.getElementById('dataset-modif-table').innerHTML = ml.join('');
9352
+ this.modifier_table.innerHTML = ml.join('');
9238
9353
  ttls.style.display = 'block';
9239
9354
  msa.style.display = 'block';
9240
9355
  mbtns.style.display = 'block';
@@ -9279,16 +9394,13 @@ class GUIDatasetManager extends DatasetManager {
9279
9394
 
9280
9395
  selectDataset(event, id) {
9281
9396
  // Select dataset, or edit it when Alt- or double-clicked
9397
+ this.focal_table = this.dataset_table;
9282
9398
  const
9283
9399
  d = MODEL.datasets[id] || null,
9284
- now = Date.now(),
9285
- dt = now - this.last_time_selected,
9286
- // Consider click to be "double" if it occurred less than 300 ms ago
9287
- edit = event.altKey || (d === this.selected_dataset && dt < 300);
9400
+ edit = event.altKey || this.doubleClicked(d);
9288
9401
  this.selected_dataset = d;
9289
- this.last_time_selected = now;
9290
9402
  if(d && edit) {
9291
- this.last_time_selected = 0;
9403
+ this.last_time_clicked = 0;
9292
9404
  this.editData();
9293
9405
  return;
9294
9406
  }
@@ -9298,19 +9410,13 @@ class GUIDatasetManager extends DatasetManager {
9298
9410
  selectModifier(event, id, x=true) {
9299
9411
  // Select modifier, or when double-clicked, edit its expression or the
9300
9412
  // name of the modifier
9413
+ this.focal_table = this.modifier_table;
9301
9414
  if(this.selected_dataset) {
9302
9415
  const m = this.selected_dataset.modifiers[UI.nameToID(id)],
9303
- now = Date.now(),
9304
- dt = now - this.last_time_selected,
9305
- // NOTE: Alt-click and double-click indicate: edit
9306
- // Consider click to be "double" if the same modifier was clicked
9307
- // less than 300 ms ago
9308
- edit = event.altKey || (m === this.selected_modifier && dt < 300);
9309
- this.last_time_selected = now;
9416
+ edit = event.altKey || this.doubleClicked(m);
9310
9417
  if(event.shiftKey) {
9311
9418
  // NOTE: prepare to update HTML class of selected dataset
9312
- const el = document.getElementById('dataset-table')
9313
- .getElementsByClassName('sel-set')[0];
9419
+ const el = this.dataset_table.getElementsByClassName('sel-set')[0];
9314
9420
  // Toggle dataset default selector
9315
9421
  if(m.selector === this.selected_dataset.default_selector) {
9316
9422
  this.selected_dataset.default_selector = '';
@@ -9322,7 +9428,7 @@ class GUIDatasetManager extends DatasetManager {
9322
9428
  }
9323
9429
  this.selected_modifier = m;
9324
9430
  if(edit) {
9325
- this.last_time_selected = 0;
9431
+ this.last_time_clicked = 0;
9326
9432
  if(x) {
9327
9433
  this.editExpression();
9328
9434
  } else {
@@ -9743,7 +9849,49 @@ class EquationManager {
9743
9849
  this.visible = false;
9744
9850
  this.selected_modifier = null;
9745
9851
  this.edited_expression = null;
9746
- this.last_time_selected = 0;
9852
+ this.last_time_clicked = 0;
9853
+ }
9854
+
9855
+ doubleClicked(obj) {
9856
+ const
9857
+ now = Date.now(),
9858
+ dt = now - this.last_time_clicked;
9859
+ this.last_time_clicked = now;
9860
+ if(obj === this.clicked_object) {
9861
+ // Consider click to be "double" if it occurred less than 300 ms ago
9862
+ if(dt < 300) {
9863
+ this.last_time_clicked = 0;
9864
+ return true;
9865
+ }
9866
+ }
9867
+ this.clicked_object = obj;
9868
+ return false;
9869
+ }
9870
+
9871
+ enterKey() {
9872
+ // Open the expression editor for the selected equation
9873
+ const srl = this.table.getElementsByClassName('sel-set');
9874
+ if(srl.length > 0) {
9875
+ const r = this.table.rows[srl[0].rowIndex];
9876
+ if(r) {
9877
+ // Emulate a double-click on the second cell to edit the expression
9878
+ this.last_time_clicked = Date.now();
9879
+ r.cells[1].dispatchEvent(new Event('click'));
9880
+ }
9881
+ }
9882
+ }
9883
+
9884
+ upDownKey(dir) {
9885
+ // Select row above or below the selected one (if possible)
9886
+ const srl = this.table.getElementsByClassName('sel-set');
9887
+ if(srl.length > 0) {
9888
+ const r = this.table.rows[srl[0].rowIndex + dir];
9889
+ if(r) {
9890
+ UI.scrollIntoView(r);
9891
+ // NOTE: not row but cell listens for onclick
9892
+ r.cells[1].dispatchEvent(new Event('click'));
9893
+ }
9894
+ }
9747
9895
  }
9748
9896
 
9749
9897
  updateDialog() {
@@ -9789,14 +9937,9 @@ class EquationManager {
9789
9937
  if(MODEL.equations_dataset) {
9790
9938
  const
9791
9939
  m = MODEL.equations_dataset.modifiers[UI.nameToID(id)] || null,
9792
- now = Date.now(),
9793
- dt = now - this.last_time_selected,
9794
- // Consider click to be "double" if it occurred less than 300 ms ago
9795
- edit = event.altKey || (m === this.selected_modifier && dt < 300);
9796
- this.last_time_selected = now;
9940
+ edit = event.altKey || this.doubleClicked(m);
9797
9941
  this.selected_modifier = m;
9798
9942
  if(m && edit) {
9799
- this.last_time_selected = 0;
9800
9943
  if(x) {
9801
9944
  this.editEquation();
9802
9945
  } else {
@@ -10088,6 +10231,31 @@ class GUIChartManager extends ChartManager {
10088
10231
  this.last_time_selected = 0;
10089
10232
  }
10090
10233
 
10234
+ enterKey() {
10235
+ // Open "edit" dialog for the selected chart variable
10236
+ const srl = this.variables_table.getElementsByClassName('sel-set');
10237
+ if(srl.length > 0) {
10238
+ const r = this.variables_table.rows[srl[0].rowIndex];
10239
+ if(r) {
10240
+ // Emulate a double-click to edit the variable properties
10241
+ this.last_time_selected = Date.now();
10242
+ r.dispatchEvent(new Event('click'));
10243
+ }
10244
+ }
10245
+ }
10246
+
10247
+ upDownKey(dir) {
10248
+ // Select row above or below the selected one (if possible)
10249
+ const srl = this.variables_table.getElementsByClassName('sel-set');
10250
+ if(srl.length > 0) {
10251
+ const r = this.variables_table.rows[srl[0].rowIndex + dir];
10252
+ if(r) {
10253
+ UI.scrollIntoView(r);
10254
+ r.dispatchEvent(new Event('click'));
10255
+ }
10256
+ }
10257
+ }
10258
+
10091
10259
  setRunsChart(show) {
10092
10260
  // Indicates whether the chart manager should display a run result chart
10093
10261
  this.runs_chart = show;
@@ -11552,7 +11720,10 @@ class GUIExperimentManager extends ExperimentManager {
11552
11720
  this.default_message = document.getElementById('experiment-default-message');
11553
11721
 
11554
11722
  this.design = document.getElementById('experiment-design');
11723
+ this.experiment_table = document.getElementById('experiment-table');
11555
11724
  this.params_div = document.getElementById('experiment-params-div');
11725
+ this.dimension_table = document.getElementById('experiment-dim-table');
11726
+ this.chart_table = document.getElementById('experiment-chart-table');
11556
11727
  // NOTE: the Exclude input field responds to several events
11557
11728
  this.exclude = document.getElementById('experiment-exclude');
11558
11729
  this.exclude.addEventListener(
@@ -11749,9 +11920,22 @@ class GUIExperimentManager extends ExperimentManager {
11749
11920
  this.edited_dimension_index = -1;
11750
11921
  this.edited_combi_selector_index = -1;
11751
11922
  this.color_scale = new ColorScale('no');
11923
+ this.focal_table = null;
11752
11924
  this.designMode();
11753
11925
  }
11754
11926
 
11927
+ upDownKey(dir) {
11928
+ // Select row above or below the selected one (if possible)
11929
+ const srl = this.focal_table.getElementsByClassName('sel-set');
11930
+ if(srl.length > 0) {
11931
+ const r = this.focal_table.rows[srl[0].rowIndex + dir];
11932
+ if(r) {
11933
+ UI.scrollIntoView(r);
11934
+ r.dispatchEvent(new Event('click'));
11935
+ }
11936
+ }
11937
+ }
11938
+
11755
11939
  updateDialog() {
11756
11940
  this.updateChartList();
11757
11941
  // Warn modeler if no meaningful experiments can be defined
@@ -11785,7 +11969,7 @@ class GUIExperimentManager extends ExperimentManager {
11785
11969
  '\');" onmouseover="EXPERIMENT_MANAGER.showInfo(', xi,
11786
11970
  ', event.shiftKey);"><td>', x.title, '</td></tr>'].join(''));
11787
11971
  }
11788
- document.getElementById('experiment-table').innerHTML = xl.join('');
11972
+ this.experiment_table.innerHTML = xl.join('');
11789
11973
  const
11790
11974
  btns = 'xp-rename xp-view xp-delete xp-ignore',
11791
11975
  icnt = document.getElementById('xp-ignore-count');
@@ -11849,7 +12033,7 @@ class GUIExperimentManager extends ExperimentManager {
11849
12033
  setString(x.dimensions[i]),
11850
12034
  '</td></tr>'].join(''));
11851
12035
  }
11852
- document.getElementById('experiment-dim-table').innerHTML = tr.join('');
12036
+ this.dimension_table.innerHTML = tr.join('');
11853
12037
  // Add button must be enabled only if there still are unused dimensions
11854
12038
  if(x.available_dimensions.length > 0) {
11855
12039
  document.getElementById('xp-d-add-btn').classList.remove('v-disab');
@@ -11865,7 +12049,7 @@ class GUIExperimentManager extends ExperimentManager {
11865
12049
  i, '\');"><td>',
11866
12050
  x.charts[i].title, '</td></tr>'].join(''));
11867
12051
  }
11868
- document.getElementById('experiment-chart-table').innerHTML = tr.join('');
12052
+ this.chart_table.innerHTML = tr.join('');
11869
12053
  if(x.charts.length === 0) canview = false;
11870
12054
  if(tr.length >= this.suitable_charts.length) {
11871
12055
  document.getElementById('xp-c-add-btn').classList.add('v-disab');
@@ -12525,6 +12709,8 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
12525
12709
 
12526
12710
  selectParameter(p) {
12527
12711
  this.selected_parameter = p;
12712
+ this.focal_table = (p.startsWith('d') ? this.dimension_table :
12713
+ this.chart_table);
12528
12714
  this.updateDialog();
12529
12715
  }
12530
12716
 
@@ -14142,7 +14328,10 @@ class Finder {
14142
14328
  this.copy_btn = document.getElementById('finder-copy-btn');
14143
14329
  this.copy_btn.addEventListener(
14144
14330
  'click', (event) => FINDER.copyAttributesToClipboard(event.shiftKey));
14145
-
14331
+ this.entity_table = document.getElementById('finder-table');
14332
+ this.item_table = document.getElementById('finder-item-table');
14333
+ this.expression_table = document.getElementById('finder-expression-table');
14334
+
14146
14335
  // Attribute headers are used by Finder to output entity attribute values
14147
14336
  this.attribute_headers = {
14148
14337
  A: 'ACTORS:\tWeight\tCash IN\tCash OUT\tCash FLOW',
@@ -14174,6 +14363,47 @@ class Finder {
14174
14363
  this.product_cluster_index = 0;
14175
14364
  }
14176
14365
 
14366
+ doubleClicked(obj) {
14367
+ const
14368
+ now = Date.now(),
14369
+ dt = now - this.last_time_clicked;
14370
+ this.last_time_clicked = now;
14371
+ if(obj === this.clicked_object) {
14372
+ // Consider click to be "double" if it occurred less than 300 ms ago
14373
+ if(dt < 300) {
14374
+ this.last_time_clicked = 0;
14375
+ return true;
14376
+ }
14377
+ }
14378
+ this.clicked_object = obj;
14379
+ return false;
14380
+ }
14381
+
14382
+ enterKey() {
14383
+ // Open "edit properties" dialog for the selected entity
14384
+ const srl = this.entity_table.getElementsByClassName('sel-set');
14385
+ if(srl.length > 0) {
14386
+ const r = this.entity_table.rows[srl[0].rowIndex];
14387
+ if(r) {
14388
+ const e = new Event('click');
14389
+ e.altKey = true;
14390
+ r.dispatchEvent(e);
14391
+ }
14392
+ }
14393
+ }
14394
+
14395
+ upDownKey(dir) {
14396
+ // Select row above or below the selected one (if possible)
14397
+ const srl = this.entity_table.getElementsByClassName('sel-set');
14398
+ if(srl.length > 0) {
14399
+ const r = this.entity_table.rows[srl[0].rowIndex + dir];
14400
+ if(r) {
14401
+ UI.scrollIntoView(r);
14402
+ r.dispatchEvent(new Event('click'));
14403
+ }
14404
+ }
14405
+ }
14406
+
14177
14407
  updateDialog() {
14178
14408
  const
14179
14409
  el = [],
@@ -14289,7 +14519,7 @@ class Finder {
14289
14519
  if(e === se) seid += i;
14290
14520
  el.push(['<tr id="etr', i, '" class="dataset',
14291
14521
  (e === se ? ' sel-set' : ''), '" onclick="FINDER.selectEntity(\'',
14292
- enl[i], '\');" onmouseover="FINDER.showInfo(\'', enl[i],
14522
+ enl[i], '\', event.altKey);" onmouseover="FINDER.showInfo(\'', enl[i],
14293
14523
  '\', event.shiftKey);"><td draggable="true" ',
14294
14524
  'ondragstart="FINDER.drag(event);"><img class="finder" src="images/',
14295
14525
  e.type.toLowerCase(), '.png">', e.displayName,
@@ -14297,7 +14527,7 @@ class Finder {
14297
14527
  }
14298
14528
  // NOTE: reset `selected_entity` if not in the new list
14299
14529
  if(seid === 'etr') this.selected_entity = null;
14300
- document.getElementById('finder-table').innerHTML = el.join('');
14530
+ this.entity_table.innerHTML = el.join('');
14301
14531
  UI.scrollIntoView(document.getElementById(seid));
14302
14532
  document.getElementById('finder-count').innerHTML = pluralS(
14303
14533
  el.length, 'entity', 'entities');
@@ -14452,7 +14682,7 @@ class Finder {
14452
14682
  e.type.toLowerCase(), '.png">', e.displayName,
14453
14683
  '</td></tr>'].join(''));
14454
14684
  }
14455
- document.getElementById('finder-item-table').innerHTML = el.join('');
14685
+ this.item_table.innerHTML = el.join('');
14456
14686
  // Clear the table row list
14457
14687
  el.length = 0;
14458
14688
  // Now fill it with entity+attribute having a matching expression
@@ -14480,7 +14710,7 @@ class Finder {
14480
14710
  '<img class="finder" src="images/', img, '.png">', td, '</td></tr>'
14481
14711
  ].join(''));
14482
14712
  }
14483
- document.getElementById('finder-expression-table').innerHTML = el.join('');
14713
+ this.expression_table.innerHTML = el.join('');
14484
14714
  document.getElementById('finder-expression-hdr').innerHTML =
14485
14715
  pluralS(el.length, 'expression');
14486
14716
  }
@@ -14515,10 +14745,37 @@ class Finder {
14515
14745
  if(e) DOCUMENTATION_MANAGER.update(e, shift);
14516
14746
  }
14517
14747
 
14518
- selectEntity(id) {
14519
- // Looks up entity, selects it in the left pane, and updates the right pane
14520
- this.selected_entity = MODEL.objectByID(id);
14748
+ selectEntity(id, alt=false) {
14749
+ // Looks up entity, selects it in the left pane, and updates the
14750
+ // right pane; opens the "edit properties" modal dialog on double-click
14751
+ // and Alt-click if the entity is editable
14752
+ const obj = MODEL.objectByID(id);
14753
+ this.selected_entity = obj;
14521
14754
  this.updateDialog();
14755
+ if(!obj) return;
14756
+ if(alt || this.doubleClicked(obj)) {
14757
+ if(obj instanceof Process) {
14758
+ UI.showProcessPropertiesDialog(obj);
14759
+ } else if(obj instanceof Product) {
14760
+ UI.showProductPropertiesDialog(obj);
14761
+ } else if(obj instanceof Link) {
14762
+ UI.showLinkPropertiesDialog(obj);
14763
+ } else if(obj instanceof Note) {
14764
+ obj.showNotePropertiesDialog();
14765
+ } else if(obj instanceof Dataset) {
14766
+ if(UI.hidden('dataset-dlg')) {
14767
+ UI.buttons.dataset.dispatchEvent(new Event('click'));
14768
+ }
14769
+ DATASET_MANAGER.selected_dataset = obj;
14770
+ DATASET_MANAGER.updateDialog();
14771
+ } else if(obj instanceof DatasetModifier) {
14772
+ if(UI.hidden('equation-dlg')) {
14773
+ UI.buttons.equation.dispatchEvent(new Event('click'));
14774
+ }
14775
+ EQUATION_MANAGER.selected_modifier = obj;
14776
+ EQUATION_MANAGER.updateDialog();
14777
+ }
14778
+ }
14522
14779
  }
14523
14780
 
14524
14781
  reveal(id) {
@@ -14569,22 +14826,12 @@ class Finder {
14569
14826
  // NOTE: return the object to save a second lookup by revealExpression
14570
14827
  return obj;
14571
14828
  }
14572
-
14829
+
14573
14830
  revealExpression(id, attr, shift=false, alt=false) {
14574
- const
14575
- obj = this.reveal(id),
14576
- now = Date.now(),
14577
- dt = now - this.last_time_clicked;
14578
- this.last_time_clicked = now;
14579
- if(obj === this.clicked_object) {
14580
- // Consider click to be "double" if it occurred less than 300 ms ago
14581
- if(dt < 300) {
14582
- this.last_time_clicked = 0;
14583
- shift = true;
14584
- }
14585
- }
14586
- this.clicked_object = obj;
14587
- if(obj && attr && (shift || alt)) {
14831
+ const obj = this.reveal(id);
14832
+ if(!obj) return;
14833
+ shift = shift || this.doubleClicked(obj);
14834
+ if(attr && (shift || alt)) {
14588
14835
  if(obj instanceof Process) {
14589
14836
  // NOTE: the second argument makes the dialog focus on the specified
14590
14837
  // attribute input field; the third makes it open the expression editor
@@ -689,8 +689,8 @@ class LinnyRModel {
689
689
  // Merge into dimension if there are shared selectors
690
690
  for(let i = 0; i < this.dimensions.length; i++) {
691
691
  const c = complement(sl, this.dimensions[i]);
692
- if(c.length > 0 && c.length < sl.length) {
693
- this.dimensions[i].push(...c);
692
+ if(c.length < sl.length) {
693
+ if(c.length > 0) this.dimensions[i].push(...c);
694
694
  newdim = false;
695
695
  break;
696
696
  }
@@ -1181,8 +1181,8 @@ class LinnyRModel {
1181
1181
  }
1182
1182
  const id = UI.nameToID(name);
1183
1183
  let d = this.namedObjectByID(id);
1184
- if(d) {
1185
- if(IO_CONTEXT && d !== this.equations_dataset) {
1184
+ if(d && d !== this.equations_dataset) {
1185
+ if(IO_CONTEXT) {
1186
1186
  IO_CONTEXT.supersede(d);
1187
1187
  } else {
1188
1188
  // Preserve name uniqueness
@@ -1205,8 +1205,6 @@ class LinnyRModel {
1205
1205
  if(eqds) {
1206
1206
  // Restore pointer to original equations dataset
1207
1207
  this.equations_dataset = eqds;
1208
- // Add included equations with prefixed names
1209
- console.log('HERE', d);
1210
1208
  // Return the extended equations dataset
1211
1209
  return eqds;
1212
1210
  } else {
@@ -4773,7 +4771,14 @@ class NodeBox extends ObjectWithXYWH {
4773
4771
  get numberContext() {
4774
4772
  // Returns the string to be used to evaluate #, so for clusters, processes
4775
4773
  // and products this is the string of trailing digits (or empty if none)
4776
- return endsWithDigits(this.name);
4774
+ // of the node name, or if that does not end with a number, the trailing
4775
+ // digits of the first prefix (from right to left) that does
4776
+ const sn = this.name.split(UI.PREFIXER);
4777
+ let nc = endsWithDigits(sn.pop());
4778
+ while(!nc && sn.length > 0) {
4779
+ nc = endsWithDigits(sn.pop());
4780
+ }
4781
+ return nc;
4777
4782
  }
4778
4783
 
4779
4784
  rename(name, actor_name) {
@@ -7593,8 +7598,8 @@ class Link {
7593
7598
  tn = this.from_node;
7594
7599
  }
7595
7600
  // Otherwise, the FROM node is checked first
7596
- let nc = endsWithDigits(fn.name);
7597
- if(!nc) nc = endsWithDigits(tn.name);
7601
+ let nc = fn.numberContext;
7602
+ if(!nc) nc = tn.numberContext;
7598
7603
  return nc;
7599
7604
  }
7600
7605
 
@@ -7847,7 +7852,12 @@ class Dataset {
7847
7852
 
7848
7853
  get numberContext() {
7849
7854
  // Returns the string to be used to evaluate # (empty string if undefined)
7850
- return endsWithDigits(this.name);
7855
+ const sn = this.name.split(UI.PREFIXER);
7856
+ let nc = endsWithDigits(sn.pop());
7857
+ while(!nc && sn.length > 0) {
7858
+ nc = endsWithDigits(sn.pop());
7859
+ }
7860
+ return nc;
7851
7861
  }
7852
7862
 
7853
7863
  get selectorList() {