wunderbaum 0.0.1-0 → 0.0.3

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.
@@ -1,8 +1,9 @@
1
1
  /*!
2
2
  * Wunderbaum - util
3
3
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
4
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
4
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
5
5
  */
6
+ /** @module util */
6
7
  /** Readable names for `MouseEvent.button` */
7
8
  const MOUSE_BUTTONS = {
8
9
  0: "",
@@ -14,7 +15,7 @@ const MOUSE_BUTTONS = {
14
15
  };
15
16
  const MAX_INT = 9007199254740991;
16
17
  const userInfo = _getUserInfo();
17
- /**True if the user is using a macOS platform. */
18
+ /**True if the client is using a macOS platform. */
18
19
  const isMac = userInfo.isMac;
19
20
  const REX_HTML = /[&<>"'/]/g; // Escape those characters
20
21
  const REX_TOOLTIP = /[<>"'/]/g; // Don't escape `&` in tooltips
@@ -162,7 +163,7 @@ function escapeTooltip(s) {
162
163
  /** TODO */
163
164
  function extractHtmlText(s) {
164
165
  if (s.indexOf(">") >= 0) {
165
- error("not implemented");
166
+ error("Not implemented");
166
167
  // return $("<div/>").html(s).text();
167
168
  }
168
169
  return s;
@@ -284,7 +285,7 @@ function setValueToElem(elem, value) {
284
285
  input.valueAsNumber = value;
285
286
  break;
286
287
  case "radio":
287
- assert(false, "not implemented");
288
+ error("Not implemented");
288
289
  // const name = input.name;
289
290
  // const checked = input.parentElement!.querySelector(
290
291
  // `input[name="${name}"]:checked`
@@ -307,7 +308,7 @@ function setValueToElem(elem, value) {
307
308
  }
308
309
  // return value;
309
310
  }
310
- /** Return an unconnected `HTMLElement` from a HTML string. */
311
+ /** Create and return an unconnected `HTMLElement` from a HTML string. */
311
312
  function elemFromHtml(html) {
312
313
  const t = document.createElement("template");
313
314
  t.innerHTML = html.trim();
@@ -324,11 +325,23 @@ function elemFromSelector(obj) {
324
325
  }
325
326
  return obj;
326
327
  }
328
+ /** Return a EventTarget from selector or cast an existing element. */
329
+ function eventTargetFromSelector(obj) {
330
+ if (!obj) {
331
+ return null;
332
+ }
333
+ if (typeof obj === "string") {
334
+ return document.querySelector(obj);
335
+ }
336
+ return obj;
337
+ }
327
338
  /**
328
- * Return a descriptive string for a keyboard or mouse event.
339
+ * Return a canonical descriptive string for a keyboard or mouse event.
329
340
  *
330
341
  * The result also contains a prefix for modifiers if any, for example
331
342
  * `"x"`, `"F2"`, `"Control+Home"`, or `"Shift+clickright"`.
343
+ * This is especially useful in `switch` statements, to make sure that modifier
344
+ * keys are considered and handled correctly.
332
345
  */
333
346
  function eventToString(event) {
334
347
  let key = event.key, et = event.type, s = [];
@@ -360,6 +373,13 @@ function eventToString(event) {
360
373
  }
361
374
  return s.join("+");
362
375
  }
376
+ /**
377
+ * Copy allproperties from one or more source objects to a target object.
378
+ *
379
+ * @returns the modified target object.
380
+ */
381
+ // TODO: use Object.assign()? --> https://stackoverflow.com/a/42740894
382
+ // TODO: support deep merge --> https://stackoverflow.com/a/42740894
363
383
  function extend(...args) {
364
384
  for (let i = 1; i < args.length; i++) {
365
385
  let arg = args[i];
@@ -374,23 +394,27 @@ function extend(...args) {
374
394
  }
375
395
  return args[0];
376
396
  }
397
+ /** Return true if `obj` is of type `array`. */
377
398
  function isArray(obj) {
378
399
  return Array.isArray(obj);
379
400
  }
401
+ /** Return true if `obj` is of type `Object` and has no propertied. */
380
402
  function isEmptyObject(obj) {
381
403
  return Object.keys(obj).length === 0 && obj.constructor === Object;
382
404
  }
405
+ /** Return true if `obj` is of type `function`. */
383
406
  function isFunction(obj) {
384
407
  return typeof obj === "function";
385
408
  }
409
+ /** Return true if `obj` is of type `Object`. */
386
410
  function isPlainObject(obj) {
387
411
  return Object.prototype.toString.call(obj) === "[object Object]";
388
412
  }
389
413
  /** A dummy function that does nothing ('no operation'). */
390
414
  function noop(...args) { }
391
- function onEvent(rootElem, eventNames, selectorOrHandler, handlerOrNone) {
415
+ function onEvent(rootTarget, eventNames, selectorOrHandler, handlerOrNone) {
392
416
  let selector, handler;
393
- rootElem = elemFromSelector(rootElem);
417
+ rootTarget = eventTargetFromSelector(rootTarget);
394
418
  if (handlerOrNone) {
395
419
  selector = selectorOrHandler;
396
420
  handler = handlerOrNone;
@@ -400,7 +424,7 @@ function onEvent(rootElem, eventNames, selectorOrHandler, handlerOrNone) {
400
424
  handler = selectorOrHandler;
401
425
  }
402
426
  eventNames.split(" ").forEach((evn) => {
403
- rootElem.addEventListener(evn, function (e) {
427
+ rootTarget.addEventListener(evn, function (e) {
404
428
  if (!selector) {
405
429
  return handler(e); // no event delegation
406
430
  }
@@ -478,6 +502,13 @@ async function sleep(ms) {
478
502
  }
479
503
  /**
480
504
  * Set or rotate checkbox status with support for tri-state.
505
+ *
506
+ * An initial 'indeterminate' state becomes 'checked' on the first call.
507
+ *
508
+ * If the input element has the class 'wb-tristate' assigned, the sequence is:<br>
509
+ * 'indeterminate' -> 'checked' -> 'unchecked' -> 'indeterminate' -> ...<br>
510
+ * Otherwise we toggle like <br>
511
+ * 'checked' -> 'unchecked' -> 'checked' -> ...
481
512
  */
482
513
  function toggleCheckbox(element, value, tristate) {
483
514
  const input = elemFromSelector(element);
@@ -535,6 +566,7 @@ function toSet(val) {
535
566
  }
536
567
  throw new Error("Cannot convert to Set<string>: " + val);
537
568
  }
569
+ /**Return a canonical string representation for an object's type (e.g. 'array', 'number', ...) */
538
570
  function type(obj) {
539
571
  return Object.prototype.toString
540
572
  .call(obj)
@@ -561,6 +593,7 @@ var util = /*#__PURE__*/Object.freeze({
561
593
  setValueToElem: setValueToElem,
562
594
  elemFromHtml: elemFromHtml,
563
595
  elemFromSelector: elemFromSelector,
596
+ eventTargetFromSelector: eventTargetFromSelector,
564
597
  eventToString: eventToString,
565
598
  extend: extend,
566
599
  isArray: isArray,
@@ -581,7 +614,7 @@ var util = /*#__PURE__*/Object.freeze({
581
614
  /*!
582
615
  * Wunderbaum - common
583
616
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
584
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
617
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
585
618
  */
586
619
  const DEFAULT_DEBUGLEVEL = 4; // Replaced by rollup script
587
620
  const ROW_HEIGHT = 22;
@@ -595,6 +628,8 @@ var ChangeType;
595
628
  ChangeType["row"] = "row";
596
629
  ChangeType["structure"] = "structure";
597
630
  ChangeType["status"] = "status";
631
+ ChangeType["vscroll"] = "vscroll";
632
+ ChangeType["header"] = "header";
598
633
  })(ChangeType || (ChangeType = {}));
599
634
  var NodeStatusType;
600
635
  (function (NodeStatusType) {
@@ -667,6 +702,8 @@ const KEY_TO_ACTION_DICT = {
667
702
  Home: "firstCol",
668
703
  "Control+End": "last",
669
704
  "Control+Home": "first",
705
+ "Meta+ArrowDown": "last",
706
+ "Meta+ArrowUp": "first",
670
707
  "*": "expandAll",
671
708
  Multiply: "expandAll",
672
709
  PageDown: "pageDown",
@@ -693,7 +730,7 @@ function makeNodeTitleStartMatcher(s) {
693
730
  /*!
694
731
  * Wunderbaum - wb_extension_base
695
732
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
696
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
733
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
697
734
  */
698
735
  class WunderbaumExtension {
699
736
  constructor(tree, id, defaults) {
@@ -1048,7 +1085,7 @@ function throttle(func, wait = 0, options = {}) {
1048
1085
  /*!
1049
1086
  * Wunderbaum - ext-filter
1050
1087
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1051
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
1088
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1052
1089
  */
1053
1090
  const START_MARKER = "\uFFF7";
1054
1091
  const END_MARKER = "\uFFF8";
@@ -1087,7 +1124,9 @@ class FilterExtension extends WunderbaumExtension {
1087
1124
  });
1088
1125
  }
1089
1126
  _applyFilterImpl(filter, branchMode, _opts) {
1090
- let match, temp, start = Date.now(), count = 0, tree = this.tree, treeOpts = tree.options, escapeTitles = treeOpts.escapeTitles, prevAutoCollapse = treeOpts.autoCollapse, opts = extend({}, treeOpts.filter, _opts), hideMode = opts.mode === "hide", leavesOnly = !!opts.leavesOnly && !branchMode;
1127
+ let match, temp, start = Date.now(), count = 0, tree = this.tree, treeOpts = tree.options,
1128
+ // escapeTitles = treeOpts.escapeTitles,
1129
+ prevAutoCollapse = treeOpts.autoCollapse, opts = extend({}, treeOpts.filter, _opts), hideMode = opts.mode === "hide", leavesOnly = !!opts.leavesOnly && !branchMode;
1091
1130
  // Default to 'match title substring (case insensitive)'
1092
1131
  if (typeof filter === "string") {
1093
1132
  if (filter === "") {
@@ -1120,37 +1159,36 @@ class FilterExtension extends WunderbaumExtension {
1120
1159
  if (!node.title) {
1121
1160
  return false;
1122
1161
  }
1123
- let text = escapeTitles ? node.title : extractHtmlText(node.title);
1162
+ // let text = escapeTitles ? node.title : extractHtmlText(node.title);
1163
+ let text = node.title;
1124
1164
  // `.match` instead of `.test` to get the capture groups
1125
1165
  let res = text.match(re);
1126
1166
  if (res && opts.highlight) {
1127
- if (escapeTitles) {
1128
- if (opts.fuzzy) {
1129
- temp = _markFuzzyMatchedChars(text, res, escapeTitles);
1130
- }
1131
- else {
1132
- // #740: we must not apply the marks to escaped entity names, e.g. `&quot;`
1133
- // Use some exotic characters to mark matches:
1134
- temp = text.replace(reHighlight, function (s) {
1135
- return START_MARKER + s + END_MARKER;
1136
- });
1137
- }
1138
- // now we can escape the title...
1139
- node.titleWithHighlight = escapeHtml(temp)
1140
- // ... and finally insert the desired `<mark>` tags
1141
- .replace(RE_START_MARKER, "<mark>")
1142
- .replace(RE_END_MARTKER, "</mark>");
1167
+ // if (escapeTitles) {
1168
+ if (opts.fuzzy) {
1169
+ temp = _markFuzzyMatchedChars(text, res, true);
1143
1170
  }
1144
1171
  else {
1145
- if (opts.fuzzy) {
1146
- node.titleWithHighlight = _markFuzzyMatchedChars(text, res);
1147
- }
1148
- else {
1149
- node.titleWithHighlight = text.replace(reHighlight, function (s) {
1150
- return "<mark>" + s + "</mark>";
1151
- });
1152
- }
1172
+ // #740: we must not apply the marks to escaped entity names, e.g. `&quot;`
1173
+ // Use some exotic characters to mark matches:
1174
+ temp = text.replace(reHighlight, function (s) {
1175
+ return START_MARKER + s + END_MARKER;
1176
+ });
1153
1177
  }
1178
+ // now we can escape the title...
1179
+ node.titleWithHighlight = escapeHtml(temp)
1180
+ // ... and finally insert the desired `<mark>` tags
1181
+ .replace(RE_START_MARKER, "<mark>")
1182
+ .replace(RE_END_MARTKER, "</mark>");
1183
+ // } else {
1184
+ // if (opts.fuzzy) {
1185
+ // node.titleWithHighlight = _markFuzzyMatchedChars(text, res);
1186
+ // } else {
1187
+ // node.titleWithHighlight = text.replace(reHighlight, function (s) {
1188
+ // return "<mark>" + s + "</mark>";
1189
+ // });
1190
+ // }
1191
+ // }
1154
1192
  // node.debug("filter", escapeTitles, text, node.titleWithHighlight);
1155
1193
  }
1156
1194
  return !!res;
@@ -1236,8 +1274,6 @@ class FilterExtension extends WunderbaumExtension {
1236
1274
  }
1237
1275
  /**
1238
1276
  * [ext-filter] Re-apply current filter.
1239
- *
1240
- * @requires jquery.fancytree.filter.js
1241
1277
  */
1242
1278
  updateFilter() {
1243
1279
  let tree = this.tree;
@@ -1252,14 +1288,11 @@ class FilterExtension extends WunderbaumExtension {
1252
1288
  }
1253
1289
  /**
1254
1290
  * [ext-filter] Reset the filter.
1255
- *
1256
- * @alias Fancytree#clearFilter
1257
- * @requires jquery.fancytree.filter.js
1258
1291
  */
1259
1292
  clearFilter() {
1260
- let tree = this.tree,
1293
+ let tree = this.tree;
1261
1294
  // statusNode = tree.root.findDirectChild(KEY_NODATA),
1262
- escapeTitles = tree.options.escapeTitles;
1295
+ // escapeTitles = tree.options.escapeTitles;
1263
1296
  // enhanceTitle = tree.options.enhanceTitle,
1264
1297
  tree.enableUpdate(false);
1265
1298
  // if (statusNode) {
@@ -1273,12 +1306,11 @@ class FilterExtension extends WunderbaumExtension {
1273
1306
  if (node.match && node._rowElem) {
1274
1307
  // #491, #601
1275
1308
  let titleElem = node._rowElem.querySelector("span.wb-title");
1276
- if (escapeTitles) {
1277
- titleElem.textContent = node.title;
1278
- }
1279
- else {
1280
- titleElem.innerHTML = node.title;
1281
- }
1309
+ // if (escapeTitles) {
1310
+ titleElem.textContent = node.title;
1311
+ // } else {
1312
+ // titleElem.innerHTML = node.title;
1313
+ // }
1282
1314
  node._callEvent("enhanceTitle", { titleElem: titleElem });
1283
1315
  }
1284
1316
  delete node.match;
@@ -1314,7 +1346,7 @@ class FilterExtension extends WunderbaumExtension {
1314
1346
  * @param {string} text
1315
1347
  * @param {RegExpMatchArray} matches
1316
1348
  */
1317
- function _markFuzzyMatchedChars(text, matches, escapeTitles = false) {
1349
+ function _markFuzzyMatchedChars(text, matches, escapeTitles = true) {
1318
1350
  let matchingIndices = [];
1319
1351
  // get the indices of matched characters (Iterate through `RegExpMatchArray`)
1320
1352
  for (let _matchingArrIdx = 1; _matchingArrIdx < matches.length; _matchingArrIdx++) {
@@ -1329,7 +1361,7 @@ function _markFuzzyMatchedChars(text, matches, escapeTitles = false) {
1329
1361
  // Map each `text` char to its position and store in `textPoses`.
1330
1362
  let textPoses = text.split("");
1331
1363
  if (escapeTitles) {
1332
- // If escaping the title, then wrap the matchng char within exotic chars
1364
+ // If escaping the title, then wrap the matching char within exotic chars
1333
1365
  matchingIndices.forEach(function (v) {
1334
1366
  textPoses[v] = START_MARKER + textPoses[v] + END_MARKER;
1335
1367
  });
@@ -1347,7 +1379,7 @@ function _markFuzzyMatchedChars(text, matches, escapeTitles = false) {
1347
1379
  /*!
1348
1380
  * Wunderbaum - ext-keynav
1349
1381
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1350
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
1382
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1351
1383
  */
1352
1384
  class KeynavExtension extends WunderbaumExtension {
1353
1385
  constructor(tree) {
@@ -1414,7 +1446,7 @@ class KeynavExtension extends WunderbaumExtension {
1414
1446
  eventName = "Add"; // expand
1415
1447
  }
1416
1448
  else if (navModeOption === NavigationModeOption.startRow) {
1417
- tree.setCellMode(NavigationMode.cellNav);
1449
+ tree.setNavigationMode(NavigationMode.cellNav);
1418
1450
  return;
1419
1451
  }
1420
1452
  break;
@@ -1453,6 +1485,8 @@ class KeynavExtension extends WunderbaumExtension {
1453
1485
  case "Home":
1454
1486
  case "Control+End":
1455
1487
  case "Control+Home":
1488
+ case "Meta+ArrowDown":
1489
+ case "Meta+ArrowUp":
1456
1490
  case "PageDown":
1457
1491
  case "PageUp":
1458
1492
  node.navigate(eventName, { activate: activate, event: event });
@@ -1484,11 +1518,11 @@ class KeynavExtension extends WunderbaumExtension {
1484
1518
  break;
1485
1519
  case "Escape":
1486
1520
  if (tree.navMode === NavigationMode.cellEdit) {
1487
- tree.setCellMode(NavigationMode.cellNav);
1521
+ tree.setNavigationMode(NavigationMode.cellNav);
1488
1522
  handled = true;
1489
1523
  }
1490
1524
  else if (tree.navMode === NavigationMode.cellNav) {
1491
- tree.setCellMode(NavigationMode.row);
1525
+ tree.setNavigationMode(NavigationMode.row);
1492
1526
  handled = true;
1493
1527
  }
1494
1528
  break;
@@ -1498,7 +1532,7 @@ class KeynavExtension extends WunderbaumExtension {
1498
1532
  handled = true;
1499
1533
  }
1500
1534
  else if (navModeOption !== NavigationModeOption.cell) {
1501
- tree.setCellMode(NavigationMode.row);
1535
+ tree.setNavigationMode(NavigationMode.row);
1502
1536
  handled = true;
1503
1537
  }
1504
1538
  break;
@@ -1515,6 +1549,8 @@ class KeynavExtension extends WunderbaumExtension {
1515
1549
  case "Home":
1516
1550
  case "Control+End":
1517
1551
  case "Control+Home":
1552
+ case "Meta+ArrowDown":
1553
+ case "Meta+ArrowUp":
1518
1554
  case "PageDown":
1519
1555
  case "PageUp":
1520
1556
  node.navigate(eventName, { activate: activate, event: event });
@@ -1533,7 +1569,7 @@ class KeynavExtension extends WunderbaumExtension {
1533
1569
  /*!
1534
1570
  * Wunderbaum - ext-logger
1535
1571
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1536
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
1572
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1537
1573
  */
1538
1574
  class LoggerExtension extends WunderbaumExtension {
1539
1575
  constructor(tree) {
@@ -1573,19 +1609,9 @@ class LoggerExtension extends WunderbaumExtension {
1573
1609
  /*!
1574
1610
  * Wunderbaum - ext-dnd
1575
1611
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1576
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
1612
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1577
1613
  */
1578
- const nodeMimeType = "application/x-fancytree-node";
1579
- // type AllowedDropRegionType =
1580
- // | "after"
1581
- // | "afterBefore"
1582
- // // | "afterBeforeOver" // == all == true
1583
- // | "afterOver"
1584
- // | "all" // == true
1585
- // | "before"
1586
- // | "beforeOver"
1587
- // | "none" // == false == "" == null
1588
- // | "over"; // == "child"
1614
+ const nodeMimeType = "application/x-wunderbaum-node";
1589
1615
  class DndExtension extends WunderbaumExtension {
1590
1616
  constructor(tree) {
1591
1617
  super(tree, "dnd", {
@@ -1848,10 +1874,181 @@ class DndExtension extends WunderbaumExtension {
1848
1874
  }
1849
1875
  }
1850
1876
 
1877
+ /*!
1878
+ * Wunderbaum - drag_observer
1879
+ * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1880
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1881
+ */
1882
+ /**
1883
+ * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'.
1884
+ */
1885
+ class DragObserver {
1886
+ constructor(opts) {
1887
+ this.start = {
1888
+ x: 0,
1889
+ y: 0,
1890
+ altKey: false,
1891
+ ctrlKey: false,
1892
+ metaKey: false,
1893
+ shiftKey: false,
1894
+ };
1895
+ this.dragElem = null;
1896
+ this.dragging = false;
1897
+ // TODO: touch events
1898
+ this.events = ["mousedown", "mouseup", "mousemove", "keydown"];
1899
+ assert(opts.root);
1900
+ this.opts = extend({ thresh: 5 }, opts);
1901
+ this.root = opts.root;
1902
+ this._handler = this.handleEvent.bind(this);
1903
+ this.events.forEach((type) => {
1904
+ this.root.addEventListener(type, this._handler);
1905
+ });
1906
+ }
1907
+ /** Unregister all event listeners. */
1908
+ disconnect() {
1909
+ this.events.forEach((type) => {
1910
+ this.root.removeEventListener(type, this._handler);
1911
+ });
1912
+ }
1913
+ getDragElem() {
1914
+ return this.dragElem;
1915
+ }
1916
+ isDragging() {
1917
+ return this.dragging;
1918
+ }
1919
+ stopDrag(cb_event) {
1920
+ if (this.dragging && this.opts.dragstop && cb_event) {
1921
+ cb_event.type = "dragstop";
1922
+ this.opts.dragstop(cb_event);
1923
+ }
1924
+ this.dragElem = null;
1925
+ this.dragging = false;
1926
+ }
1927
+ handleEvent(e) {
1928
+ const type = e.type;
1929
+ const opts = this.opts;
1930
+ const cb_event = {
1931
+ type: e.type,
1932
+ event: e,
1933
+ dragElem: this.dragElem,
1934
+ dx: e.pageX - this.start.x,
1935
+ dy: e.pageY - this.start.y,
1936
+ apply: undefined,
1937
+ };
1938
+ switch (type) {
1939
+ case "keydown":
1940
+ this.stopDrag(cb_event);
1941
+ break;
1942
+ case "mousedown":
1943
+ if (this.dragElem) {
1944
+ this.stopDrag(cb_event);
1945
+ break;
1946
+ }
1947
+ if (opts.selector) {
1948
+ let elem = e.target;
1949
+ if (elem.matches(opts.selector)) {
1950
+ this.dragElem = elem;
1951
+ }
1952
+ else {
1953
+ elem = elem.closest(opts.selector);
1954
+ if (elem) {
1955
+ this.dragElem = elem;
1956
+ }
1957
+ else {
1958
+ break; // no event delegation selector matched
1959
+ }
1960
+ }
1961
+ }
1962
+ this.start.x = e.pageX;
1963
+ this.start.y = e.pageY;
1964
+ this.start.altKey = e.altKey;
1965
+ this.start.ctrlKey = e.ctrlKey;
1966
+ this.start.metaKey = e.metaKey;
1967
+ this.start.shiftKey = e.shiftKey;
1968
+ break;
1969
+ case "mousemove":
1970
+ // TODO: debounce/throttle?
1971
+ // TODO: horizontal mode: ignore if dx unchanged
1972
+ if (!this.dragElem) {
1973
+ break;
1974
+ }
1975
+ if (!this.dragging) {
1976
+ if (opts.thresh) {
1977
+ const dist2 = cb_event.dx * cb_event.dx + cb_event.dy * cb_event.dy;
1978
+ if (dist2 < opts.thresh * opts.thresh) {
1979
+ break;
1980
+ }
1981
+ }
1982
+ cb_event.type = "dragstart";
1983
+ if (opts.dragstart(cb_event) === false) {
1984
+ this.stopDrag(cb_event);
1985
+ break;
1986
+ }
1987
+ this.dragging = true;
1988
+ }
1989
+ if (this.dragging && this.opts.drag) {
1990
+ cb_event.type = "drag";
1991
+ this.opts.drag(cb_event);
1992
+ }
1993
+ break;
1994
+ case "mouseup":
1995
+ if (!this.dragging) {
1996
+ this.stopDrag(cb_event);
1997
+ break;
1998
+ }
1999
+ if (e.button === 0) {
2000
+ cb_event.apply = true;
2001
+ }
2002
+ else {
2003
+ cb_event.apply = false;
2004
+ }
2005
+ this.stopDrag(cb_event);
2006
+ break;
2007
+ }
2008
+ }
2009
+ }
2010
+
2011
+ /*!
2012
+ * Wunderbaum - ext-grid
2013
+ * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
2014
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
2015
+ */
2016
+ class GridExtension extends WunderbaumExtension {
2017
+ constructor(tree) {
2018
+ super(tree, "grid", {
2019
+ // throttle: 200,
2020
+ });
2021
+ this.observer = new DragObserver({
2022
+ root: window.document,
2023
+ selector: "span.wb-col-resizer",
2024
+ thresh: 4,
2025
+ // throttle: 400,
2026
+ dragstart: (e) => {
2027
+ return this.tree.element.contains(e.dragElem);
2028
+ },
2029
+ drag: (e) => {
2030
+ // TODO: throttle
2031
+ return this.handleDrag(e);
2032
+ },
2033
+ dragstop: (e) => {
2034
+ return this.handleDrag(e);
2035
+ },
2036
+ });
2037
+ }
2038
+ init() {
2039
+ super.init();
2040
+ }
2041
+ handleDrag(e) {
2042
+ const info = Wunderbaum.getEventInfo(e.event);
2043
+ // this.tree.options.
2044
+ this.tree.log(`${e.type}(${e.dx})`, e, info);
2045
+ }
2046
+ }
2047
+
1851
2048
  /*!
1852
2049
  * Wunderbaum - deferred
1853
2050
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1854
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
2051
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1855
2052
  */
1856
2053
  /**
1857
2054
  * Deferred is a ES6 Promise, that exposes the resolve() and reject()` method.
@@ -1894,7 +2091,7 @@ class Deferred {
1894
2091
  /*!
1895
2092
  * Wunderbaum - wunderbaum_node
1896
2093
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1897
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
2094
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1898
2095
  */
1899
2096
  /** Top-level properties that can be passed with `data`. */
1900
2097
  const NODE_PROPS = new Set([
@@ -1931,15 +2128,31 @@ const NODE_ATTRS = new Set([
1931
2128
  "unselectableIgnore",
1932
2129
  "unselectableStatus",
1933
2130
  ]);
2131
+ /**
2132
+ * A single tree node.
2133
+ *
2134
+ * **NOTE:** <br>
2135
+ * Generally you should not modify properties directly, since this may break
2136
+ * the internal bookkeeping.
2137
+ */
1934
2138
  class WunderbaumNode {
1935
2139
  constructor(tree, parent, data) {
1936
2140
  var _a, _b;
2141
+ /** Reference key. Unlike {@link key}, a `refKey` may occur multiple
2142
+ * times within a tree (in this case we have 'clone nodes').
2143
+ * @see Use {@link setKey} to modify.
2144
+ */
1937
2145
  this.refKey = undefined;
1938
2146
  this.children = null;
1939
2147
  this.lazy = false;
2148
+ /** Expansion state.
2149
+ * @see {@link isExpandable}, {@link isExpanded}, {@link setExpanded}. */
1940
2150
  this.expanded = false;
2151
+ /** Selection state.
2152
+ * @see {@link isSelected}, {@link setSelected}. */
1941
2153
  this.selected = false;
1942
- /** Additional classes added to `div.wb-row`. */
2154
+ /** Additional classes added to `div.wb-row`.
2155
+ * @see {@link addClass}, {@link removeClass}, {@link toggleClass}. */
1943
2156
  this.extraClasses = new Set();
1944
2157
  /** Custom data that was passed to the constructor */
1945
2158
  this.data = {};
@@ -2066,7 +2279,7 @@ class WunderbaumNode {
2066
2279
  // this.fixSelection3FromEndNodes();
2067
2280
  // }
2068
2281
  // this.triggerModifyChild("add", nodeList.length === 1 ? nodeList[0] : null);
2069
- this.tree.setModified(ChangeType.structure, this);
2282
+ this.tree.setModified(ChangeType.structure);
2070
2283
  return nodeList[0];
2071
2284
  }
2072
2285
  finally {
@@ -2105,7 +2318,8 @@ class WunderbaumNode {
2105
2318
  }
2106
2319
  /**
2107
2320
  * Apply a modification (or navigation) operation.
2108
- * @see Wunderbaum#applyCommand
2321
+ *
2322
+ * @see {@link Wunderbaum.applyCommand}
2109
2323
  */
2110
2324
  applyCommand(cmd, opts) {
2111
2325
  return this.tree.applyCommand(cmd, this, opts);
@@ -2198,8 +2412,7 @@ class WunderbaumNode {
2198
2412
  }
2199
2413
  /** Find a node relative to self.
2200
2414
  *
2201
- * @param where The keyCode that would normally trigger this move,
2202
- * or a keyword ('down', 'first', 'last', 'left', 'parent', 'right', 'up').
2415
+ * @see {@link Wunderbaum.findRelatedNode|tree.findRelatedNode()}
2203
2416
  */
2204
2417
  findRelatedNode(where, includeHidden = false) {
2205
2418
  return this.tree.findRelatedNode(this, where, includeHidden);
@@ -2500,7 +2713,7 @@ class WunderbaumNode {
2500
2713
  assert(!this.parent);
2501
2714
  tree.columns = data.columns;
2502
2715
  delete data.columns;
2503
- tree.renderHeader();
2716
+ tree.updateColumns({ calculateCols: false });
2504
2717
  }
2505
2718
  this._loadSourceObject(data);
2506
2719
  }
@@ -2536,19 +2749,20 @@ class WunderbaumNode {
2536
2749
  this.setStatus(NodeStatusType.ok);
2537
2750
  return;
2538
2751
  }
2539
- assert(isArray(source) || (source && source.url), "The lazyLoad event must return a node list, `{url: ...}` or false.");
2752
+ assert(isArray(source) || (source && source.url), "The lazyLoad event must return a node list, `{url: ...}`, or false.");
2540
2753
  await this.load(source); // also calls setStatus('ok')
2541
2754
  if (wasExpanded) {
2542
2755
  this.expanded = true;
2543
- this.tree.updateViewport();
2756
+ this.tree.setModified(ChangeType.structure);
2544
2757
  }
2545
2758
  else {
2546
- this.render(); // Fix expander icon to 'loaded'
2759
+ this.setModified(); // Fix expander icon to 'loaded'
2547
2760
  }
2548
2761
  }
2549
2762
  catch (e) {
2763
+ this.logError("Error during loadLazy()", e);
2764
+ this._callEvent("error", { error: e });
2550
2765
  this.setStatus(NodeStatusType.error, "" + e);
2551
- // } finally {
2552
2766
  }
2553
2767
  return;
2554
2768
  }
@@ -2696,7 +2910,7 @@ class WunderbaumNode {
2696
2910
  n.tree = targetNode.tree;
2697
2911
  }, true);
2698
2912
  }
2699
- tree.updateViewport();
2913
+ tree.setModified(ChangeType.structure);
2700
2914
  // TODO: fix selection state
2701
2915
  // TODO: fix active state
2702
2916
  }
@@ -2716,31 +2930,33 @@ class WunderbaumNode {
2716
2930
  // Allow to pass 'ArrowLeft' instead of 'left'
2717
2931
  where = KEY_TO_ACTION_DICT[where] || where;
2718
2932
  // Otherwise activate or focus the related node
2719
- let node = this.findRelatedNode(where);
2720
- if (node) {
2721
- // setFocus/setActive will scroll later (if autoScroll is specified)
2722
- try {
2723
- node.makeVisible({ scrollIntoView: false });
2724
- }
2725
- catch (e) { } // #272
2726
- node.setFocus();
2727
- if ((options === null || options === void 0 ? void 0 : options.activate) === false) {
2728
- return Promise.resolve(this);
2729
- }
2730
- return node.setActive(true, { event: options === null || options === void 0 ? void 0 : options.event });
2933
+ const node = this.findRelatedNode(where);
2934
+ if (!node) {
2935
+ this.logWarn(`Could not find related node '${where}'.`);
2936
+ return Promise.resolve(this);
2937
+ }
2938
+ // setFocus/setActive will scroll later (if autoScroll is specified)
2939
+ try {
2940
+ node.makeVisible({ scrollIntoView: false });
2731
2941
  }
2732
- this.logWarn("Could not find related node '" + where + "'.");
2733
- return Promise.resolve(this);
2942
+ catch (e) { } // #272
2943
+ node.setFocus();
2944
+ if ((options === null || options === void 0 ? void 0 : options.activate) === false) {
2945
+ return Promise.resolve(this);
2946
+ }
2947
+ return node.setActive(true, { event: options === null || options === void 0 ? void 0 : options.event });
2734
2948
  }
2735
2949
  /** Delete this node and all descendants. */
2736
2950
  remove() {
2737
2951
  const tree = this.tree;
2738
2952
  const pos = this.parent.children.indexOf(this);
2953
+ this.triggerModify("remove");
2739
2954
  this.parent.children.splice(pos, 1);
2740
2955
  this.visit((n) => {
2741
2956
  n.removeMarkup();
2742
2957
  tree._unregisterNode(n);
2743
2958
  }, true);
2959
+ tree.setModified(ChangeType.structure);
2744
2960
  }
2745
2961
  /** Remove all descendants of this node. */
2746
2962
  removeChildren() {
@@ -2772,7 +2988,7 @@ class WunderbaumNode {
2772
2988
  if (!this.isRootNode()) {
2773
2989
  this.expanded = false;
2774
2990
  }
2775
- this.tree.updateViewport();
2991
+ this.tree.setModified(ChangeType.structure);
2776
2992
  }
2777
2993
  /** Remove all HTML markup from the DOM. */
2778
2994
  removeMarkup() {
@@ -2865,6 +3081,7 @@ class WunderbaumNode {
2865
3081
  const activeColIdx = tree.navMode === NavigationMode.row ? null : tree.activeColIdx;
2866
3082
  // let colElems: HTMLElement[];
2867
3083
  const isNew = !rowDiv;
3084
+ assert(!isNew || (opts && opts.after), "opts.after expected, unless updating");
2868
3085
  assert(!this.isRootNode());
2869
3086
  //
2870
3087
  let rowClasses = ["wb-row"];
@@ -2916,7 +3133,7 @@ class WunderbaumNode {
2916
3133
  nodeElem.appendChild(elem);
2917
3134
  ofsTitlePx += ICON_WIDTH;
2918
3135
  }
2919
- if (level > treeOptions.minExpandLevel) {
3136
+ if (!treeOptions.minExpandLevel || level > treeOptions.minExpandLevel) {
2920
3137
  expanderSpan = document.createElement("i");
2921
3138
  nodeElem.appendChild(expanderSpan);
2922
3139
  ofsTitlePx += ICON_WIDTH;
@@ -3007,11 +3224,11 @@ class WunderbaumNode {
3007
3224
  if (this.titleWithHighlight) {
3008
3225
  titleSpan.innerHTML = this.titleWithHighlight;
3009
3226
  }
3010
- else if (tree.options.escapeTitles) {
3011
- titleSpan.textContent = this.title;
3012
- }
3013
3227
  else {
3014
- titleSpan.innerHTML = this.title;
3228
+ // } else if (tree.options.escapeTitles) {
3229
+ titleSpan.textContent = this.title;
3230
+ // } else {
3231
+ // titleSpan.innerHTML = this.title;
3015
3232
  }
3016
3233
  // Set the width of the title span, so overflow ellipsis work
3017
3234
  if (!treeOptions.skeleton) {
@@ -3045,9 +3262,19 @@ class WunderbaumNode {
3045
3262
  });
3046
3263
  }
3047
3264
  // Attach to DOM as late as possible
3048
- // if (!this._rowElem) {
3049
- tree.nodeListElement.appendChild(rowDiv);
3050
- // }
3265
+ if (isNew) {
3266
+ const after = opts ? opts.after : "last";
3267
+ switch (after) {
3268
+ case "first":
3269
+ tree.nodeListElement.prepend(rowDiv);
3270
+ break;
3271
+ case "last":
3272
+ tree.nodeListElement.appendChild(rowDiv);
3273
+ break;
3274
+ default:
3275
+ opts.after.after(rowDiv);
3276
+ }
3277
+ }
3051
3278
  }
3052
3279
  /**
3053
3280
  * Remove all children, collapse, and set the lazy-flag, so that the lazyLoad
@@ -3058,7 +3285,7 @@ class WunderbaumNode {
3058
3285
  this.expanded = false;
3059
3286
  this.lazy = true;
3060
3287
  this.children = null;
3061
- this.tree.updateViewport();
3288
+ this.tree.setModified(ChangeType.structure);
3062
3289
  }
3063
3290
  /** Convert node (or whole branch) into a plain object.
3064
3291
  *
@@ -3121,14 +3348,15 @@ class WunderbaumNode {
3121
3348
  *
3122
3349
  * Evaluation sequence:
3123
3350
  *
3124
- * If `tree.options.<name>` is a callback that returns something, use that.
3125
- * Else if `node.<name>` is defined, use that.
3126
- * Else if `tree.types[<node.type>]` is a value, use that.
3127
- * Else if `tree.options.<name>` is a value, use that.
3128
- * Else use `defaultValue`.
3351
+ * - If `tree.options.<name>` is a callback that returns something, use that.
3352
+ * - Else if `node.<name>` is defined, use that.
3353
+ * - Else if `tree.types[<node.type>]` is a value, use that.
3354
+ * - Else if `tree.options.<name>` is a value, use that.
3355
+ * - Else use `defaultValue`.
3129
3356
  *
3130
3357
  * @param name name of the option property (on node and tree)
3131
3358
  * @param defaultValue return this if nothing else matched
3359
+ * {@link Wunderbaum.getOption|Wunderbaum.getOption()}
3132
3360
  */
3133
3361
  getOption(name, defaultValue) {
3134
3362
  let tree = this.tree;
@@ -3163,15 +3391,21 @@ class WunderbaumNode {
3163
3391
  // Use value from value options dict, fallback do default
3164
3392
  return value !== null && value !== void 0 ? value : defaultValue;
3165
3393
  }
3394
+ /** Make sure that this node is visible in the viewport.
3395
+ * @see {@link Wunderbaum.scrollTo|Wunderbaum.scrollTo()}
3396
+ */
3166
3397
  async scrollIntoView(options) {
3167
3398
  return this.tree.scrollTo(this);
3168
3399
  }
3400
+ /**
3401
+ * Activate this node, deactivate previous, send events, activate column and scroll int viewport.
3402
+ */
3169
3403
  async setActive(flag = true, options) {
3170
3404
  const tree = this.tree;
3171
3405
  const prev = tree.activeNode;
3172
3406
  const retrigger = options === null || options === void 0 ? void 0 : options.retrigger;
3173
- const noEvent = options === null || options === void 0 ? void 0 : options.noEvent;
3174
- if (!noEvent) {
3407
+ const noEvents = options === null || options === void 0 ? void 0 : options.noEvents;
3408
+ if (!noEvents) {
3175
3409
  let orgEvent = options === null || options === void 0 ? void 0 : options.event;
3176
3410
  if (flag) {
3177
3411
  if (prev !== this || retrigger) {
@@ -3186,7 +3420,7 @@ class WunderbaumNode {
3186
3420
  orgEvent: orgEvent,
3187
3421
  }) === false) {
3188
3422
  tree.activeNode = null;
3189
- prev === null || prev === void 0 ? void 0 : prev.setDirty(ChangeType.status);
3423
+ prev === null || prev === void 0 ? void 0 : prev.setModified();
3190
3424
  return;
3191
3425
  }
3192
3426
  }
@@ -3197,8 +3431,8 @@ class WunderbaumNode {
3197
3431
  }
3198
3432
  if (prev !== this) {
3199
3433
  tree.activeNode = this;
3200
- prev === null || prev === void 0 ? void 0 : prev.setDirty(ChangeType.status);
3201
- this.setDirty(ChangeType.status);
3434
+ prev === null || prev === void 0 ? void 0 : prev.setModified();
3435
+ this.setModified();
3202
3436
  }
3203
3437
  if (options &&
3204
3438
  options.colIdx != null &&
@@ -3209,57 +3443,62 @@ class WunderbaumNode {
3209
3443
  // requestAnimationFrame(() => {
3210
3444
  // this.scrollIntoView();
3211
3445
  // })
3212
- this.scrollIntoView();
3213
- }
3214
- setDirty(type) {
3215
- if (this.tree._disableUpdate) {
3216
- return;
3217
- }
3218
- if (type === ChangeType.structure) {
3219
- this.tree.updateViewport();
3220
- }
3221
- else if (this._rowElem) {
3222
- // otherwise not in viewport, so no need to render
3223
- this.render();
3224
- }
3446
+ return this.scrollIntoView();
3225
3447
  }
3448
+ /**
3449
+ * Expand or collapse this node.
3450
+ */
3226
3451
  async setExpanded(flag = true, options) {
3227
3452
  // alert("" + this.getLevel() + ", "+ this.getOption("minExpandLevel");
3228
3453
  if (!flag &&
3229
3454
  this.isExpanded() &&
3230
3455
  this.getLevel() < this.getOption("minExpandLevel") &&
3231
3456
  !getOption(options, "force")) {
3232
- this.logDebug("Ignored collapse request.");
3457
+ this.logDebug("Ignored collapse request below expandLevel.");
3233
3458
  return;
3234
3459
  }
3235
3460
  if (flag && this.lazy && this.children == null) {
3236
3461
  await this.loadLazy();
3237
3462
  }
3238
3463
  this.expanded = flag;
3239
- this.setDirty(ChangeType.structure);
3240
- }
3241
- setIcon() {
3242
- throw new Error("Not yet implemented");
3243
- // this.setDirty(ChangeType.status);
3464
+ this.tree.setModified(ChangeType.structure);
3244
3465
  }
3466
+ /**
3467
+ * Set keyboard focus here.
3468
+ * @see {@link setActive}
3469
+ */
3245
3470
  setFocus(flag = true, options) {
3246
3471
  const prev = this.tree.focusNode;
3247
3472
  this.tree.focusNode = this;
3248
- prev === null || prev === void 0 ? void 0 : prev.setDirty(ChangeType.status);
3249
- this.setDirty(ChangeType.status);
3473
+ prev === null || prev === void 0 ? void 0 : prev.setModified();
3474
+ this.setModified();
3250
3475
  }
3476
+ /** Set a new icon path or class. */
3477
+ setIcon() {
3478
+ throw new Error("Not yet implemented");
3479
+ // this.setModified();
3480
+ }
3481
+ /** Change node's {@link key} and/or {@link refKey}. */
3482
+ setKey(key, refKey) {
3483
+ throw new Error("Not yet implemented");
3484
+ }
3485
+ /** Schedule a render, typically called to update after a status or data change. */
3486
+ setModified(change = ChangeType.status) {
3487
+ assert(change === ChangeType.status);
3488
+ this.tree.setModified(ChangeType.row, this);
3489
+ }
3490
+ /** Modify the check/uncheck state. */
3251
3491
  setSelected(flag = true, options) {
3252
3492
  const prev = this.selected;
3253
3493
  if (!!flag !== prev) {
3254
3494
  this._callEvent("select", { flag: flag });
3255
3495
  }
3256
3496
  this.selected = !!flag;
3257
- this.setDirty(ChangeType.status);
3497
+ this.setModified();
3258
3498
  }
3259
- /** Show node status (ok, loading, error, noData) using styles and a dummy child node.
3260
- */
3499
+ /** Display node status (ok, loading, error, noData) using styles and a dummy child node. */
3261
3500
  setStatus(status, message, details) {
3262
- let tree = this.tree;
3501
+ const tree = this.tree;
3263
3502
  let statusNode = null;
3264
3503
  const _clearStatusNode = () => {
3265
3504
  // Remove dedicated dummy node, if any
@@ -3330,12 +3569,13 @@ class WunderbaumNode {
3330
3569
  default:
3331
3570
  error("invalid node status " + status);
3332
3571
  }
3333
- tree.updateViewport();
3572
+ tree.setModified(ChangeType.structure);
3334
3573
  return statusNode;
3335
3574
  }
3575
+ /** Rename this node. */
3336
3576
  setTitle(title) {
3337
3577
  this.title = title;
3338
- this.setDirty(ChangeType.status);
3578
+ this.setModified();
3339
3579
  // this.triggerModify("rename"); // TODO
3340
3580
  }
3341
3581
  /**
@@ -3356,10 +3596,16 @@ class WunderbaumNode {
3356
3596
  * @param {object} [extra]
3357
3597
  */
3358
3598
  triggerModify(operation, extra) {
3599
+ if (!this.parent) {
3600
+ return;
3601
+ }
3359
3602
  this.parent.triggerModifyChild(operation, this, extra);
3360
3603
  }
3361
- /** Call fn(node) for all child nodes in hierarchical order (depth-first).<br>
3362
- * Stop iteration, if fn() returns false. Skip current branch, if fn() returns "skip".<br>
3604
+ /**
3605
+ * Call fn(node) for all child nodes in hierarchical order (depth-first).
3606
+ *
3607
+ * Stop iteration, if fn() returns false. Skip current branch, if fn()
3608
+ * returns "skip".<br>
3363
3609
  * Return false if iteration was stopped.
3364
3610
  *
3365
3611
  * @param {function} callback the callback function.
@@ -3403,7 +3649,8 @@ class WunderbaumNode {
3403
3649
  }
3404
3650
  return true;
3405
3651
  }
3406
- /** Call fn(node) for all sibling nodes.<br>
3652
+ /**
3653
+ * Call fn(node) for all sibling nodes.<br>
3407
3654
  * Stop iteration, if fn() returns false.<br>
3408
3655
  * Return false if iteration was stopped.
3409
3656
  *
@@ -3434,7 +3681,7 @@ WunderbaumNode.sequence = 0;
3434
3681
  /*!
3435
3682
  * Wunderbaum - ext-edit
3436
3683
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
3437
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
3684
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
3438
3685
  */
3439
3686
  // const START_MARKER = "\uFFF7";
3440
3687
  class EditExtension extends WunderbaumExtension {
@@ -3552,7 +3799,7 @@ class EditExtension extends WunderbaumExtension {
3552
3799
  break;
3553
3800
  case "F2":
3554
3801
  if (trigger.indexOf("F2") >= 0) {
3555
- // tree.setCellMode(NavigationMode.cellEdit);
3802
+ // tree.setNavigationMode(NavigationMode.cellEdit);
3556
3803
  this.startEditTitle();
3557
3804
  return false;
3558
3805
  }
@@ -3593,6 +3840,7 @@ class EditExtension extends WunderbaumExtension {
3593
3840
  if (validity) {
3594
3841
  // Permanently apply input validations (CSS and tooltip)
3595
3842
  inputElem.addEventListener("keydown", (e) => {
3843
+ inputElem.setCustomValidity("");
3596
3844
  if (!inputElem.reportValidity()) ;
3597
3845
  });
3598
3846
  }
@@ -3633,6 +3881,11 @@ class EditExtension extends WunderbaumExtension {
3633
3881
  }
3634
3882
  node.logDebug(`stopEditTitle(${apply})`, opts, focusElem, newValue);
3635
3883
  if (apply && newValue !== null && newValue !== node.title) {
3884
+ const errMsg = focusElem.validationMessage;
3885
+ if (errMsg) {
3886
+ // input element's native validation failed
3887
+ throw new Error(`Input validation failed for "${newValue}": ${errMsg}.`);
3888
+ }
3636
3889
  const colElem = node.getColElem(0);
3637
3890
  this._applyChange("edit.apply", node, colElem, {
3638
3891
  oldValue: node.title,
@@ -3719,12 +3972,12 @@ class EditExtension extends WunderbaumExtension {
3719
3972
  * Copyright (c) 2021-2022, Martin Wendt (https://wwWendt.de).
3720
3973
  * Released under the MIT license.
3721
3974
  *
3722
- * @version v0.0.1-0
3723
- * @date Thu, 31 Mar 2022 10:19:39 GMT
3975
+ * @version v0.0.3
3976
+ * @date Mon, 18 Apr 2022 11:52:44 GMT
3724
3977
  */
3725
3978
  // const class_prefix = "wb-";
3726
3979
  // const node_props: string[] = ["title", "key", "refKey"];
3727
- const MAX_CHANGED_NODES = 10;
3980
+ // const MAX_CHANGED_NODES = 10;
3728
3981
  /**
3729
3982
  * A persistent plain object or array.
3730
3983
  *
@@ -3736,36 +3989,43 @@ class Wunderbaum {
3736
3989
  this.extensions = {};
3737
3990
  this.keyMap = new Map();
3738
3991
  this.refKeyMap = new Map();
3739
- this.viewNodes = new Set();
3740
- // protected rows: WunderbaumNode[] = [];
3741
- // protected _rowCount = 0;
3992
+ // protected viewNodes = new Set<WunderbaumNode>();
3993
+ this.treeRowCount = 0;
3994
+ this._disableUpdateCount = 0;
3742
3995
  // protected eventHandlers : Array<function> = [];
3996
+ /** Currently active node if any. */
3743
3997
  this.activeNode = null;
3998
+ /** Current node hat has keyboard focus if any. */
3744
3999
  this.focusNode = null;
3745
- this._disableUpdate = 0;
3746
- this._disableUpdateCount = 0;
3747
4000
  /** Shared properties, referenced by `node.type`. */
3748
4001
  this.types = {};
3749
4002
  /** List of column definitions. */
3750
4003
  this.columns = [];
3751
4004
  this._columnsById = {};
3752
4005
  // Modification Status
3753
- this.changedSince = 0;
3754
- this.changes = new Set();
3755
- this.changedNodes = new Set();
4006
+ // protected changedSince = 0;
4007
+ // protected changes = new Set<ChangeType>();
4008
+ // protected changedNodes = new Set<WunderbaumNode>();
4009
+ this.changeRedrawRequestPending = false;
4010
+ /** Expose some useful methods of the util.ts module as `tree._util`. */
4011
+ this._util = util;
3756
4012
  // --- FILTER ---
3757
4013
  this.filterMode = null;
3758
4014
  // --- KEYNAV ---
4015
+ /** @internal Use `setColumn()`/`getActiveColElem()`*/
3759
4016
  this.activeColIdx = 0;
4017
+ /** @internal */
3760
4018
  this.navMode = NavigationMode.row;
4019
+ /** @internal */
3761
4020
  this.lastQuicksearchTime = 0;
4021
+ /** @internal */
3762
4022
  this.lastQuicksearchTerm = "";
3763
4023
  // --- EDIT ---
3764
4024
  this.lastClickTime = 0;
3765
- // TODO: make accessible in compiled JS like this?
3766
- this._util = util;
3767
- /** Alias for `logDebug` */
3768
- this.log = this.logDebug; // Alias
4025
+ /** Alias for {@link Wunderbaum.logDebug}.
4026
+ * @alias Wunderbaum.logDebug
4027
+ */
4028
+ this.log = this.logDebug;
3769
4029
  let opts = (this.options = extend({
3770
4030
  id: null,
3771
4031
  source: null,
@@ -3776,7 +4036,7 @@ class Wunderbaum {
3776
4036
  rowHeightPx: ROW_HEIGHT,
3777
4037
  columns: null,
3778
4038
  types: null,
3779
- escapeTitles: true,
4039
+ // escapeTitles: true,
3780
4040
  showSpinner: false,
3781
4041
  checkbox: true,
3782
4042
  minExpandLevel: 0,
@@ -3833,6 +4093,7 @@ class Wunderbaum {
3833
4093
  this._registerExtension(new EditExtension(this));
3834
4094
  this._registerExtension(new FilterExtension(this));
3835
4095
  this._registerExtension(new DndExtension(this));
4096
+ this._registerExtension(new GridExtension(this));
3836
4097
  this._registerExtension(new LoggerExtension(this));
3837
4098
  // --- Evaluate options
3838
4099
  this.columns = opts.columns;
@@ -3926,11 +4187,9 @@ class Wunderbaum {
3926
4187
  var _a;
3927
4188
  (_a = this.element.querySelector("progress.spinner")) === null || _a === void 0 ? void 0 : _a.remove();
3928
4189
  this.element.classList.remove("wb-initializing");
3929
- // this.updateViewport();
3930
4190
  });
3931
4191
  }
3932
4192
  else {
3933
- // this.updateViewport();
3934
4193
  readyDeferred.resolve();
3935
4194
  }
3936
4195
  // TODO: This is sometimes required, because this.element.clientWidth
@@ -3940,13 +4199,10 @@ class Wunderbaum {
3940
4199
  }, 50);
3941
4200
  // --- Bind listeners
3942
4201
  this.scrollContainer.addEventListener("scroll", (e) => {
3943
- this.updateViewport();
4202
+ this.setModified(ChangeType.vscroll);
3944
4203
  });
3945
- // window.addEventListener("resize", (e: Event) => {
3946
- // this.updateViewport();
3947
- // });
3948
4204
  this.resizeObserver = new ResizeObserver((entries) => {
3949
- this.updateViewport();
4205
+ this.setModified(ChangeType.vscroll);
3950
4206
  console.log("ResizeObserver: Size changed", entries);
3951
4207
  });
3952
4208
  this.resizeObserver.observe(this.element);
@@ -4003,37 +4259,18 @@ class Wunderbaum {
4003
4259
  forceClose: true,
4004
4260
  });
4005
4261
  }
4006
- // if (flag && !this.activeNode ) {
4007
- // setTimeout(() => {
4008
- // if (!this.activeNode) {
4009
- // const firstNode = this.getFirstChild();
4010
- // if (firstNode && !firstNode?.isStatusNode()) {
4011
- // firstNode.logInfo("Activate on focus", e);
4012
- // firstNode.setActive(true, { event: e });
4013
- // }
4014
- // }
4015
- // }, 10);
4016
- // }
4017
4262
  });
4018
4263
  }
4019
- /** */
4020
- // _renderHeader(){
4021
- // const coldivs = "<span class='wb-col'></span>".repeat(this.columns.length);
4022
- // this.element.innerHTML = `
4023
- // <div class='wb-header'>
4024
- // <div class='wb-row'>
4025
- // ${coldivs}
4026
- // </div>
4027
- // </div>`;
4028
- // }
4029
- /** Return a Wunderbaum instance, from element, index, or event.
4264
+ /**
4265
+ * Return a Wunderbaum instance, from element, id, index, or event.
4030
4266
  *
4031
- * @example
4032
- * getTree(); // Get first Wunderbaum instance on page
4033
- * getTree(1); // Get second Wunderbaum instance on page
4034
- * getTree(event); // Get tree for this mouse- or keyboard event
4035
- * getTree("foo"); // Get tree for this `tree.options.id`
4267
+ * ```js
4268
+ * getTree(); // Get first Wunderbaum instance on page
4269
+ * getTree(1); // Get second Wunderbaum instance on page
4270
+ * getTree(event); // Get tree for this mouse- or keyboard event
4271
+ * getTree("foo"); // Get tree for this `tree.options.id`
4036
4272
  * getTree("#tree"); // Get tree for this matching element
4273
+ * ```
4037
4274
  */
4038
4275
  static getTree(el) {
4039
4276
  if (el instanceof Wunderbaum) {
@@ -4074,9 +4311,8 @@ class Wunderbaum {
4074
4311
  }
4075
4312
  return null;
4076
4313
  }
4077
- /** Return a WunderbaumNode instance from element, event.
4078
- *
4079
- * @param el
4314
+ /**
4315
+ * Return a WunderbaumNode instance from element or event.
4080
4316
  */
4081
4317
  static getNode(el) {
4082
4318
  if (!el) {
@@ -4098,7 +4334,7 @@ class Wunderbaum {
4098
4334
  }
4099
4335
  return null;
4100
4336
  }
4101
- /** */
4337
+ /** @internal */
4102
4338
  _registerExtension(extension) {
4103
4339
  this.extensionList.push(extension);
4104
4340
  this.extensions[extension.id] = extension;
@@ -4140,7 +4376,7 @@ class Wunderbaum {
4140
4376
  node.tree = null;
4141
4377
  node.parent = null;
4142
4378
  // node.title = "DISPOSED: " + node.title
4143
- this.viewNodes.delete(node);
4379
+ // this.viewNodes.delete(node);
4144
4380
  node.removeMarkup();
4145
4381
  }
4146
4382
  /** Call all hook methods of all registered extensions.*/
@@ -4158,7 +4394,9 @@ class Wunderbaum {
4158
4394
  }
4159
4395
  return res;
4160
4396
  }
4161
- /** Call tree method or extension method if defined.
4397
+ /**
4398
+ * Call tree method or extension method if defined.
4399
+ *
4162
4400
  * Example:
4163
4401
  * ```js
4164
4402
  * tree._callMethod("edit.startEdit", "arg1", "arg2")
@@ -4175,7 +4413,9 @@ class Wunderbaum {
4175
4413
  this.logError(`Calling undefined method '${name}()'.`);
4176
4414
  }
4177
4415
  }
4178
- /** Call event handler if defined in tree.options.
4416
+ /**
4417
+ * Call event handler if defined in tree or tree.EXTENSION options.
4418
+ *
4179
4419
  * Example:
4180
4420
  * ```js
4181
4421
  * tree._callEvent("edit.beforeEdit", {foo: 42})
@@ -4191,27 +4431,33 @@ class Wunderbaum {
4191
4431
  // this.logError(`Triggering undefined event '${name}'.`)
4192
4432
  }
4193
4433
  }
4194
- /** Return the topmost visible node in the viewport */
4195
- _firstNodeInView(complete = true) {
4196
- let topIdx, node;
4197
- if (complete) {
4198
- topIdx = Math.ceil(this.scrollContainer.scrollTop / ROW_HEIGHT);
4199
- }
4200
- else {
4201
- topIdx = Math.floor(this.scrollContainer.scrollTop / ROW_HEIGHT);
4202
- }
4434
+ /** Return the node for given row index. */
4435
+ _getNodeByRowIdx(idx) {
4203
4436
  // TODO: start searching from active node (reverse)
4437
+ let node = null;
4204
4438
  this.visitRows((n) => {
4205
- if (n._rowIdx === topIdx) {
4439
+ if (n._rowIdx === idx) {
4206
4440
  node = n;
4207
4441
  return false;
4208
4442
  }
4209
4443
  });
4210
4444
  return node;
4211
4445
  }
4212
- /** Return the lowest visible node in the viewport */
4446
+ /** Return the topmost visible node in the viewport. */
4447
+ _firstNodeInView(complete = true) {
4448
+ let topIdx;
4449
+ const gracePy = 1; // ignore subpixel scrolling
4450
+ if (complete) {
4451
+ topIdx = Math.ceil((this.scrollContainer.scrollTop - gracePy) / ROW_HEIGHT);
4452
+ }
4453
+ else {
4454
+ topIdx = Math.floor(this.scrollContainer.scrollTop / ROW_HEIGHT);
4455
+ }
4456
+ return this._getNodeByRowIdx(topIdx);
4457
+ }
4458
+ /** Return the lowest visible node in the viewport. */
4213
4459
  _lastNodeInView(complete = true) {
4214
- let bottomIdx, node;
4460
+ let bottomIdx;
4215
4461
  if (complete) {
4216
4462
  bottomIdx =
4217
4463
  Math.floor((this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
@@ -4222,16 +4468,10 @@ class Wunderbaum {
4222
4468
  Math.ceil((this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
4223
4469
  ROW_HEIGHT) - 1;
4224
4470
  }
4225
- // TODO: start searching from active node
4226
- this.visitRows((n) => {
4227
- if (n._rowIdx === bottomIdx) {
4228
- node = n;
4229
- return false;
4230
- }
4231
- });
4232
- return node;
4471
+ bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
4472
+ return this._getNodeByRowIdx(bottomIdx);
4233
4473
  }
4234
- /** Return preceeding visible node in the viewport */
4474
+ /** Return preceeding visible node in the viewport. */
4235
4475
  _getPrevNodeInView(node, ofs = 1) {
4236
4476
  this.visitRows((n) => {
4237
4477
  node = n;
@@ -4241,7 +4481,7 @@ class Wunderbaum {
4241
4481
  }, { reverse: true, start: node || this.getActiveNode() });
4242
4482
  return node;
4243
4483
  }
4244
- /** Return following visible node in the viewport */
4484
+ /** Return following visible node in the viewport. */
4245
4485
  _getNextNodeInView(node, ofs = 1) {
4246
4486
  this.visitRows((n) => {
4247
4487
  node = n;
@@ -4251,10 +4491,15 @@ class Wunderbaum {
4251
4491
  }, { reverse: false, start: node || this.getActiveNode() });
4252
4492
  return node;
4253
4493
  }
4494
+ /**
4495
+ * Append (or insert) a list of toplevel nodes.
4496
+ *
4497
+ * @see {@link WunderbaumNode.addChildren}
4498
+ */
4254
4499
  addChildren(nodeData, options) {
4255
4500
  return this.root.addChildren(nodeData, options);
4256
4501
  }
4257
- /*
4502
+ /**
4258
4503
  * Apply a modification or navigation operation.
4259
4504
  *
4260
4505
  * Most of these commands simply map to a node or tree method.
@@ -4379,16 +4624,17 @@ class Wunderbaum {
4379
4624
  this.root.children = null;
4380
4625
  this.keyMap.clear();
4381
4626
  this.refKeyMap.clear();
4382
- this.viewNodes.clear();
4627
+ // this.viewNodes.clear();
4628
+ this.treeRowCount = 0;
4383
4629
  this.activeNode = null;
4384
4630
  this.focusNode = null;
4385
4631
  // this.types = {};
4386
4632
  // this. columns =[];
4387
4633
  // this._columnsById = {};
4388
4634
  // Modification Status
4389
- this.changedSince = 0;
4390
- this.changes.clear();
4391
- this.changedNodes.clear();
4635
+ // this.changedSince = 0;
4636
+ // this.changes.clear();
4637
+ // this.changedNodes.clear();
4392
4638
  // // --- FILTER ---
4393
4639
  // public filterMode: FilterModeType = null;
4394
4640
  // // --- KEYNAV ---
@@ -4396,7 +4642,7 @@ class Wunderbaum {
4396
4642
  // public cellNavMode = false;
4397
4643
  // public lastQuicksearchTime = 0;
4398
4644
  // public lastQuicksearchTerm = "";
4399
- this.updateViewport();
4645
+ this.setModified(ChangeType.structure);
4400
4646
  }
4401
4647
  /**
4402
4648
  * Clear nodes and markup and detach events and observers.
@@ -4416,10 +4662,11 @@ class Wunderbaum {
4416
4662
  /**
4417
4663
  * Return `tree.option.NAME` (also resolving if this is a callback).
4418
4664
  *
4419
- * See also [[WunderbaumNode.getOption()]] to consider `node.NAME` setting and
4420
- * `tree.types[node.type].NAME`.
4665
+ * See also {@link WunderbaumNode.getOption|WunderbaumNode.getOption()}
4666
+ * to consider `node.NAME` setting and `tree.types[node.type].NAME`.
4421
4667
  *
4422
- * @param name option name (use dot notation to access extension option, e.g. `filter.mode`)
4668
+ * @param name option name (use dot notation to access extension option, e.g.
4669
+ * `filter.mode`)
4423
4670
  */
4424
4671
  getOption(name, defaultValue) {
4425
4672
  let ext;
@@ -4463,18 +4710,14 @@ class Wunderbaum {
4463
4710
  }
4464
4711
  /** Run code, but defer `updateViewport()` until done. */
4465
4712
  runWithoutUpdate(func, hint = null) {
4466
- // const prev = this._disableUpdate;
4467
- // const start = Date.now();
4468
- // this._disableUpdate = Date.now();
4469
4713
  try {
4470
4714
  this.enableUpdate(false);
4471
- return func();
4715
+ const res = func();
4716
+ assert(!(res instanceof Promise));
4717
+ return res;
4472
4718
  }
4473
4719
  finally {
4474
4720
  this.enableUpdate(true);
4475
- // if (!prev && this._disableUpdate === start) {
4476
- // this._disableUpdate = 0;
4477
- // }
4478
4721
  }
4479
4722
  }
4480
4723
  /** Recursively expand all expandable nodes (triggers lazy load id needed). */
@@ -4492,11 +4735,12 @@ class Wunderbaum {
4492
4735
  /** Return the number of nodes in the data model.*/
4493
4736
  count(visible = false) {
4494
4737
  if (visible) {
4495
- return this.viewNodes.size;
4738
+ return this.treeRowCount;
4739
+ // return this.viewNodes.size;
4496
4740
  }
4497
4741
  return this.keyMap.size;
4498
4742
  }
4499
- /* Internal sanity check. */
4743
+ /** @internal sanity check. */
4500
4744
  _check() {
4501
4745
  let i = 0;
4502
4746
  this.visit((n) => {
@@ -4507,25 +4751,30 @@ class Wunderbaum {
4507
4751
  }
4508
4752
  // util.assert(this.keyMap.size === i);
4509
4753
  }
4510
- /**Find all nodes that matches condition.
4754
+ /**
4755
+ * Find all nodes that matches condition.
4511
4756
  *
4512
4757
  * @param match title string to search for, or a
4513
4758
  * callback function that returns `true` if a node is matched.
4514
- * @see [[WunderbaumNode.findAll]]
4759
+ *
4760
+ * @see {@link WunderbaumNode.findAll}
4515
4761
  */
4516
4762
  findAll(match) {
4517
4763
  return this.root.findAll(match);
4518
4764
  }
4519
- /**Find first node that matches condition.
4765
+ /**
4766
+ * Find first node that matches condition.
4520
4767
  *
4521
4768
  * @param match title string to search for, or a
4522
4769
  * callback function that returns `true` if a node is matched.
4523
- * @see [[WunderbaumNode.findFirst]]
4770
+ * @see {@link WunderbaumNode.findFirst}
4771
+ *
4524
4772
  */
4525
4773
  findFirst(match) {
4526
4774
  return this.root.findFirst(match);
4527
4775
  }
4528
- /** Find the next visible node that starts with `match`, starting at `startNode`
4776
+ /**
4777
+ * Find the next visible node that starts with `match`, starting at `startNode`
4529
4778
  * and wrap-around at the end.
4530
4779
  */
4531
4780
  findNextNode(match, startNode) {
@@ -4555,7 +4804,8 @@ class Wunderbaum {
4555
4804
  }
4556
4805
  return res;
4557
4806
  }
4558
- /** Find a node relative to another node.
4807
+ /**
4808
+ * Find a node relative to another node.
4559
4809
  *
4560
4810
  * @param node
4561
4811
  * @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'.
@@ -4565,7 +4815,7 @@ class Wunderbaum {
4565
4815
  */
4566
4816
  findRelatedNode(node, where, includeHidden = false) {
4567
4817
  let res = null;
4568
- let pageSize = Math.floor(this.scrollContainer.clientHeight / ROW_HEIGHT);
4818
+ const pageSize = Math.floor(this.scrollContainer.clientHeight / ROW_HEIGHT);
4569
4819
  switch (where) {
4570
4820
  case "parent":
4571
4821
  if (node.parent && node.parent.parent) {
@@ -4621,9 +4871,9 @@ class Wunderbaum {
4621
4871
  res = this._getNextNodeInView(node);
4622
4872
  break;
4623
4873
  case "pageDown":
4624
- let bottomNode = this._lastNodeInView();
4625
- // this.logDebug(where, this.focusNode, bottomNode);
4626
- if (this.focusNode !== bottomNode) {
4874
+ const bottomNode = this._lastNodeInView();
4875
+ // this.logDebug(`${where}(${node}) -> ${bottomNode}`);
4876
+ if (node._rowIdx < bottomNode._rowIdx) {
4627
4877
  res = bottomNode;
4628
4878
  }
4629
4879
  else {
@@ -4631,12 +4881,13 @@ class Wunderbaum {
4631
4881
  }
4632
4882
  break;
4633
4883
  case "pageUp":
4634
- if (this.focusNode && this.focusNode._rowIdx === 0) {
4635
- res = this.focusNode;
4884
+ if (node._rowIdx === 0) {
4885
+ res = node;
4636
4886
  }
4637
4887
  else {
4638
- let topNode = this._firstNodeInView();
4639
- if (this.focusNode !== topNode) {
4888
+ const topNode = this._firstNodeInView();
4889
+ // this.logDebug(`${where}(${node}) -> ${topNode}`);
4890
+ if (node._rowIdx > topNode._rowIdx) {
4640
4891
  res = topNode;
4641
4892
  }
4642
4893
  else {
@@ -4650,7 +4901,7 @@ class Wunderbaum {
4650
4901
  return res;
4651
4902
  }
4652
4903
  /**
4653
- * Return the active cell of the currently active node or null.
4904
+ * Return the active cell (`span.wb-col`) of the currently active node or null.
4654
4905
  */
4655
4906
  getActiveColElem() {
4656
4907
  if (this.activeNode && this.activeColIdx >= 0) {
@@ -4664,8 +4915,8 @@ class Wunderbaum {
4664
4915
  getActiveNode() {
4665
4916
  return this.activeNode;
4666
4917
  }
4667
- /** Return the first top level node if any (not the invisible root node).
4668
- * @returns {FancytreeNode | null}
4918
+ /**
4919
+ * Return the first top level node if any (not the invisible root node).
4669
4920
  */
4670
4921
  getFirstChild() {
4671
4922
  return this.root.getFirstChild();
@@ -4676,14 +4927,15 @@ class Wunderbaum {
4676
4927
  getFocusNode() {
4677
4928
  return this.focusNode;
4678
4929
  }
4679
- /** Return a {node: FancytreeNode, region: TYPE} object for a mouse event.
4930
+ /** Return a {node: WunderbaumNode, region: TYPE} object for a mouse event.
4680
4931
  *
4681
4932
  * @param {Event} event Mouse event, e.g. click, ...
4682
- * @returns {object} Return a {node: FancytreeNode, region: TYPE} object
4933
+ * @returns {object} Return a {node: WunderbaumNode, region: TYPE} object
4683
4934
  * TYPE: 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined
4684
4935
  */
4685
4936
  static getEventInfo(event) {
4686
- let target = event.target, cl = target.classList, parentCol = target.closest(".wb-col"), node = Wunderbaum.getNode(target), res = {
4937
+ let target = event.target, cl = target.classList, parentCol = target.closest("span.wb-col"), node = Wunderbaum.getNode(target), tree = node ? node.tree : Wunderbaum.getTree(event), res = {
4938
+ tree: tree,
4687
4939
  node: node,
4688
4940
  region: TargetType.unknown,
4689
4941
  colDef: undefined,
@@ -4715,13 +4967,15 @@ class Wunderbaum {
4715
4967
  }
4716
4968
  else {
4717
4969
  // Somewhere near the title
4718
- console.warn("getEventInfo(): not found", event, res);
4970
+ if (event.type !== "mousemove" && !(event instanceof KeyboardEvent)) {
4971
+ console.warn("getEventInfo(): not found", event, res);
4972
+ }
4719
4973
  return res;
4720
4974
  }
4721
4975
  if (res.colIdx === -1) {
4722
4976
  res.colIdx = 0;
4723
4977
  }
4724
- res.colDef = node.tree.columns[res.colIdx];
4978
+ res.colDef = tree === null || tree === void 0 ? void 0 : tree.columns[res.colIdx];
4725
4979
  res.colDef != null ? (res.colId = res.colDef.id) : 0;
4726
4980
  // this.log("Event", event, res);
4727
4981
  return res;
@@ -4745,7 +4999,8 @@ class Wunderbaum {
4745
4999
  isEditing() {
4746
5000
  return this._callMethod("edit.isEditingTitle");
4747
5001
  }
4748
- /** Return true if any node is currently beeing loaded, i.e. a Ajax request is pending.
5002
+ /**
5003
+ * Return true if any node is currently beeing loaded, i.e. a Ajax request is pending.
4749
5004
  */
4750
5005
  isLoading() {
4751
5006
  var res = false;
@@ -4772,7 +5027,7 @@ class Wunderbaum {
4772
5027
  console.error.apply(console, args);
4773
5028
  }
4774
5029
  }
4775
- /* Log to console if opts.debugLevel >= 3 */
5030
+ /** Log to console if opts.debugLevel >= 3 */
4776
5031
  logInfo(...args) {
4777
5032
  if (this.options.debugLevel >= 3) {
4778
5033
  Array.prototype.unshift.call(args, this.toString());
@@ -4799,77 +5054,13 @@ class Wunderbaum {
4799
5054
  console.warn.apply(console, args);
4800
5055
  }
4801
5056
  }
4802
- /** */
4803
- render(opts) {
4804
- const label = this.logTime("render");
4805
- let idx = 0;
4806
- let top = 0;
4807
- const height = ROW_HEIGHT;
4808
- let modified = false;
4809
- let start = opts === null || opts === void 0 ? void 0 : opts.startIdx;
4810
- let end = opts === null || opts === void 0 ? void 0 : opts.endIdx;
4811
- const obsoleteViewNodes = this.viewNodes;
4812
- this.viewNodes = new Set();
4813
- let viewNodes = this.viewNodes;
4814
- // this.debug("render", opts);
4815
- assert(start != null && end != null);
4816
- // Make sure start is always even, so the alternating row colors don't
4817
- // change when scrolling:
4818
- if (start % 2) {
4819
- start--;
4820
- }
4821
- this.visitRows(function (node) {
4822
- const prevIdx = node._rowIdx;
4823
- viewNodes.add(node);
4824
- obsoleteViewNodes.delete(node);
4825
- if (prevIdx !== idx) {
4826
- node._rowIdx = idx;
4827
- modified = true;
4828
- }
4829
- if (idx < start || idx > end) {
4830
- node._callEvent("discard");
4831
- node.removeMarkup();
4832
- }
4833
- else {
4834
- // if (!node._rowElem || prevIdx != idx) {
4835
- node.render({ top: top });
4836
- }
4837
- idx++;
4838
- top += height;
4839
- });
4840
- for (const prevNode of obsoleteViewNodes) {
4841
- prevNode._callEvent("discard");
4842
- prevNode.removeMarkup();
4843
- }
4844
- // Resize tree container
4845
- this.nodeListElement.style.height = "" + top + "px";
4846
- // this.log("render()", this.nodeListElement.style.height);
4847
- this.logTimeEnd(label);
4848
- return modified;
4849
- }
4850
- /**Recalc and apply header columns from `this.columns`. */
4851
- renderHeader() {
4852
- if (!this.headerElement) {
4853
- return;
4854
- }
4855
- const headerRow = this.headerElement.querySelector(".wb-row");
4856
- assert(headerRow);
4857
- headerRow.innerHTML = "<span class='wb-col'></span>".repeat(this.columns.length);
4858
- for (let i = 0; i < this.columns.length; i++) {
4859
- let col = this.columns[i];
4860
- let colElem = headerRow.children[i];
4861
- colElem.style.left = col._ofsPx + "px";
4862
- colElem.style.width = col._widthPx + "px";
4863
- colElem.textContent = col.title || col.id;
4864
- }
4865
- }
4866
5057
  /**
5058
+ * Make sure that this node is scrolled into the viewport.
4867
5059
  *
4868
5060
  * @param {boolean | PlainObject} [effects=false] animation options.
4869
5061
  * @param {object} [options=null] {topNode: null, effects: ..., parent: ...}
4870
5062
  * this node will remain visible in
4871
5063
  * any case, even if `this` is outside the scroll pane.
4872
- * Make sure that a node is scrolled into the viewport.
4873
5064
  */
4874
5065
  scrollTo(opts) {
4875
5066
  const MARGIN = 1;
@@ -4890,30 +5081,18 @@ class Wunderbaum {
4890
5081
  // Node is above viewport
4891
5082
  newTop = nodeOfs + MARGIN;
4892
5083
  }
4893
- this.log("scrollTo(" + nodeOfs + "): " + curTop + " => " + newTop, height);
4894
5084
  if (newTop != null) {
5085
+ this.log("scrollTo(" + nodeOfs + "): " + curTop + " => " + newTop, height);
4895
5086
  this.scrollContainer.scrollTop = newTop;
4896
- this.updateViewport();
4897
- }
4898
- }
4899
- /** */
4900
- setCellMode(mode) {
4901
- // util.assert(this.cellNavMode);
4902
- // util.assert(0 <= colIdx && colIdx < this.columns.length);
4903
- if (mode === this.navMode) {
4904
- return;
4905
- }
4906
- const prevMode = this.navMode;
4907
- const cellMode = mode !== NavigationMode.row;
4908
- this.navMode = mode;
4909
- if (cellMode && prevMode === NavigationMode.row) {
4910
- this.setColumn(0);
5087
+ this.setModified(ChangeType.vscroll);
4911
5088
  }
4912
- this.element.classList.toggle("wb-cell-mode", cellMode);
4913
- this.element.classList.toggle("wb-cell-edit-mode", mode === NavigationMode.cellEdit);
4914
- this.setModified(ChangeType.row, this.activeNode);
4915
5089
  }
4916
- /** */
5090
+ /**
5091
+ * Set column #colIdx to 'active'.
5092
+ *
5093
+ * This higlights the column header and -cells by adding the `wb-active` class.
5094
+ * Available in cell-nav and cell-edit mode, not in row-mode.
5095
+ */
4917
5096
  setColumn(colIdx) {
4918
5097
  assert(this.navMode !== NavigationMode.row);
4919
5098
  assert(0 <= colIdx && colIdx < this.columns.length);
@@ -4938,7 +5117,7 @@ class Wunderbaum {
4938
5117
  }
4939
5118
  }
4940
5119
  }
4941
- /** */
5120
+ /** Set or remove keybaord focus to the tree container. */
4942
5121
  setFocus(flag = true) {
4943
5122
  if (flag) {
4944
5123
  this.element.focus();
@@ -4947,88 +5126,145 @@ class Wunderbaum {
4947
5126
  this.element.blur();
4948
5127
  }
4949
5128
  }
4950
- /** */
4951
5129
  setModified(change, node, options) {
4952
- if (!this.changedSince) {
4953
- this.changedSince = Date.now();
5130
+ if (this._disableUpdateCount) {
5131
+ // Assuming that we redraw all when enableUpdate() is re-enabled.
5132
+ // this.log(
5133
+ // `IGNORED setModified(${change}) node=${node} (disable level ${this._disableUpdateCount})`
5134
+ // );
5135
+ return;
4954
5136
  }
4955
- this.changes.add(change);
4956
- if (change === ChangeType.structure) {
4957
- this.changedNodes.clear();
5137
+ // this.log(`setModified(${change}) node=${node}`);
5138
+ if (!(node instanceof WunderbaumNode)) {
5139
+ options = node;
5140
+ }
5141
+ const immediate = !!getOption(options, "immediate");
5142
+ switch (change) {
5143
+ case ChangeType.any:
5144
+ case ChangeType.structure:
5145
+ case ChangeType.header:
5146
+ this.changeRedrawRequestPending = true;
5147
+ this.updateViewport(immediate);
5148
+ break;
5149
+ case ChangeType.vscroll:
5150
+ this.updateViewport(immediate);
5151
+ break;
5152
+ case ChangeType.row:
5153
+ case ChangeType.status:
5154
+ // Single nodes are immedialtely updated if already inside the viewport
5155
+ // (otherwise we can ignore)
5156
+ if (node._rowElem) {
5157
+ node.render();
5158
+ }
5159
+ break;
5160
+ default:
5161
+ error(`Invalid change type ${change}`);
4958
5162
  }
4959
- else if (node && !this.changes.has(ChangeType.structure)) {
4960
- if (this.changedNodes.size < MAX_CHANGED_NODES) {
4961
- this.changedNodes.add(node);
4962
- }
4963
- else {
4964
- this.changes.add(ChangeType.structure);
4965
- this.changedNodes.clear();
4966
- }
5163
+ }
5164
+ /** Set the tree's navigation mode. */
5165
+ setNavigationMode(mode) {
5166
+ // util.assert(this.cellNavMode);
5167
+ // util.assert(0 <= colIdx && colIdx < this.columns.length);
5168
+ if (mode === this.navMode) {
5169
+ return;
4967
5170
  }
4968
- // this.log("setModified(" + change + ")", node);
5171
+ const prevMode = this.navMode;
5172
+ const cellMode = mode !== NavigationMode.row;
5173
+ this.navMode = mode;
5174
+ if (cellMode && prevMode === NavigationMode.row) {
5175
+ this.setColumn(0);
5176
+ }
5177
+ this.element.classList.toggle("wb-cell-mode", cellMode);
5178
+ this.element.classList.toggle("wb-cell-edit-mode", mode === NavigationMode.cellEdit);
5179
+ this.setModified(ChangeType.row, this.activeNode);
4969
5180
  }
5181
+ /** Display tree status (ok, loading, error, noData) using styles and a dummy root node. */
4970
5182
  setStatus(status, message, details) {
4971
5183
  return this.root.setStatus(status, message, details);
4972
5184
  }
4973
5185
  /** Update column headers and width. */
4974
5186
  updateColumns(opts) {
4975
- let modified = false;
4976
- let minWidth = 4;
4977
- let vpWidth = this.element.clientWidth;
5187
+ opts = Object.assign({ calculateCols: true, updateRows: true }, opts);
5188
+ const minWidth = 4;
5189
+ const vpWidth = this.element.clientWidth;
4978
5190
  let totalWeight = 0;
4979
5191
  let fixedWidth = 0;
4980
- // Gather width requests
4981
- this._columnsById = {};
4982
- for (let col of this.columns) {
4983
- this._columnsById[col.id] = col;
4984
- let cw = col.width;
4985
- if (!cw || cw === "*") {
4986
- col._weight = 1.0;
4987
- totalWeight += 1.0;
4988
- }
4989
- else if (typeof cw === "number") {
4990
- col._weight = cw;
4991
- totalWeight += cw;
4992
- }
4993
- else if (typeof cw === "string" && cw.endsWith("px")) {
4994
- col._weight = 0;
4995
- let px = parseFloat(cw.slice(0, -2));
4996
- if (col._widthPx != px) {
4997
- modified = true;
4998
- col._widthPx = px;
5192
+ let modified = false;
5193
+ if (opts.calculateCols) {
5194
+ // Gather width requests
5195
+ this._columnsById = {};
5196
+ for (let col of this.columns) {
5197
+ this._columnsById[col.id] = col;
5198
+ let cw = col.width;
5199
+ if (!cw || cw === "*") {
5200
+ col._weight = 1.0;
5201
+ totalWeight += 1.0;
5202
+ }
5203
+ else if (typeof cw === "number") {
5204
+ col._weight = cw;
5205
+ totalWeight += cw;
5206
+ }
5207
+ else if (typeof cw === "string" && cw.endsWith("px")) {
5208
+ col._weight = 0;
5209
+ let px = parseFloat(cw.slice(0, -2));
5210
+ if (col._widthPx != px) {
5211
+ modified = true;
5212
+ col._widthPx = px;
5213
+ }
5214
+ fixedWidth += px;
5215
+ }
5216
+ else {
5217
+ error("Invalid column width: " + cw);
4999
5218
  }
5000
- fixedWidth += px;
5001
5219
  }
5002
- else {
5003
- error("Invalid column width: " + cw);
5004
- }
5005
- }
5006
- // Share remaining space between non-fixed columns
5007
- let restPx = Math.max(0, vpWidth - fixedWidth);
5008
- let ofsPx = 0;
5009
- for (let col of this.columns) {
5010
- if (col._weight) {
5011
- let px = Math.max(minWidth, (restPx * col._weight) / totalWeight);
5012
- if (col._widthPx != px) {
5013
- modified = true;
5014
- col._widthPx = px;
5220
+ // Share remaining space between non-fixed columns
5221
+ const restPx = Math.max(0, vpWidth - fixedWidth);
5222
+ let ofsPx = 0;
5223
+ for (let col of this.columns) {
5224
+ if (col._weight) {
5225
+ const px = Math.max(minWidth, (restPx * col._weight) / totalWeight);
5226
+ if (col._widthPx != px) {
5227
+ modified = true;
5228
+ col._widthPx = px;
5229
+ }
5015
5230
  }
5231
+ col._ofsPx = ofsPx;
5232
+ ofsPx += col._widthPx;
5016
5233
  }
5017
- col._ofsPx = ofsPx;
5018
- ofsPx += col._widthPx;
5019
5234
  }
5020
5235
  // Every column has now a calculated `_ofsPx` and `_widthPx`
5021
5236
  // this.logInfo("UC", this.columns, vpWidth, this.element.clientWidth, this.element);
5022
5237
  // console.trace();
5023
5238
  // util.error("BREAK");
5024
5239
  if (modified) {
5025
- this.renderHeader();
5026
- if (opts.render !== false) {
5027
- this.render();
5240
+ this._renderHeaderMarkup();
5241
+ if (opts.updateRows) {
5242
+ this._updateRows();
5028
5243
  }
5029
5244
  }
5030
5245
  }
5031
- /** Render all rows that are visible in the viewport. */
5246
+ /** Create/update header markup from `this.columns` definition.
5247
+ * @internal
5248
+ */
5249
+ _renderHeaderMarkup() {
5250
+ if (!this.headerElement) {
5251
+ return;
5252
+ }
5253
+ const headerRow = this.headerElement.querySelector(".wb-row");
5254
+ assert(headerRow);
5255
+ headerRow.innerHTML = "<span class='wb-col'></span>".repeat(this.columns.length);
5256
+ for (let i = 0; i < this.columns.length; i++) {
5257
+ const col = this.columns[i];
5258
+ const colElem = headerRow.children[i];
5259
+ colElem.style.left = col._ofsPx + "px";
5260
+ colElem.style.width = col._widthPx + "px";
5261
+ // colElem.textContent = col.title || col.id;
5262
+ const title = escapeHtml(col.title || col.id);
5263
+ colElem.innerHTML = `<span class="wb-col-title">${title}</span> <span class="wb-col-resizer"></span>`;
5264
+ // colElem.innerHTML = `${title} <span class="wb-col-resizer"></span>`;
5265
+ }
5266
+ }
5267
+ /** Render header and all rows that are visible in the viewport (async, throttled). */
5032
5268
  updateViewport(immediate = false) {
5033
5269
  // Call the `throttle` wrapper for `this._updateViewport()` which will
5034
5270
  // execute immediately on the leading edge of a sequence:
@@ -5037,39 +5273,163 @@ class Wunderbaum {
5037
5273
  this._updateViewportThrottled.flush();
5038
5274
  }
5039
5275
  }
5276
+ /**
5277
+ * This is the actual update method, which is wrapped inside a throttle method.
5278
+ * This protected method should not be called directly but via
5279
+ * `tree.updateViewport()` or `tree.setModified()`.
5280
+ * It calls `updateColumns()` and `_updateRows()`.
5281
+ * @internal
5282
+ */
5040
5283
  _updateViewport() {
5041
- if (this._disableUpdate) {
5284
+ if (this._disableUpdateCount) {
5285
+ this.log(`IGNORED _updateViewport() disable level: ${this._disableUpdateCount}`);
5042
5286
  return;
5043
5287
  }
5288
+ const newNodesOnly = !this.changeRedrawRequestPending;
5289
+ this.changeRedrawRequestPending = false;
5044
5290
  let height = this.scrollContainer.clientHeight;
5045
- // We cannot get the height for abolut positioned parent, so look at first col
5291
+ // We cannot get the height for absolute positioned parent, so look at first col
5046
5292
  // let headerHeight = this.headerElement.clientHeight
5047
5293
  // let headerHeight = this.headerElement.children[0].children[0].clientHeight;
5048
5294
  const headerHeight = this.options.headerHeightPx;
5049
- let wantHeight = this.element.clientHeight - headerHeight;
5050
- let ofs = this.scrollContainer.scrollTop;
5295
+ const wantHeight = this.element.clientHeight - headerHeight;
5051
5296
  if (Math.abs(height - wantHeight) > 1.0) {
5052
5297
  // this.log("resize", height, wantHeight);
5053
5298
  this.scrollContainer.style.height = wantHeight + "px";
5054
5299
  height = wantHeight;
5055
5300
  }
5056
- this.updateColumns({ render: false });
5057
- this.render({
5058
- startIdx: Math.max(0, ofs / ROW_HEIGHT - RENDER_MAX_PREFETCH),
5059
- endIdx: Math.max(0, (ofs + height) / ROW_HEIGHT + RENDER_MAX_PREFETCH),
5060
- });
5301
+ this.updateColumns({ updateRows: false });
5302
+ this._updateRows({ newNodesOnly: newNodesOnly });
5061
5303
  this._callEvent("update");
5062
5304
  }
5063
- /** Call callback(node) for all nodes in hierarchical order (depth-first).
5305
+ /**
5306
+ * Assert that TR order matches the natural node order
5307
+ * @internal
5308
+ */
5309
+ _validateRows() {
5310
+ let trs = this.nodeListElement.childNodes;
5311
+ let i = 0;
5312
+ let prev = -1;
5313
+ let ok = true;
5314
+ trs.forEach((element) => {
5315
+ const tr = element;
5316
+ const top = Number.parseInt(tr.style.top);
5317
+ const n = tr._wb_node;
5318
+ // if (i < 4) {
5319
+ // console.info(
5320
+ // `TR#${i}, rowIdx=${n._rowIdx} , top=${top}px: '${n.title}'`
5321
+ // );
5322
+ // }
5323
+ if (top <= prev) {
5324
+ console.warn(`TR order mismatch at index ${i}: top=${top}px, node=${n}`);
5325
+ // throw new Error("fault");
5326
+ ok = false;
5327
+ }
5328
+ prev = top;
5329
+ i++;
5330
+ });
5331
+ return ok;
5332
+ }
5333
+ /*
5334
+ * - Traverse all *visible* of the whole tree, i.e. skip collapsed nodes.
5335
+ * - Store count of rows to `tree.treeRowCount`.
5336
+ * - Renumber `node._rowIdx` for all visible nodes.
5337
+ * - Calculate the index range that must be rendered to fill the viewport
5338
+ * (including upper and lower prefetch)
5339
+ * -
5340
+ */
5341
+ _updateRows(opts) {
5342
+ const label = this.logTime("_updateRows");
5343
+ opts = Object.assign({ newNodesOnly: false }, opts);
5344
+ const newNodesOnly = !!opts.newNodesOnly;
5345
+ const row_height = ROW_HEIGHT;
5346
+ const vp_height = this.scrollContainer.clientHeight;
5347
+ const prefetch = RENDER_MAX_PREFETCH;
5348
+ const ofs = this.scrollContainer.scrollTop;
5349
+ let startIdx = Math.max(0, ofs / row_height - prefetch);
5350
+ startIdx = Math.floor(startIdx);
5351
+ // Make sure start is always even, so the alternating row colors don't
5352
+ // change when scrolling:
5353
+ if (startIdx % 2) {
5354
+ startIdx--;
5355
+ }
5356
+ let endIdx = Math.max(0, (ofs + vp_height) / row_height + prefetch);
5357
+ endIdx = Math.ceil(endIdx);
5358
+ // const obsoleteViewNodes = this.viewNodes;
5359
+ // this.viewNodes = new Set();
5360
+ // const viewNodes = this.viewNodes;
5361
+ // this.debug("render", opts);
5362
+ const obsoleteNodes = new Set();
5363
+ this.nodeListElement.childNodes.forEach((elem) => {
5364
+ const tr = elem;
5365
+ obsoleteNodes.add(tr._wb_node);
5366
+ });
5367
+ let idx = 0;
5368
+ let top = 0;
5369
+ let modified = false;
5370
+ let prevElem = "first";
5371
+ this.visitRows(function (node) {
5372
+ // console.log("visit", node)
5373
+ const rowDiv = node._rowElem;
5374
+ // Renumber all expanded nodes
5375
+ if (node._rowIdx !== idx) {
5376
+ node._rowIdx = idx;
5377
+ modified = true;
5378
+ }
5379
+ if (idx < startIdx || idx > endIdx) {
5380
+ // row is outside viewport bounds
5381
+ if (rowDiv) {
5382
+ prevElem = rowDiv;
5383
+ }
5384
+ }
5385
+ else if (rowDiv && newNodesOnly) {
5386
+ obsoleteNodes.delete(node);
5387
+ // no need to update existing node markup
5388
+ rowDiv.style.top = idx * ROW_HEIGHT + "px";
5389
+ prevElem = rowDiv;
5390
+ }
5391
+ else {
5392
+ obsoleteNodes.delete(node);
5393
+ // Create new markup
5394
+ node.render({ top: top, after: prevElem });
5395
+ // console.log("render", top, prevElem, "=>", node._rowElem);
5396
+ prevElem = node._rowElem;
5397
+ }
5398
+ idx++;
5399
+ top += row_height;
5400
+ });
5401
+ this.treeRowCount = idx;
5402
+ for (const n of obsoleteNodes) {
5403
+ n._callEvent("discard");
5404
+ n.removeMarkup();
5405
+ }
5406
+ // Resize tree container
5407
+ this.nodeListElement.style.height = `${top}px`;
5408
+ // this.log(
5409
+ // `render(scrollOfs:${ofs}, ${startIdx}..${endIdx})`,
5410
+ // this.nodeListElement.style.height
5411
+ // );
5412
+ this.logTimeEnd(label);
5413
+ this._validateRows();
5414
+ return modified;
5415
+ }
5416
+ /**
5417
+ * Call callback(node) for all nodes in hierarchical order (depth-first).
5064
5418
  *
5065
5419
  * @param {function} callback the callback function.
5066
- * Return false to stop iteration, return "skip" to skip this node and children only.
5420
+ * Return false to stop iteration, return "skip" to skip this node and
5421
+ * children only.
5067
5422
  * @returns {boolean} false, if the iterator was stopped.
5068
5423
  */
5069
5424
  visit(callback) {
5070
5425
  return this.root.visit(callback, false);
5071
5426
  }
5072
- /** Call fn(node) for all nodes in vertical order, top down (or bottom up).<br>
5427
+ /**
5428
+ * Call fn(node) for all nodes in vertical order, top down (or bottom up).
5429
+ *
5430
+ * Note that this considers expansion state, i.e. children of collapsed nodes
5431
+ * are skipped.
5432
+ *
5073
5433
  * Stop iteration, if fn() returns false.<br>
5074
5434
  * Return false if iteration was stopped.
5075
5435
  *
@@ -5149,7 +5509,8 @@ class Wunderbaum {
5149
5509
  }
5150
5510
  return true;
5151
5511
  }
5152
- /** Call fn(node) for all nodes in vertical order, bottom up.
5512
+ /**
5513
+ * Call fn(node) for all nodes in vertical order, bottom up.
5153
5514
  * @internal
5154
5515
  */
5155
5516
  _visitRowsUp(callback, opts) {
@@ -5193,19 +5554,36 @@ class Wunderbaum {
5193
5554
  }
5194
5555
  return true;
5195
5556
  }
5196
- /** . */
5557
+ /**
5558
+ * Reload the tree with a new source.
5559
+ *
5560
+ * Previous data is cleared.
5561
+ * Pass `options.columns` to define a header (may also be part of `source.columns`).
5562
+ */
5197
5563
  load(source, options = {}) {
5198
5564
  this.clear();
5199
5565
  const columns = options.columns || source.columns;
5200
5566
  if (columns) {
5201
5567
  this.columns = options.columns;
5202
- this.renderHeader();
5203
- // this.updateColumns({ render: false });
5568
+ // this._renderHeaderMarkup();
5569
+ this.updateColumns({ calculateCols: false });
5204
5570
  }
5205
5571
  return this.root.load(source);
5206
5572
  }
5207
5573
  /**
5574
+ * Disable render requests during operations that would trigger many updates.
5208
5575
  *
5576
+ * ```js
5577
+ * try {
5578
+ * tree.enableUpdate(false);
5579
+ * // ... (long running operation that would trigger many updates)
5580
+ * foo();
5581
+ * // ... NOTE: make sure that async operations have finished
5582
+ * await foo();
5583
+ * } finally {
5584
+ * tree.enableUpdate(true);
5585
+ * }
5586
+ * ```
5209
5587
  */
5210
5588
  enableUpdate(flag) {
5211
5589
  /*
@@ -5213,20 +5591,22 @@ class Wunderbaum {
5213
5591
  1 >-------------------------------------<
5214
5592
  2 >--------------------<
5215
5593
  3 >--------------------------<
5216
-
5217
- 5
5218
-
5219
5594
  */
5220
- // this.logDebug( `enableUpdate(${flag}): count=${this._disableUpdateCount}...` );
5221
5595
  if (flag) {
5222
- assert(this._disableUpdateCount > 0);
5596
+ assert(this._disableUpdateCount > 0, "enableUpdate(true) was called too often");
5223
5597
  this._disableUpdateCount--;
5598
+ // this.logDebug(
5599
+ // `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...`
5600
+ // );
5224
5601
  if (this._disableUpdateCount === 0) {
5225
5602
  this.updateViewport();
5226
5603
  }
5227
5604
  }
5228
5605
  else {
5229
5606
  this._disableUpdateCount++;
5607
+ // this.logDebug(
5608
+ // `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...`
5609
+ // );
5230
5610
  // this._disableUpdate = Date.now();
5231
5611
  }
5232
5612
  // return !flag; // return previous value
@@ -5259,8 +5639,10 @@ class Wunderbaum {
5259
5639
  return this.extensions.filter.updateFilter();
5260
5640
  }
5261
5641
  }
5262
- Wunderbaum.version = "v0.0.1-0"; // Set to semver by 'grunt release'
5263
5642
  Wunderbaum.sequence = 0;
5643
+ /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
5644
+ Wunderbaum.version = "v0.0.3"; // Set to semver by 'grunt release'
5645
+ /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
5264
5646
  Wunderbaum.util = util;
5265
5647
 
5266
5648
  export { Wunderbaum };