linny-r 1.5.8 → 1.6.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.
@@ -309,35 +309,55 @@ class DocumentationManager {
309
309
  }
310
310
 
311
311
  update(e, shift) {
312
- // Display name of entity under cursor on the infoline, and details in
313
- // the documentation dialog
312
+ // Display name of entity under cursor on the infoline, and details
313
+ // in the documentation dialog.
314
314
  if(!e) return;
315
- const
316
- et = e.type,
315
+ let et = e.type,
317
316
  edn = e.displayName;
318
- // TO DO: when debugging, display additional data for nodes on the infoline
317
+ if(et === 'Equation' && e.selector.startsWith(':')) et = 'Method';
318
+ // TO DO: when debugging, display additional data for nodes on the
319
+ // infoline.
319
320
  UI.setMessage(
320
321
  e instanceof NodeBox ? e.infoLineName : `<em>${et}:</em> ${edn}`);
321
- // NOTE: update the dialog ONLY when shift is pressed (this permits modelers
322
- // to rapidly browse comments without having to click on entities, and then
323
- // release the shift key to move to the documentation dialog to edit)
324
- // Moreover, the documentation dialog must be visible, and the entity must
325
- // have the `comments` property
326
- if(!this.editing && shift && this.visible && e.hasOwnProperty('comments')) {
327
- this.title.innerHTML = `<em>${et}:</em>&nbsp;${edn}`;
328
- this.entity = e;
329
- this.markup = (e.comments ? e.comments : '');
330
- this.editor.value = this.markup;
331
- this.viewer.innerHTML = this.markdown;
332
- this.edit_btn.classList.remove('disab');
333
- this.edit_btn.classList.add('enab');
334
- // NOTE: permit documentation of the model by raising the dialog
335
- if(this.entity === MODEL) this.dialog.style.zIndex = 101;
322
+ // NOTE: Update the dialog ONLY when shift is pressed. This permits
323
+ // modelers to rapidly browse comments without having to click on
324
+ // entities, and then release the shift key to move to the documentation
325
+ // dialog to edit. Moreover, the documentation dialog must be visible,
326
+ // and the entity must have the `comments` property.
327
+ // NOTE: Equations constitute an exception, as DatasetModifiers do
328
+ // not have the `comments` property. Now that methods can be defined
329
+ // (since version 1.6.0), the documentation window displays the eligible
330
+ // prefixes when the cursor is Shift-moved over the name of a method
331
+ // (in the Equation Manager).
332
+ if(!this.editing && shift && this.visible) {
333
+ if(e.hasOwnProperty('comments')) {
334
+ this.title.innerHTML = `<em>${et}:</em>&nbsp;${edn}`;
335
+ this.entity = e;
336
+ this.markup = (e.comments ? e.comments : '');
337
+ this.editor.value = this.markup;
338
+ this.viewer.innerHTML = this.markdown;
339
+ this.edit_btn.classList.remove('disab');
340
+ this.edit_btn.classList.add('enab');
341
+ // NOTE: Permit documentation of the model by raising the dialog.
342
+ if(this.entity === MODEL) this.dialog.style.zIndex = 101;
343
+ } else if(e instanceof DatasetModifier) {
344
+ this.title.innerHTML = e.selector;
345
+ this.viewer.innerHTML = 'Method <tt>' + e.selector +
346
+ '</tt> does not apply to any entity';
347
+ if(e.expression.eligible_prefixes) {
348
+ const el = Object.keys(e.expression.eligible_prefixes)
349
+ .sort(compareSelectors);
350
+ if(el.length > 0) this.viewer.innerHTML = [
351
+ 'Method <tt>', e.selector, '</tt> applies to ',
352
+ pluralS(el.length, 'prefixed entity group'),
353
+ ':<ul><li>', el.join('</li><li>'), '</li></ul>'].join('');
354
+ }
355
+ }
336
356
  }
337
357
  }
338
358
 
339
359
  rewrite(str) {
340
- // Apply all the rewriting rules to `str`
360
+ // Apply all the rewriting rules to `str`.
341
361
  str = '\n' + str + '\n';
342
362
  this.rules.forEach(
343
363
  (rule) => { str = str.replace(rule.pattern, rule.rewrite); });
@@ -345,28 +365,29 @@ class DocumentationManager {
345
365
  }
346
366
 
347
367
  makeList(par, isp, type) {
348
- // Split on the *global multi-line* item separator pattern
368
+ // Split on the *global multi-line* item separator pattern.
349
369
  const splitter = new RegExp(isp, 'gm'),
350
370
  list = par.split(splitter);
351
371
  if(list.length < 2) return false;
352
- // Now we know that the paragraph contains at least one list item line
372
+ // Now we know that the paragraph contains at least one list item line.
353
373
  let start = 0;
354
- // Paragraph may start with plain text, so check using the original pattern
374
+ // Paragraph may start with plain text, so check using the original
375
+ // pattern.
355
376
  if(!par.match(isp)) {
356
377
  // If so, retain this first part as a separate paragraph...
357
378
  start = 1;
358
- // NOTE: add it only if it contains text
379
+ // NOTE: Add it only if it contains text.
359
380
  par = (list[0].trim() ? `<p>${this.rewrite(list[0])}</p>` : '');
360
- // ... and clear it as list item
381
+ // ... and clear it as list item.
361
382
  list[0] = '';
362
383
  } else {
363
384
  par = '';
364
385
  }
365
- // Rewrite each list item fragment that contains text
386
+ // Rewrite each list item fragment that contains text.
366
387
  for(let j = start; j < list.length; j++) {
367
388
  list[j] = (list[j].trim() ? `<li>${this.rewrite(list[j])}</li>` : '');
368
389
  }
369
- // Return assembled parts
390
+ // Return assembled parts.
370
391
  return [par, '<', type, 'l>', list.join(''), '</', type, 'l>'].join('');
371
392
  }
372
393
 
@@ -375,16 +396,16 @@ class DocumentationManager {
375
396
  const html = this.markup.split(/\n{2,}/);
376
397
  let list;
377
398
  for(let i = 0; i < html.length; i++) {
378
- // Paragraph with only dashes and spaces becomes a horizontal rule
399
+ // Paragraph with only dashes and spaces becomes a horizontal rule.
379
400
  if(html[i].match(/^( *-)+$/)) {
380
401
  html[i] = '<hr>';
381
- // Paragraph may contain a bulleted list
402
+ // Paragraph may contain a bulleted list.
382
403
  } else if ((list = this.makeList(html[i], /^ *- +/, 'u')) !== false) {
383
404
  html[i] = list;
384
- // Paragraph may contain a numbered list
405
+ // Paragraph may contain a numbered list.
385
406
  } else if ((list = this.makeList(html[i], /^ *\d+. +/, 'o')) !== false) {
386
407
  html[i] = list;
387
- // Otherwise: default HTML paragraph
408
+ // Otherwise: default HTML paragraph.
388
409
  } else {
389
410
  html[i] = `<p>${this.rewrite(html[i])}</p>`;
390
411
  }
@@ -411,7 +432,7 @@ class DocumentationManager {
411
432
  }
412
433
 
413
434
  insertSymbol(sym) {
414
- // Insert symbol (clicked item in list below text area) into text area
435
+ // Insert symbol (clicked item in list below text area) into text area.
415
436
  this.editor.focus();
416
437
  let p = this.editor.selectionStart;
417
438
  const
@@ -434,7 +455,7 @@ class DocumentationManager {
434
455
  } else if(this.entity instanceof Constraint) {
435
456
  UI.paper.drawConstraint(this.entity);
436
457
  } else if (typeof this.entity.draw === 'function') {
437
- // Only draw if the entity responds to that method
458
+ // Only draw if the entity responds to that method.
438
459
  this.entity.draw();
439
460
  }
440
461
  }
@@ -481,14 +502,14 @@ class DocumentationManager {
481
502
  }
482
503
 
483
504
  addMessage(msg) {
484
- // Append message to the info messages list
505
+ // Append message to the info messages list.
485
506
  if(msg) this.info_messages.push(msg);
486
- // Update dialog only when it is showing
507
+ // Update dialog only when it is showing.
487
508
  if(!UI.hidden(this.dialog.id)) this.showInfoMessages(true);
488
509
  }
489
510
 
490
511
  showInfoMessages(shift) {
491
- // Show all messages that have appeared on the status line
512
+ // Show all messages that have appeared on the status line.
492
513
  const
493
514
  n = this.info_messages.length,
494
515
  title = pluralS(n, 'message') + ' since the current model was loaded';
@@ -504,21 +525,21 @@ class DocumentationManager {
504
525
  '<div class="', m.status, first, '-msg">', m.text, '</div></div>');
505
526
  }
506
527
  this.viewer.innerHTML = divs.join('');
507
- // Set the dialog title
528
+ // Set the dialog title.
508
529
  this.title.innerHTML = title;
509
530
  }
510
531
  }
511
532
 
512
533
  showArrowLinks(arrow) {
513
- // Show list of links represented by a composite arrow
534
+ // Show list of links represented by a composite arrow.
514
535
  const
515
536
  n = arrow.links.length,
516
537
  msg = 'Arrow represents ' + pluralS(n, 'link');
517
538
  UI.setMessage(msg);
518
539
  if(this.visible && !this.editing) {
519
- // Set the dialog title
540
+ // Set the dialog title.
520
541
  this.title.innerHTML = msg;
521
- // Show list
542
+ // Show list.
522
543
  const lis = [];
523
544
  let l, dn, c, af;
524
545
  for(let i = 0; i < n; i++) {
@@ -550,7 +571,8 @@ class DocumentationManager {
550
571
  }
551
572
 
552
573
  showHiddenIO(node, arrow) {
553
- // Show list of products or processes linked to node by an invisible arrow
574
+ // Show list of products or processes linked to node by an invisible
575
+ // arrow (i.e., links represented by a block arrow).
554
576
  let msg, iol;
555
577
  if(arrow === UI.BLOCK_IN) {
556
578
  iol = node.hidden_inputs;
@@ -566,9 +588,9 @@ class DocumentationManager {
566
588
  UI.on_block_arrow = true;
567
589
  UI.setMessage(msg);
568
590
  if(this.visible && !this.editing) {
569
- // Set the dialog title
591
+ // Set the dialog title.
570
592
  this.title.innerHTML = msg;
571
- // Show list
593
+ // Show list.
572
594
  const lis = [];
573
595
  for(let i = 0; i < iol.length; i++) {
574
596
  lis.push(`<li>${iol[i].displayName}</li>`);
@@ -579,17 +601,19 @@ class DocumentationManager {
579
601
  }
580
602
 
581
603
  showAllDocumentation() {
604
+ // Show (as HTML) all model entities (categorized by type) with their
605
+ // associated comments (if added by the modeler).
582
606
  const
583
607
  html = [],
584
608
  sl = MODEL.listOfAllComments;
585
609
  for(let i = 0; i < sl.length; i++) {
586
610
  if(sl[i].startsWith('_____')) {
587
- // 5-underscore leader indicates: start of new category
611
+ // 5-underscore leader indicates: start of new category.
588
612
  html.push('<h2>', sl[i].substring(5), '</h2>');
589
613
  } else {
590
614
  // Expect model element name...
591
615
  html.push('<p><tt>', sl[i], '</tt><br><small>');
592
- // ... immediately followed by its associated marked-up comments
616
+ // ... immediately followed by its associated marked-up comments.
593
617
  i++;
594
618
  this.markup = sl[i];
595
619
  html.push(this.markdown, '</small></p>');
@@ -597,7 +621,7 @@ class DocumentationManager {
597
621
  }
598
622
  this.title.innerHTML = 'Complete model documentation';
599
623
  this.viewer.innerHTML = html.join('');
600
- // Deselect entity and disable editing
624
+ // Deselect entity and disable editing.
601
625
  this.entity = null;
602
626
  this.edit_btn.classList.remove('enab');
603
627
  this.edit_btn.classList.add('disab');
@@ -139,16 +139,27 @@ class EquationManager {
139
139
  const
140
140
  m = ed.modifiers[UI.nameToID(msl[i])],
141
141
  wild = (m.selector.indexOf('??') >= 0),
142
+ method = m.selector.startsWith(':'),
143
+ issue = (m.expression.compile_issue ? ' compile-issue' :
144
+ (m.expression.compute_issue ? ' compute-issue' : '')),
142
145
  clk = '" onclick="EQUATION_MANAGER.selectModifier(event, \'' +
143
- m.selector + '\'';
146
+ m.selector + '\'',
147
+ mover = (method ? ' onmouseover="EQUATION_MANAGER.showInfo(\'' +
148
+ m.identifier + '\', event.shiftKey);"' : '');
144
149
  if(m === sm) smid += i;
145
150
  ml.push(['<tr id="eqmtr', i, '" class="dataset-modif',
146
151
  (m === sm ? ' sel-set' : ''),
147
152
  '"><td class="equation-selector',
148
- (m.expression.isStatic ? '' : ' it'),
149
- (wild ? ' wildcard' : ''), clk, ', false);">',
153
+ (method ? ' method' : ''),
154
+ // Display in gray when method cannot be applied.
155
+ (m.expression.noMethodObject ? ' no-object' : ''),
156
+ (m.expression.isStatic ? '' : ' it'), issue,
157
+ (wild ? ' wildcard' : ''), clk, ', false);"', mover, '>',
150
158
  (wild ? wildcardFormat(m.selector) : m.selector),
151
- '</td><td class="equation-expression',
159
+ '</td><td class="equation-expression', issue,
160
+ (issue ? '"title="' +
161
+ safeDoubleQuotes(m.expression.compile_issue ||
162
+ m.expression.compute_issue) : ''),
152
163
  clk, ');">', m.expression.text, '</td></tr>'].join(''));
153
164
  }
154
165
  this.table.innerHTML = ml.join('');
@@ -164,6 +175,8 @@ class EquationManager {
164
175
 
165
176
  showInfo(id, shift) {
166
177
  // @@TO DO: Display documentation for the equation => extra comments field?
178
+ const d = MODEL.equations_dataset.modifiers[id];
179
+ if(d) DOCUMENTATION_MANAGER.update(d, shift);
167
180
  }
168
181
 
169
182
  selectModifier(event, id, x=true) {
@@ -255,20 +268,23 @@ class EquationManager {
255
268
  if(!this.selected_modifier) return;
256
269
  const
257
270
  sel = this.rename_modal.element('name').value,
258
- // Keep track of old name
271
+ // Keep track of old name.
259
272
  oldm = this.selected_modifier,
260
273
  olds = oldm.selector,
261
- // NOTE: addModifier returns existing one if selector not changed
274
+ // NOTE: addModifier returns existing one if selector not changed.
262
275
  m = MODEL.equations_dataset.addModifier(sel);
263
- // NULL indicates invalid name
276
+ // NULL indicates invalid name.
264
277
  if(!m) return;
265
- // If only case has changed, update the selector
266
- // NOTE: equation names may contain spaces; if so, reduce to single space
278
+ // If only case has changed, update the selector.
279
+ // NOTE: Equation names may contain spaces; if so, reduce to single space.
267
280
  if(m === oldm) {
268
281
  m.selector = sel.trim().replace(/\s+/g, ' ');
269
282
  } else {
270
- // When a new modifier has been added, more actions are needed
283
+ // When a new modifier has been added, more actions are needed.
271
284
  m.expression = oldm.expression;
285
+ // NOTE: The `attribute` property of the expression must be updated
286
+ // because it identifies the "owner" of the expression.
287
+ m.expression.attribute = m.selector;
272
288
  this.deleteEquation();
273
289
  this.selected_modifier = m;
274
290
  }
@@ -114,11 +114,14 @@ class GUIExperimentManager extends ExperimentManager {
114
114
  document.getElementById('xv-download-btn').addEventListener(
115
115
  'click', () => EXPERIMENT_MANAGER.promptForDownload());
116
116
  // The viewer's drop-down selectors
117
- document.getElementById('viewer-variable').addEventListener(
117
+ this.viewer_variable = document.getElementById('viewer-variable');
118
+ this.viewer_variable.addEventListener(
118
119
  'change', () => EXPERIMENT_MANAGER.setVariable());
119
- document.getElementById('viewer-statistic').addEventListener(
120
+ this.viewer_statistic = document.getElementById('viewer-statistic');
121
+ this.viewer_statistic.addEventListener(
120
122
  'change', () => EXPERIMENT_MANAGER.setStatistic());
121
- document.getElementById('viewer-scale').addEventListener(
123
+ this.viewer_scale = document.getElementById('viewer-scale');
124
+ this.viewer_scale.addEventListener(
122
125
  'change', () => EXPERIMENT_MANAGER.setScale());
123
126
  // The spin buttons
124
127
  document.getElementById('xp-cd-minus').addEventListener(
@@ -272,12 +275,12 @@ class GUIExperimentManager extends ExperimentManager {
272
275
 
273
276
  updateDialog() {
274
277
  this.updateChartList();
275
- // Warn modeler if no meaningful experiments can be defined
278
+ // Warn modeler if no meaningful experiments can be defined.
276
279
  if(MODEL.outcomeNames.length === 0 && this.suitable_charts.length === 0) {
277
280
  this.default_message.style.display = 'block';
278
281
  this.params_div.style.display = 'none';
279
282
  this.selected_experiment = null;
280
- // Disable experiment dialog menu buttons
283
+ // Disable experiment dialog menu buttons.
281
284
  UI.disableButtons('xp-new xp-rename xp-view xp-delete xp-ignore');
282
285
  } else {
283
286
  this.default_message.style.display = 'none';
@@ -388,7 +391,7 @@ class GUIExperimentManager extends ExperimentManager {
388
391
  x.charts[i].title, '</td></tr>'].join(''));
389
392
  }
390
393
  this.chart_table.innerHTML = tr.join('');
391
- // Do not show viewer unless at least 1 dependent variable has been defined
394
+ // Do not show viewer unless at least 1 dependent variable has been defined.
392
395
  if(x.charts.length === 0 && MODEL.outcomeNames.length === 0) canview = false;
393
396
  if(tr.length >= this.suitable_charts.length) {
394
397
  document.getElementById('xp-c-add-btn').classList.add('v-disab');
@@ -409,7 +412,7 @@ class GUIExperimentManager extends ExperimentManager {
409
412
  dbtn.classList.add('v-disab');
410
413
  cbtn.classList.add('v-disab');
411
414
  }
412
- // Enable viewing only if > 1 dimensions and > 1 outcome variables
415
+ // Enable viewing only if > 1 dimensions and > 1 outcome variables.
413
416
  if(canview) {
414
417
  UI.enableButtons('xp-view');
415
418
  } else {
@@ -478,38 +481,51 @@ class GUIExperimentManager extends ExperimentManager {
478
481
  if(x) {
479
482
  this.design.style.display = 'none';
480
483
  document.getElementById('viewer-title').innerHTML = x.title;
481
- document.getElementById('viewer-statistic').value = x.selected_statistic;
484
+ this.viewer_statistic.value = x.selected_statistic;
482
485
  this.updateViewerVariable();
483
486
  // NOTE: calling updateSpinner with dir=0 will update without changes
484
487
  this.updateSpinner('c', 0);
485
488
  this.drawTable();
486
- document.getElementById('viewer-scale').value = x.selected_scale;
489
+ this.viewer_scale.value = x.selected_scale;
487
490
  this.setColorScale(x.selected_color_scale);
488
491
  this.viewer.style.display = 'block';
489
492
  }
490
493
  }
491
494
 
492
495
  updateViewerVariable() {
493
- // Update the variable drop-down selector of the viewer
496
+ // Update the variable drop-down selector of the viewer.
494
497
  const x = this.selected_experiment;
495
498
  if(x) {
496
499
  x.inferVariables();
497
500
  const
498
501
  ol = [],
499
- vl = MODEL.outcomeNames;
502
+ ov = MODEL.outcomeNames,
503
+ vl = [...ov];
500
504
  for(let i = 0; i < x.variables.length; i++) {
501
- addDistinct(x.variables[i].displayName, vl);
505
+ const
506
+ vn = x.variables[i].displayName,
507
+ oi = ov.indexOf(vn);
508
+ // If an outcome dataset or equation is plotted in an experiment
509
+ // chart, remove its name from the outcome variable list.
510
+ if(oi >= 0) ov.splice(oi, 1);
511
+ addDistinct(vn, vl);
502
512
  }
503
513
  vl.sort((a, b) => UI.compareFullNames(a, b));
504
514
  for(let i = 0; i < vl.length; i++) {
505
- ol.push(['<option value="', vl[i], '"',
506
- (vl[i] == x.selected_variable ? ' selected="selected"' : ''),
507
- '>', vl[i], '</option>'].join(''));
508
- }
509
- document.getElementById('viewer-variable').innerHTML = ol.join('');
510
- if(x.selected_variable === '') {
511
- x.selected_variable = vl[0];
515
+ const vn = vl[i];
516
+ // NOTE: FireFox selector dropdown areas have a pale gray
517
+ // background that darkens when color is set, so always set it
518
+ // to white (like Chrome). Then set color of outcome variables
519
+ // to fuchsia to differentiate from variables for which time
520
+ // series are stored as experiment run results.
521
+ ol.push(['<option value="', vn, '" style="background-color: white',
522
+ (ov.indexOf(vn) >= 0 ? '; color: #b00080"' : '"'),
523
+ (vn == x.selected_variable ? ' selected="selected"' : ''),
524
+ '>', vn, '</option>'].join(''));
512
525
  }
526
+ this.viewer_variable.innerHTML = ol.join('');
527
+ // Initially, select the first variable on the list.
528
+ if(x.selected_variable === '') x.selected_variable = vl[0];
513
529
  }
514
530
  }
515
531
 
@@ -684,8 +700,10 @@ class GUIExperimentManager extends ExperimentManager {
684
700
  for(let j = 0; j < n; j++) {
685
701
  const
686
702
  c = r + j + i * nrows,
703
+ run = x.runs[c],
687
704
  ic = x.chart_combinations.indexOf(c);
688
- if(add) {
705
+ // NOTE: Only add if run has been executed and stored.
706
+ if(add && run) {
689
707
  if(ic < 0) x.chart_combinations.push(c);
690
708
  } else {
691
709
  if(ic >= 0) x.chart_combinations.splice(ic, 1);
@@ -952,7 +970,7 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
952
970
  // Update view for selected variable
953
971
  const x = this.selected_experiment;
954
972
  if(x) {
955
- x.selected_variable = document.getElementById('viewer-variable').value;
973
+ x.selected_variable = this.viewer_variable.value;
956
974
  this.updateData();
957
975
  }
958
976
  }
@@ -961,7 +979,7 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
961
979
  // Update view for selected variable.
962
980
  const x = this.selected_experiment;
963
981
  if(x) {
964
- x.selected_statistic = document.getElementById('viewer-statistic').value;
982
+ x.selected_statistic = this.viewer_statistic.value;
965
983
  this.updateData();
966
984
  // NOTE: Update of Chart Manager is needed only when it is showing
967
985
  // run statistics.
@@ -1027,7 +1045,7 @@ N = ${rr.N}, vector length = ${rr.vector.length}` : '')].join('');
1027
1045
  // Update view for selected scale
1028
1046
  const x = this.selected_experiment;
1029
1047
  if(x) {
1030
- x.selected_scale = document.getElementById('viewer-scale').value;
1048
+ x.selected_scale = this.viewer_scale.value;
1031
1049
  this.updateData();
1032
1050
  // NOTE: Update of Chart Manager is needed when it is showing
1033
1051
  // run statistics because solver times may be plotted.
@@ -162,24 +162,24 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
162
162
  Monadic operators take precedence over dyadic operators.
163
163
  Use parentheses to override the default evaluation precedence.
164
164
  </p>`;
165
- // Add listeners to the GUI elements
165
+ // Add listeners to the GUI elements.
166
166
  const md = UI.modals.expression;
167
167
  md.ok.addEventListener('click', () => X_EDIT.parseExpression());
168
168
  md.cancel.addEventListener('click', () => X_EDIT.cancel());
169
- // NOTE: this modal also has an information button in its header
169
+ // NOTE: This modal also has an information button in its header.
170
170
  md.info.addEventListener(
171
171
  'click', () => X_EDIT.toggleExpressionInfo());
172
- document.getElementById('variable-obj').addEventListener(
172
+ this.obj.addEventListener(
173
173
  'change', () => X_EDIT.updateVariableBar());
174
- document.getElementById('variable-name').addEventListener(
174
+ this.name.addEventListener(
175
175
  'change', () => X_EDIT.updateAttributeSelector());
176
176
  document.getElementById('variable-insert').addEventListener(
177
177
  'click', () => X_EDIT.insertVariable());
178
178
  }
179
179
 
180
180
  editExpression(event) {
181
- // Infers which entity property expression is to edited from the button
182
- // that was clicked, and then opens the dialog
181
+ // Infer which entity property expression is to edited from the button
182
+ // that was clicked, and then opens the dialog.
183
183
  const
184
184
  btn = event.target,
185
185
  ids = btn.id.split('-'), // 3-tuple [entity type, attribute, 'x']
@@ -196,7 +196,8 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
196
196
  let n = '',
197
197
  a = '';
198
198
  if(ids[0] === 'link') {
199
- n = document.getElementById('link-from-name').innerHTML + UI.LINK_ARROW +
199
+ n = document.getElementById('link-from-name').innerHTML +
200
+ UI.LINK_ARROW +
200
201
  document.getElementById('link-to-name').innerHTML;
201
202
  } else {
202
203
  n = document.getElementById(ids[0] + '-name').value;
@@ -211,20 +212,19 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
211
212
  this.edited_expression = UI.edited_object.attributeExpression(ids[1]);
212
213
  }
213
214
  const md = UI.modals.expression;
214
- md.element('property').innerHTML = prop;
215
- md.element('text').value = document.getElementById(
216
- this.edited_input_id).value.trim();
217
- document.getElementById('variable-obj').value = 0;
215
+ this.property.innerHTML = prop;
216
+ this.text.value = document.getElementById(this.edited_input_id).value.trim();
217
+ this.obj.value = 0;
218
218
  this.updateVariableBar();
219
219
  this.clearStatusBar();
220
220
  md.show('text');
221
221
  }
222
222
 
223
223
  cancel() {
224
- // Closes the expression editor dialog
224
+ // Close the expression editor dialog.
225
225
  UI.modals.expression.hide();
226
226
  // Clear the "shortcut flag" that may be set by Shift-clicking the
227
- // "add chart variable" button in the chart dialog
227
+ // "add chart variable" button in the chart dialog.
228
228
  EQUATION_MANAGER.add_to_chart = false;
229
229
  // CLear other properties that relate to the edited expression.
230
230
  this.edited_input_id = '';
@@ -232,20 +232,25 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
232
232
  }
233
233
 
234
234
  parseExpression() {
235
- // Parses the contents of the expression editor
236
- let xt = this.text.value;
237
- // NOTE: the Insert button is quite close to the OK button, and often
238
- // the modeler clicks OK before Insert, leaving the expression empty;
239
- // hence assume that modeler meant to insert a variable if text is empty,
240
- // but all three variable components have been selected
235
+ // Parse the contents of the expression editor.
236
+ let xt = this.text.value.trim();
237
+ // NOTE: The Insert button is quite close to the OK button, and often
238
+ // the modeler clicks OK before Insert, leaving the expression empty.
239
+ // Hence assume that modeler meant to insert a variable if text is
240
+ // empty but all three variable components have been selected.
241
241
  if(xt === '') {
242
242
  const
243
243
  n = this.name.options[this.name.selectedIndex].innerHTML,
244
244
  a = this.attr.options[this.attr.selectedIndex].innerHTML;
245
245
  if(n && a) xt = `[${n}${UI.OA_SEPARATOR}${a}]`;
246
246
  }
247
+ // Remove all non-functional whitespace from variable references.
248
+ xt = monoSpacedVariables(xt);
249
+ // Update the text shown in the editor, otherwise the position of
250
+ // errors in the text may be incorrect.
251
+ this.text.value = xt;
247
252
  // NOTE: If the expression is a dataset modifier or an equation, pass
248
- // the dataset and the selector as extra parameters for the parser
253
+ // the dataset and the selector as extra parameters for the parser.
249
254
  let own = null,
250
255
  sel = '';
251
256
  if(!this.edited_input_id && DATASET_MANAGER.edited_expression) {
@@ -270,7 +275,7 @@ NOTE: Grouping groups results in a single group, e.g., (1;2);(3;4;5) evaluates a
270
275
  } else {
271
276
  if(this.edited_input_id) {
272
277
  document.getElementById(this.edited_input_id).value = xp.expr;
273
- // NOTE: entity properties must be exogenous parameters
278
+ // NOTE: Entity properties must be exogenous parameters.
274
279
  const eo = UI.edited_object;
275
280
  if(eo && xp.is_level_based &&
276
281
  !(eo instanceof Dataset || eo instanceof Note)) {
@@ -441,6 +441,7 @@ class Finder {
441
441
  }
442
442
  }
443
443
  document.getElementById('finder-item-header').innerHTML = hdr;
444
+ occ.sort(compareSelectors);
444
445
  for(let i = 0; i < occ.length; i++) {
445
446
  const e = MODEL.objectByID(occ[i]);
446
447
  el.push(['<tr id="eotr', i, '" class="dataset" onclick="FINDER.reveal(\'',
@@ -175,7 +175,7 @@ class GUIMonitor {
175
175
  this.block_count = VM.block_count;
176
176
  // Shows the appropriate text in the monitor's textarea
177
177
  let b = this.shown_block;
178
- // By default, show information on the block being calculated
178
+ // By default, show information on the block being calculated.
179
179
  if(b === 0) b = this.block_count;
180
180
  if(this.block_count === 0) {
181
181
  this.messages_text.value = VM.no_messages;
@@ -184,9 +184,9 @@ class GUIMonitor {
184
184
  this.messages_text.value = VM.messages[b - 1];
185
185
  this.equations_text.value = VM.equations[b - 1];
186
186
  }
187
- // Legend to variables is not block-dependent
188
- this.variables_text.value = VM.variablesLegend(b);
189
- // Show the text area for the selected tab
187
+ // Legend to variables is not block-dependent.
188
+ this.variables_text.value = VM.variablesLegend();
189
+ // Show the text area for the selected tab.
190
190
  if(this.tab !== tab) {
191
191
  let mt = 'monitor-' + this.tab;
192
192
  document.getElementById(mt).style.display = 'none';