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.
@@ -2,13 +2,14 @@
2
2
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
3
  typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.mar10 = {}));
5
- }(this, (function (exports) { 'use strict';
5
+ })(this, (function (exports) { 'use strict';
6
6
 
7
7
  /*!
8
8
  * Wunderbaum - util
9
9
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
10
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
10
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
11
11
  */
12
+ /** @module util */
12
13
  /** Readable names for `MouseEvent.button` */
13
14
  const MOUSE_BUTTONS = {
14
15
  0: "",
@@ -20,7 +21,7 @@
20
21
  };
21
22
  const MAX_INT = 9007199254740991;
22
23
  const userInfo = _getUserInfo();
23
- /**True if the user is using a macOS platform. */
24
+ /**True if the client is using a macOS platform. */
24
25
  const isMac = userInfo.isMac;
25
26
  const REX_HTML = /[&<>"'/]/g; // Escape those characters
26
27
  const REX_TOOLTIP = /[<>"'/]/g; // Don't escape `&` in tooltips
@@ -168,7 +169,7 @@
168
169
  /** TODO */
169
170
  function extractHtmlText(s) {
170
171
  if (s.indexOf(">") >= 0) {
171
- error("not implemented");
172
+ error("Not implemented");
172
173
  // return $("<div/>").html(s).text();
173
174
  }
174
175
  return s;
@@ -290,7 +291,7 @@
290
291
  input.valueAsNumber = value;
291
292
  break;
292
293
  case "radio":
293
- assert(false, "not implemented");
294
+ error("Not implemented");
294
295
  // const name = input.name;
295
296
  // const checked = input.parentElement!.querySelector(
296
297
  // `input[name="${name}"]:checked`
@@ -313,7 +314,7 @@
313
314
  }
314
315
  // return value;
315
316
  }
316
- /** Return an unconnected `HTMLElement` from a HTML string. */
317
+ /** Create and return an unconnected `HTMLElement` from a HTML string. */
317
318
  function elemFromHtml(html) {
318
319
  const t = document.createElement("template");
319
320
  t.innerHTML = html.trim();
@@ -330,11 +331,23 @@
330
331
  }
331
332
  return obj;
332
333
  }
334
+ /** Return a EventTarget from selector or cast an existing element. */
335
+ function eventTargetFromSelector(obj) {
336
+ if (!obj) {
337
+ return null;
338
+ }
339
+ if (typeof obj === "string") {
340
+ return document.querySelector(obj);
341
+ }
342
+ return obj;
343
+ }
333
344
  /**
334
- * Return a descriptive string for a keyboard or mouse event.
345
+ * Return a canonical descriptive string for a keyboard or mouse event.
335
346
  *
336
347
  * The result also contains a prefix for modifiers if any, for example
337
348
  * `"x"`, `"F2"`, `"Control+Home"`, or `"Shift+clickright"`.
349
+ * This is especially useful in `switch` statements, to make sure that modifier
350
+ * keys are considered and handled correctly.
338
351
  */
339
352
  function eventToString(event) {
340
353
  let key = event.key, et = event.type, s = [];
@@ -366,6 +379,13 @@
366
379
  }
367
380
  return s.join("+");
368
381
  }
382
+ /**
383
+ * Copy allproperties from one or more source objects to a target object.
384
+ *
385
+ * @returns the modified target object.
386
+ */
387
+ // TODO: use Object.assign()? --> https://stackoverflow.com/a/42740894
388
+ // TODO: support deep merge --> https://stackoverflow.com/a/42740894
369
389
  function extend(...args) {
370
390
  for (let i = 1; i < args.length; i++) {
371
391
  let arg = args[i];
@@ -380,23 +400,27 @@
380
400
  }
381
401
  return args[0];
382
402
  }
403
+ /** Return true if `obj` is of type `array`. */
383
404
  function isArray(obj) {
384
405
  return Array.isArray(obj);
385
406
  }
407
+ /** Return true if `obj` is of type `Object` and has no propertied. */
386
408
  function isEmptyObject(obj) {
387
409
  return Object.keys(obj).length === 0 && obj.constructor === Object;
388
410
  }
411
+ /** Return true if `obj` is of type `function`. */
389
412
  function isFunction(obj) {
390
413
  return typeof obj === "function";
391
414
  }
415
+ /** Return true if `obj` is of type `Object`. */
392
416
  function isPlainObject(obj) {
393
417
  return Object.prototype.toString.call(obj) === "[object Object]";
394
418
  }
395
419
  /** A dummy function that does nothing ('no operation'). */
396
420
  function noop(...args) { }
397
- function onEvent(rootElem, eventNames, selectorOrHandler, handlerOrNone) {
421
+ function onEvent(rootTarget, eventNames, selectorOrHandler, handlerOrNone) {
398
422
  let selector, handler;
399
- rootElem = elemFromSelector(rootElem);
423
+ rootTarget = eventTargetFromSelector(rootTarget);
400
424
  if (handlerOrNone) {
401
425
  selector = selectorOrHandler;
402
426
  handler = handlerOrNone;
@@ -406,7 +430,7 @@
406
430
  handler = selectorOrHandler;
407
431
  }
408
432
  eventNames.split(" ").forEach((evn) => {
409
- rootElem.addEventListener(evn, function (e) {
433
+ rootTarget.addEventListener(evn, function (e) {
410
434
  if (!selector) {
411
435
  return handler(e); // no event delegation
412
436
  }
@@ -484,6 +508,13 @@
484
508
  }
485
509
  /**
486
510
  * Set or rotate checkbox status with support for tri-state.
511
+ *
512
+ * An initial 'indeterminate' state becomes 'checked' on the first call.
513
+ *
514
+ * If the input element has the class 'wb-tristate' assigned, the sequence is:<br>
515
+ * 'indeterminate' -> 'checked' -> 'unchecked' -> 'indeterminate' -> ...<br>
516
+ * Otherwise we toggle like <br>
517
+ * 'checked' -> 'unchecked' -> 'checked' -> ...
487
518
  */
488
519
  function toggleCheckbox(element, value, tristate) {
489
520
  const input = elemFromSelector(element);
@@ -541,6 +572,7 @@
541
572
  }
542
573
  throw new Error("Cannot convert to Set<string>: " + val);
543
574
  }
575
+ /**Return a canonical string representation for an object's type (e.g. 'array', 'number', ...) */
544
576
  function type(obj) {
545
577
  return Object.prototype.toString
546
578
  .call(obj)
@@ -567,6 +599,7 @@
567
599
  setValueToElem: setValueToElem,
568
600
  elemFromHtml: elemFromHtml,
569
601
  elemFromSelector: elemFromSelector,
602
+ eventTargetFromSelector: eventTargetFromSelector,
570
603
  eventToString: eventToString,
571
604
  extend: extend,
572
605
  isArray: isArray,
@@ -587,7 +620,7 @@
587
620
  /*!
588
621
  * Wunderbaum - common
589
622
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
590
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
623
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
591
624
  */
592
625
  const DEFAULT_DEBUGLEVEL = 4; // Replaced by rollup script
593
626
  const ROW_HEIGHT = 22;
@@ -601,6 +634,8 @@
601
634
  ChangeType["row"] = "row";
602
635
  ChangeType["structure"] = "structure";
603
636
  ChangeType["status"] = "status";
637
+ ChangeType["vscroll"] = "vscroll";
638
+ ChangeType["header"] = "header";
604
639
  })(ChangeType || (ChangeType = {}));
605
640
  var NodeStatusType;
606
641
  (function (NodeStatusType) {
@@ -673,6 +708,8 @@
673
708
  Home: "firstCol",
674
709
  "Control+End": "last",
675
710
  "Control+Home": "first",
711
+ "Meta+ArrowDown": "last",
712
+ "Meta+ArrowUp": "first",
676
713
  "*": "expandAll",
677
714
  Multiply: "expandAll",
678
715
  PageDown: "pageDown",
@@ -699,7 +736,7 @@
699
736
  /*!
700
737
  * Wunderbaum - wb_extension_base
701
738
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
702
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
739
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
703
740
  */
704
741
  class WunderbaumExtension {
705
742
  constructor(tree, id, defaults) {
@@ -1054,7 +1091,7 @@
1054
1091
  /*!
1055
1092
  * Wunderbaum - ext-filter
1056
1093
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1057
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
1094
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1058
1095
  */
1059
1096
  const START_MARKER = "\uFFF7";
1060
1097
  const END_MARKER = "\uFFF8";
@@ -1093,7 +1130,9 @@
1093
1130
  });
1094
1131
  }
1095
1132
  _applyFilterImpl(filter, branchMode, _opts) {
1096
- 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;
1133
+ let match, temp, start = Date.now(), count = 0, tree = this.tree, treeOpts = tree.options,
1134
+ // escapeTitles = treeOpts.escapeTitles,
1135
+ prevAutoCollapse = treeOpts.autoCollapse, opts = extend({}, treeOpts.filter, _opts), hideMode = opts.mode === "hide", leavesOnly = !!opts.leavesOnly && !branchMode;
1097
1136
  // Default to 'match title substring (case insensitive)'
1098
1137
  if (typeof filter === "string") {
1099
1138
  if (filter === "") {
@@ -1126,37 +1165,36 @@
1126
1165
  if (!node.title) {
1127
1166
  return false;
1128
1167
  }
1129
- let text = escapeTitles ? node.title : extractHtmlText(node.title);
1168
+ // let text = escapeTitles ? node.title : extractHtmlText(node.title);
1169
+ let text = node.title;
1130
1170
  // `.match` instead of `.test` to get the capture groups
1131
1171
  let res = text.match(re);
1132
1172
  if (res && opts.highlight) {
1133
- if (escapeTitles) {
1134
- if (opts.fuzzy) {
1135
- temp = _markFuzzyMatchedChars(text, res, escapeTitles);
1136
- }
1137
- else {
1138
- // #740: we must not apply the marks to escaped entity names, e.g. `&quot;`
1139
- // Use some exotic characters to mark matches:
1140
- temp = text.replace(reHighlight, function (s) {
1141
- return START_MARKER + s + END_MARKER;
1142
- });
1143
- }
1144
- // now we can escape the title...
1145
- node.titleWithHighlight = escapeHtml(temp)
1146
- // ... and finally insert the desired `<mark>` tags
1147
- .replace(RE_START_MARKER, "<mark>")
1148
- .replace(RE_END_MARTKER, "</mark>");
1173
+ // if (escapeTitles) {
1174
+ if (opts.fuzzy) {
1175
+ temp = _markFuzzyMatchedChars(text, res, true);
1149
1176
  }
1150
1177
  else {
1151
- if (opts.fuzzy) {
1152
- node.titleWithHighlight = _markFuzzyMatchedChars(text, res);
1153
- }
1154
- else {
1155
- node.titleWithHighlight = text.replace(reHighlight, function (s) {
1156
- return "<mark>" + s + "</mark>";
1157
- });
1158
- }
1178
+ // #740: we must not apply the marks to escaped entity names, e.g. `&quot;`
1179
+ // Use some exotic characters to mark matches:
1180
+ temp = text.replace(reHighlight, function (s) {
1181
+ return START_MARKER + s + END_MARKER;
1182
+ });
1159
1183
  }
1184
+ // now we can escape the title...
1185
+ node.titleWithHighlight = escapeHtml(temp)
1186
+ // ... and finally insert the desired `<mark>` tags
1187
+ .replace(RE_START_MARKER, "<mark>")
1188
+ .replace(RE_END_MARTKER, "</mark>");
1189
+ // } else {
1190
+ // if (opts.fuzzy) {
1191
+ // node.titleWithHighlight = _markFuzzyMatchedChars(text, res);
1192
+ // } else {
1193
+ // node.titleWithHighlight = text.replace(reHighlight, function (s) {
1194
+ // return "<mark>" + s + "</mark>";
1195
+ // });
1196
+ // }
1197
+ // }
1160
1198
  // node.debug("filter", escapeTitles, text, node.titleWithHighlight);
1161
1199
  }
1162
1200
  return !!res;
@@ -1242,8 +1280,6 @@
1242
1280
  }
1243
1281
  /**
1244
1282
  * [ext-filter] Re-apply current filter.
1245
- *
1246
- * @requires jquery.fancytree.filter.js
1247
1283
  */
1248
1284
  updateFilter() {
1249
1285
  let tree = this.tree;
@@ -1258,14 +1294,11 @@
1258
1294
  }
1259
1295
  /**
1260
1296
  * [ext-filter] Reset the filter.
1261
- *
1262
- * @alias Fancytree#clearFilter
1263
- * @requires jquery.fancytree.filter.js
1264
1297
  */
1265
1298
  clearFilter() {
1266
- let tree = this.tree,
1299
+ let tree = this.tree;
1267
1300
  // statusNode = tree.root.findDirectChild(KEY_NODATA),
1268
- escapeTitles = tree.options.escapeTitles;
1301
+ // escapeTitles = tree.options.escapeTitles;
1269
1302
  // enhanceTitle = tree.options.enhanceTitle,
1270
1303
  tree.enableUpdate(false);
1271
1304
  // if (statusNode) {
@@ -1279,12 +1312,11 @@
1279
1312
  if (node.match && node._rowElem) {
1280
1313
  // #491, #601
1281
1314
  let titleElem = node._rowElem.querySelector("span.wb-title");
1282
- if (escapeTitles) {
1283
- titleElem.textContent = node.title;
1284
- }
1285
- else {
1286
- titleElem.innerHTML = node.title;
1287
- }
1315
+ // if (escapeTitles) {
1316
+ titleElem.textContent = node.title;
1317
+ // } else {
1318
+ // titleElem.innerHTML = node.title;
1319
+ // }
1288
1320
  node._callEvent("enhanceTitle", { titleElem: titleElem });
1289
1321
  }
1290
1322
  delete node.match;
@@ -1320,7 +1352,7 @@
1320
1352
  * @param {string} text
1321
1353
  * @param {RegExpMatchArray} matches
1322
1354
  */
1323
- function _markFuzzyMatchedChars(text, matches, escapeTitles = false) {
1355
+ function _markFuzzyMatchedChars(text, matches, escapeTitles = true) {
1324
1356
  let matchingIndices = [];
1325
1357
  // get the indices of matched characters (Iterate through `RegExpMatchArray`)
1326
1358
  for (let _matchingArrIdx = 1; _matchingArrIdx < matches.length; _matchingArrIdx++) {
@@ -1335,7 +1367,7 @@
1335
1367
  // Map each `text` char to its position and store in `textPoses`.
1336
1368
  let textPoses = text.split("");
1337
1369
  if (escapeTitles) {
1338
- // If escaping the title, then wrap the matchng char within exotic chars
1370
+ // If escaping the title, then wrap the matching char within exotic chars
1339
1371
  matchingIndices.forEach(function (v) {
1340
1372
  textPoses[v] = START_MARKER + textPoses[v] + END_MARKER;
1341
1373
  });
@@ -1353,7 +1385,7 @@
1353
1385
  /*!
1354
1386
  * Wunderbaum - ext-keynav
1355
1387
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1356
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
1388
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1357
1389
  */
1358
1390
  class KeynavExtension extends WunderbaumExtension {
1359
1391
  constructor(tree) {
@@ -1420,7 +1452,7 @@
1420
1452
  eventName = "Add"; // expand
1421
1453
  }
1422
1454
  else if (navModeOption === NavigationModeOption.startRow) {
1423
- tree.setCellMode(NavigationMode.cellNav);
1455
+ tree.setNavigationMode(NavigationMode.cellNav);
1424
1456
  return;
1425
1457
  }
1426
1458
  break;
@@ -1459,6 +1491,8 @@
1459
1491
  case "Home":
1460
1492
  case "Control+End":
1461
1493
  case "Control+Home":
1494
+ case "Meta+ArrowDown":
1495
+ case "Meta+ArrowUp":
1462
1496
  case "PageDown":
1463
1497
  case "PageUp":
1464
1498
  node.navigate(eventName, { activate: activate, event: event });
@@ -1490,11 +1524,11 @@
1490
1524
  break;
1491
1525
  case "Escape":
1492
1526
  if (tree.navMode === NavigationMode.cellEdit) {
1493
- tree.setCellMode(NavigationMode.cellNav);
1527
+ tree.setNavigationMode(NavigationMode.cellNav);
1494
1528
  handled = true;
1495
1529
  }
1496
1530
  else if (tree.navMode === NavigationMode.cellNav) {
1497
- tree.setCellMode(NavigationMode.row);
1531
+ tree.setNavigationMode(NavigationMode.row);
1498
1532
  handled = true;
1499
1533
  }
1500
1534
  break;
@@ -1504,7 +1538,7 @@
1504
1538
  handled = true;
1505
1539
  }
1506
1540
  else if (navModeOption !== NavigationModeOption.cell) {
1507
- tree.setCellMode(NavigationMode.row);
1541
+ tree.setNavigationMode(NavigationMode.row);
1508
1542
  handled = true;
1509
1543
  }
1510
1544
  break;
@@ -1521,6 +1555,8 @@
1521
1555
  case "Home":
1522
1556
  case "Control+End":
1523
1557
  case "Control+Home":
1558
+ case "Meta+ArrowDown":
1559
+ case "Meta+ArrowUp":
1524
1560
  case "PageDown":
1525
1561
  case "PageUp":
1526
1562
  node.navigate(eventName, { activate: activate, event: event });
@@ -1539,7 +1575,7 @@
1539
1575
  /*!
1540
1576
  * Wunderbaum - ext-logger
1541
1577
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1542
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
1578
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1543
1579
  */
1544
1580
  class LoggerExtension extends WunderbaumExtension {
1545
1581
  constructor(tree) {
@@ -1579,19 +1615,9 @@
1579
1615
  /*!
1580
1616
  * Wunderbaum - ext-dnd
1581
1617
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1582
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
1618
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1583
1619
  */
1584
- const nodeMimeType = "application/x-fancytree-node";
1585
- // type AllowedDropRegionType =
1586
- // | "after"
1587
- // | "afterBefore"
1588
- // // | "afterBeforeOver" // == all == true
1589
- // | "afterOver"
1590
- // | "all" // == true
1591
- // | "before"
1592
- // | "beforeOver"
1593
- // | "none" // == false == "" == null
1594
- // | "over"; // == "child"
1620
+ const nodeMimeType = "application/x-wunderbaum-node";
1595
1621
  class DndExtension extends WunderbaumExtension {
1596
1622
  constructor(tree) {
1597
1623
  super(tree, "dnd", {
@@ -1854,10 +1880,181 @@
1854
1880
  }
1855
1881
  }
1856
1882
 
1883
+ /*!
1884
+ * Wunderbaum - drag_observer
1885
+ * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1886
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1887
+ */
1888
+ /**
1889
+ * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'.
1890
+ */
1891
+ class DragObserver {
1892
+ constructor(opts) {
1893
+ this.start = {
1894
+ x: 0,
1895
+ y: 0,
1896
+ altKey: false,
1897
+ ctrlKey: false,
1898
+ metaKey: false,
1899
+ shiftKey: false,
1900
+ };
1901
+ this.dragElem = null;
1902
+ this.dragging = false;
1903
+ // TODO: touch events
1904
+ this.events = ["mousedown", "mouseup", "mousemove", "keydown"];
1905
+ assert(opts.root);
1906
+ this.opts = extend({ thresh: 5 }, opts);
1907
+ this.root = opts.root;
1908
+ this._handler = this.handleEvent.bind(this);
1909
+ this.events.forEach((type) => {
1910
+ this.root.addEventListener(type, this._handler);
1911
+ });
1912
+ }
1913
+ /** Unregister all event listeners. */
1914
+ disconnect() {
1915
+ this.events.forEach((type) => {
1916
+ this.root.removeEventListener(type, this._handler);
1917
+ });
1918
+ }
1919
+ getDragElem() {
1920
+ return this.dragElem;
1921
+ }
1922
+ isDragging() {
1923
+ return this.dragging;
1924
+ }
1925
+ stopDrag(cb_event) {
1926
+ if (this.dragging && this.opts.dragstop && cb_event) {
1927
+ cb_event.type = "dragstop";
1928
+ this.opts.dragstop(cb_event);
1929
+ }
1930
+ this.dragElem = null;
1931
+ this.dragging = false;
1932
+ }
1933
+ handleEvent(e) {
1934
+ const type = e.type;
1935
+ const opts = this.opts;
1936
+ const cb_event = {
1937
+ type: e.type,
1938
+ event: e,
1939
+ dragElem: this.dragElem,
1940
+ dx: e.pageX - this.start.x,
1941
+ dy: e.pageY - this.start.y,
1942
+ apply: undefined,
1943
+ };
1944
+ switch (type) {
1945
+ case "keydown":
1946
+ this.stopDrag(cb_event);
1947
+ break;
1948
+ case "mousedown":
1949
+ if (this.dragElem) {
1950
+ this.stopDrag(cb_event);
1951
+ break;
1952
+ }
1953
+ if (opts.selector) {
1954
+ let elem = e.target;
1955
+ if (elem.matches(opts.selector)) {
1956
+ this.dragElem = elem;
1957
+ }
1958
+ else {
1959
+ elem = elem.closest(opts.selector);
1960
+ if (elem) {
1961
+ this.dragElem = elem;
1962
+ }
1963
+ else {
1964
+ break; // no event delegation selector matched
1965
+ }
1966
+ }
1967
+ }
1968
+ this.start.x = e.pageX;
1969
+ this.start.y = e.pageY;
1970
+ this.start.altKey = e.altKey;
1971
+ this.start.ctrlKey = e.ctrlKey;
1972
+ this.start.metaKey = e.metaKey;
1973
+ this.start.shiftKey = e.shiftKey;
1974
+ break;
1975
+ case "mousemove":
1976
+ // TODO: debounce/throttle?
1977
+ // TODO: horizontal mode: ignore if dx unchanged
1978
+ if (!this.dragElem) {
1979
+ break;
1980
+ }
1981
+ if (!this.dragging) {
1982
+ if (opts.thresh) {
1983
+ const dist2 = cb_event.dx * cb_event.dx + cb_event.dy * cb_event.dy;
1984
+ if (dist2 < opts.thresh * opts.thresh) {
1985
+ break;
1986
+ }
1987
+ }
1988
+ cb_event.type = "dragstart";
1989
+ if (opts.dragstart(cb_event) === false) {
1990
+ this.stopDrag(cb_event);
1991
+ break;
1992
+ }
1993
+ this.dragging = true;
1994
+ }
1995
+ if (this.dragging && this.opts.drag) {
1996
+ cb_event.type = "drag";
1997
+ this.opts.drag(cb_event);
1998
+ }
1999
+ break;
2000
+ case "mouseup":
2001
+ if (!this.dragging) {
2002
+ this.stopDrag(cb_event);
2003
+ break;
2004
+ }
2005
+ if (e.button === 0) {
2006
+ cb_event.apply = true;
2007
+ }
2008
+ else {
2009
+ cb_event.apply = false;
2010
+ }
2011
+ this.stopDrag(cb_event);
2012
+ break;
2013
+ }
2014
+ }
2015
+ }
2016
+
2017
+ /*!
2018
+ * Wunderbaum - ext-grid
2019
+ * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
2020
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
2021
+ */
2022
+ class GridExtension extends WunderbaumExtension {
2023
+ constructor(tree) {
2024
+ super(tree, "grid", {
2025
+ // throttle: 200,
2026
+ });
2027
+ this.observer = new DragObserver({
2028
+ root: window.document,
2029
+ selector: "span.wb-col-resizer",
2030
+ thresh: 4,
2031
+ // throttle: 400,
2032
+ dragstart: (e) => {
2033
+ return this.tree.element.contains(e.dragElem);
2034
+ },
2035
+ drag: (e) => {
2036
+ // TODO: throttle
2037
+ return this.handleDrag(e);
2038
+ },
2039
+ dragstop: (e) => {
2040
+ return this.handleDrag(e);
2041
+ },
2042
+ });
2043
+ }
2044
+ init() {
2045
+ super.init();
2046
+ }
2047
+ handleDrag(e) {
2048
+ const info = Wunderbaum.getEventInfo(e.event);
2049
+ // this.tree.options.
2050
+ this.tree.log(`${e.type}(${e.dx})`, e, info);
2051
+ }
2052
+ }
2053
+
1857
2054
  /*!
1858
2055
  * Wunderbaum - deferred
1859
2056
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1860
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
2057
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1861
2058
  */
1862
2059
  /**
1863
2060
  * Deferred is a ES6 Promise, that exposes the resolve() and reject()` method.
@@ -1900,7 +2097,7 @@
1900
2097
  /*!
1901
2098
  * Wunderbaum - wunderbaum_node
1902
2099
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1903
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
2100
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1904
2101
  */
1905
2102
  /** Top-level properties that can be passed with `data`. */
1906
2103
  const NODE_PROPS = new Set([
@@ -1937,15 +2134,31 @@
1937
2134
  "unselectableIgnore",
1938
2135
  "unselectableStatus",
1939
2136
  ]);
2137
+ /**
2138
+ * A single tree node.
2139
+ *
2140
+ * **NOTE:** <br>
2141
+ * Generally you should not modify properties directly, since this may break
2142
+ * the internal bookkeeping.
2143
+ */
1940
2144
  class WunderbaumNode {
1941
2145
  constructor(tree, parent, data) {
1942
2146
  var _a, _b;
2147
+ /** Reference key. Unlike {@link key}, a `refKey` may occur multiple
2148
+ * times within a tree (in this case we have 'clone nodes').
2149
+ * @see Use {@link setKey} to modify.
2150
+ */
1943
2151
  this.refKey = undefined;
1944
2152
  this.children = null;
1945
2153
  this.lazy = false;
2154
+ /** Expansion state.
2155
+ * @see {@link isExpandable}, {@link isExpanded}, {@link setExpanded}. */
1946
2156
  this.expanded = false;
2157
+ /** Selection state.
2158
+ * @see {@link isSelected}, {@link setSelected}. */
1947
2159
  this.selected = false;
1948
- /** Additional classes added to `div.wb-row`. */
2160
+ /** Additional classes added to `div.wb-row`.
2161
+ * @see {@link addClass}, {@link removeClass}, {@link toggleClass}. */
1949
2162
  this.extraClasses = new Set();
1950
2163
  /** Custom data that was passed to the constructor */
1951
2164
  this.data = {};
@@ -2072,7 +2285,7 @@
2072
2285
  // this.fixSelection3FromEndNodes();
2073
2286
  // }
2074
2287
  // this.triggerModifyChild("add", nodeList.length === 1 ? nodeList[0] : null);
2075
- this.tree.setModified(ChangeType.structure, this);
2288
+ this.tree.setModified(ChangeType.structure);
2076
2289
  return nodeList[0];
2077
2290
  }
2078
2291
  finally {
@@ -2111,7 +2324,8 @@
2111
2324
  }
2112
2325
  /**
2113
2326
  * Apply a modification (or navigation) operation.
2114
- * @see Wunderbaum#applyCommand
2327
+ *
2328
+ * @see {@link Wunderbaum.applyCommand}
2115
2329
  */
2116
2330
  applyCommand(cmd, opts) {
2117
2331
  return this.tree.applyCommand(cmd, this, opts);
@@ -2204,8 +2418,7 @@
2204
2418
  }
2205
2419
  /** Find a node relative to self.
2206
2420
  *
2207
- * @param where The keyCode that would normally trigger this move,
2208
- * or a keyword ('down', 'first', 'last', 'left', 'parent', 'right', 'up').
2421
+ * @see {@link Wunderbaum.findRelatedNode|tree.findRelatedNode()}
2209
2422
  */
2210
2423
  findRelatedNode(where, includeHidden = false) {
2211
2424
  return this.tree.findRelatedNode(this, where, includeHidden);
@@ -2506,7 +2719,7 @@
2506
2719
  assert(!this.parent);
2507
2720
  tree.columns = data.columns;
2508
2721
  delete data.columns;
2509
- tree.renderHeader();
2722
+ tree.updateColumns({ calculateCols: false });
2510
2723
  }
2511
2724
  this._loadSourceObject(data);
2512
2725
  }
@@ -2542,19 +2755,20 @@
2542
2755
  this.setStatus(NodeStatusType.ok);
2543
2756
  return;
2544
2757
  }
2545
- assert(isArray(source) || (source && source.url), "The lazyLoad event must return a node list, `{url: ...}` or false.");
2758
+ assert(isArray(source) || (source && source.url), "The lazyLoad event must return a node list, `{url: ...}`, or false.");
2546
2759
  await this.load(source); // also calls setStatus('ok')
2547
2760
  if (wasExpanded) {
2548
2761
  this.expanded = true;
2549
- this.tree.updateViewport();
2762
+ this.tree.setModified(ChangeType.structure);
2550
2763
  }
2551
2764
  else {
2552
- this.render(); // Fix expander icon to 'loaded'
2765
+ this.setModified(); // Fix expander icon to 'loaded'
2553
2766
  }
2554
2767
  }
2555
2768
  catch (e) {
2769
+ this.logError("Error during loadLazy()", e);
2770
+ this._callEvent("error", { error: e });
2556
2771
  this.setStatus(NodeStatusType.error, "" + e);
2557
- // } finally {
2558
2772
  }
2559
2773
  return;
2560
2774
  }
@@ -2702,7 +2916,7 @@
2702
2916
  n.tree = targetNode.tree;
2703
2917
  }, true);
2704
2918
  }
2705
- tree.updateViewport();
2919
+ tree.setModified(ChangeType.structure);
2706
2920
  // TODO: fix selection state
2707
2921
  // TODO: fix active state
2708
2922
  }
@@ -2722,31 +2936,33 @@
2722
2936
  // Allow to pass 'ArrowLeft' instead of 'left'
2723
2937
  where = KEY_TO_ACTION_DICT[where] || where;
2724
2938
  // Otherwise activate or focus the related node
2725
- let node = this.findRelatedNode(where);
2726
- if (node) {
2727
- // setFocus/setActive will scroll later (if autoScroll is specified)
2728
- try {
2729
- node.makeVisible({ scrollIntoView: false });
2730
- }
2731
- catch (e) { } // #272
2732
- node.setFocus();
2733
- if ((options === null || options === void 0 ? void 0 : options.activate) === false) {
2734
- return Promise.resolve(this);
2735
- }
2736
- return node.setActive(true, { event: options === null || options === void 0 ? void 0 : options.event });
2939
+ const node = this.findRelatedNode(where);
2940
+ if (!node) {
2941
+ this.logWarn(`Could not find related node '${where}'.`);
2942
+ return Promise.resolve(this);
2943
+ }
2944
+ // setFocus/setActive will scroll later (if autoScroll is specified)
2945
+ try {
2946
+ node.makeVisible({ scrollIntoView: false });
2737
2947
  }
2738
- this.logWarn("Could not find related node '" + where + "'.");
2739
- return Promise.resolve(this);
2948
+ catch (e) { } // #272
2949
+ node.setFocus();
2950
+ if ((options === null || options === void 0 ? void 0 : options.activate) === false) {
2951
+ return Promise.resolve(this);
2952
+ }
2953
+ return node.setActive(true, { event: options === null || options === void 0 ? void 0 : options.event });
2740
2954
  }
2741
2955
  /** Delete this node and all descendants. */
2742
2956
  remove() {
2743
2957
  const tree = this.tree;
2744
2958
  const pos = this.parent.children.indexOf(this);
2959
+ this.triggerModify("remove");
2745
2960
  this.parent.children.splice(pos, 1);
2746
2961
  this.visit((n) => {
2747
2962
  n.removeMarkup();
2748
2963
  tree._unregisterNode(n);
2749
2964
  }, true);
2965
+ tree.setModified(ChangeType.structure);
2750
2966
  }
2751
2967
  /** Remove all descendants of this node. */
2752
2968
  removeChildren() {
@@ -2778,7 +2994,7 @@
2778
2994
  if (!this.isRootNode()) {
2779
2995
  this.expanded = false;
2780
2996
  }
2781
- this.tree.updateViewport();
2997
+ this.tree.setModified(ChangeType.structure);
2782
2998
  }
2783
2999
  /** Remove all HTML markup from the DOM. */
2784
3000
  removeMarkup() {
@@ -2871,6 +3087,7 @@
2871
3087
  const activeColIdx = tree.navMode === NavigationMode.row ? null : tree.activeColIdx;
2872
3088
  // let colElems: HTMLElement[];
2873
3089
  const isNew = !rowDiv;
3090
+ assert(!isNew || (opts && opts.after), "opts.after expected, unless updating");
2874
3091
  assert(!this.isRootNode());
2875
3092
  //
2876
3093
  let rowClasses = ["wb-row"];
@@ -2922,7 +3139,7 @@
2922
3139
  nodeElem.appendChild(elem);
2923
3140
  ofsTitlePx += ICON_WIDTH;
2924
3141
  }
2925
- if (level > treeOptions.minExpandLevel) {
3142
+ if (!treeOptions.minExpandLevel || level > treeOptions.minExpandLevel) {
2926
3143
  expanderSpan = document.createElement("i");
2927
3144
  nodeElem.appendChild(expanderSpan);
2928
3145
  ofsTitlePx += ICON_WIDTH;
@@ -3013,11 +3230,11 @@
3013
3230
  if (this.titleWithHighlight) {
3014
3231
  titleSpan.innerHTML = this.titleWithHighlight;
3015
3232
  }
3016
- else if (tree.options.escapeTitles) {
3017
- titleSpan.textContent = this.title;
3018
- }
3019
3233
  else {
3020
- titleSpan.innerHTML = this.title;
3234
+ // } else if (tree.options.escapeTitles) {
3235
+ titleSpan.textContent = this.title;
3236
+ // } else {
3237
+ // titleSpan.innerHTML = this.title;
3021
3238
  }
3022
3239
  // Set the width of the title span, so overflow ellipsis work
3023
3240
  if (!treeOptions.skeleton) {
@@ -3051,9 +3268,19 @@
3051
3268
  });
3052
3269
  }
3053
3270
  // Attach to DOM as late as possible
3054
- // if (!this._rowElem) {
3055
- tree.nodeListElement.appendChild(rowDiv);
3056
- // }
3271
+ if (isNew) {
3272
+ const after = opts ? opts.after : "last";
3273
+ switch (after) {
3274
+ case "first":
3275
+ tree.nodeListElement.prepend(rowDiv);
3276
+ break;
3277
+ case "last":
3278
+ tree.nodeListElement.appendChild(rowDiv);
3279
+ break;
3280
+ default:
3281
+ opts.after.after(rowDiv);
3282
+ }
3283
+ }
3057
3284
  }
3058
3285
  /**
3059
3286
  * Remove all children, collapse, and set the lazy-flag, so that the lazyLoad
@@ -3064,7 +3291,7 @@
3064
3291
  this.expanded = false;
3065
3292
  this.lazy = true;
3066
3293
  this.children = null;
3067
- this.tree.updateViewport();
3294
+ this.tree.setModified(ChangeType.structure);
3068
3295
  }
3069
3296
  /** Convert node (or whole branch) into a plain object.
3070
3297
  *
@@ -3127,14 +3354,15 @@
3127
3354
  *
3128
3355
  * Evaluation sequence:
3129
3356
  *
3130
- * If `tree.options.<name>` is a callback that returns something, use that.
3131
- * Else if `node.<name>` is defined, use that.
3132
- * Else if `tree.types[<node.type>]` is a value, use that.
3133
- * Else if `tree.options.<name>` is a value, use that.
3134
- * Else use `defaultValue`.
3357
+ * - If `tree.options.<name>` is a callback that returns something, use that.
3358
+ * - Else if `node.<name>` is defined, use that.
3359
+ * - Else if `tree.types[<node.type>]` is a value, use that.
3360
+ * - Else if `tree.options.<name>` is a value, use that.
3361
+ * - Else use `defaultValue`.
3135
3362
  *
3136
3363
  * @param name name of the option property (on node and tree)
3137
3364
  * @param defaultValue return this if nothing else matched
3365
+ * {@link Wunderbaum.getOption|Wunderbaum.getOption()}
3138
3366
  */
3139
3367
  getOption(name, defaultValue) {
3140
3368
  let tree = this.tree;
@@ -3169,15 +3397,21 @@
3169
3397
  // Use value from value options dict, fallback do default
3170
3398
  return value !== null && value !== void 0 ? value : defaultValue;
3171
3399
  }
3400
+ /** Make sure that this node is visible in the viewport.
3401
+ * @see {@link Wunderbaum.scrollTo|Wunderbaum.scrollTo()}
3402
+ */
3172
3403
  async scrollIntoView(options) {
3173
3404
  return this.tree.scrollTo(this);
3174
3405
  }
3406
+ /**
3407
+ * Activate this node, deactivate previous, send events, activate column and scroll int viewport.
3408
+ */
3175
3409
  async setActive(flag = true, options) {
3176
3410
  const tree = this.tree;
3177
3411
  const prev = tree.activeNode;
3178
3412
  const retrigger = options === null || options === void 0 ? void 0 : options.retrigger;
3179
- const noEvent = options === null || options === void 0 ? void 0 : options.noEvent;
3180
- if (!noEvent) {
3413
+ const noEvents = options === null || options === void 0 ? void 0 : options.noEvents;
3414
+ if (!noEvents) {
3181
3415
  let orgEvent = options === null || options === void 0 ? void 0 : options.event;
3182
3416
  if (flag) {
3183
3417
  if (prev !== this || retrigger) {
@@ -3192,7 +3426,7 @@
3192
3426
  orgEvent: orgEvent,
3193
3427
  }) === false) {
3194
3428
  tree.activeNode = null;
3195
- prev === null || prev === void 0 ? void 0 : prev.setDirty(ChangeType.status);
3429
+ prev === null || prev === void 0 ? void 0 : prev.setModified();
3196
3430
  return;
3197
3431
  }
3198
3432
  }
@@ -3203,8 +3437,8 @@
3203
3437
  }
3204
3438
  if (prev !== this) {
3205
3439
  tree.activeNode = this;
3206
- prev === null || prev === void 0 ? void 0 : prev.setDirty(ChangeType.status);
3207
- this.setDirty(ChangeType.status);
3440
+ prev === null || prev === void 0 ? void 0 : prev.setModified();
3441
+ this.setModified();
3208
3442
  }
3209
3443
  if (options &&
3210
3444
  options.colIdx != null &&
@@ -3215,57 +3449,62 @@
3215
3449
  // requestAnimationFrame(() => {
3216
3450
  // this.scrollIntoView();
3217
3451
  // })
3218
- this.scrollIntoView();
3219
- }
3220
- setDirty(type) {
3221
- if (this.tree._disableUpdate) {
3222
- return;
3223
- }
3224
- if (type === ChangeType.structure) {
3225
- this.tree.updateViewport();
3226
- }
3227
- else if (this._rowElem) {
3228
- // otherwise not in viewport, so no need to render
3229
- this.render();
3230
- }
3452
+ return this.scrollIntoView();
3231
3453
  }
3454
+ /**
3455
+ * Expand or collapse this node.
3456
+ */
3232
3457
  async setExpanded(flag = true, options) {
3233
3458
  // alert("" + this.getLevel() + ", "+ this.getOption("minExpandLevel");
3234
3459
  if (!flag &&
3235
3460
  this.isExpanded() &&
3236
3461
  this.getLevel() < this.getOption("minExpandLevel") &&
3237
3462
  !getOption(options, "force")) {
3238
- this.logDebug("Ignored collapse request.");
3463
+ this.logDebug("Ignored collapse request below expandLevel.");
3239
3464
  return;
3240
3465
  }
3241
3466
  if (flag && this.lazy && this.children == null) {
3242
3467
  await this.loadLazy();
3243
3468
  }
3244
3469
  this.expanded = flag;
3245
- this.setDirty(ChangeType.structure);
3246
- }
3247
- setIcon() {
3248
- throw new Error("Not yet implemented");
3249
- // this.setDirty(ChangeType.status);
3470
+ this.tree.setModified(ChangeType.structure);
3250
3471
  }
3472
+ /**
3473
+ * Set keyboard focus here.
3474
+ * @see {@link setActive}
3475
+ */
3251
3476
  setFocus(flag = true, options) {
3252
3477
  const prev = this.tree.focusNode;
3253
3478
  this.tree.focusNode = this;
3254
- prev === null || prev === void 0 ? void 0 : prev.setDirty(ChangeType.status);
3255
- this.setDirty(ChangeType.status);
3479
+ prev === null || prev === void 0 ? void 0 : prev.setModified();
3480
+ this.setModified();
3256
3481
  }
3482
+ /** Set a new icon path or class. */
3483
+ setIcon() {
3484
+ throw new Error("Not yet implemented");
3485
+ // this.setModified();
3486
+ }
3487
+ /** Change node's {@link key} and/or {@link refKey}. */
3488
+ setKey(key, refKey) {
3489
+ throw new Error("Not yet implemented");
3490
+ }
3491
+ /** Schedule a render, typically called to update after a status or data change. */
3492
+ setModified(change = ChangeType.status) {
3493
+ assert(change === ChangeType.status);
3494
+ this.tree.setModified(ChangeType.row, this);
3495
+ }
3496
+ /** Modify the check/uncheck state. */
3257
3497
  setSelected(flag = true, options) {
3258
3498
  const prev = this.selected;
3259
3499
  if (!!flag !== prev) {
3260
3500
  this._callEvent("select", { flag: flag });
3261
3501
  }
3262
3502
  this.selected = !!flag;
3263
- this.setDirty(ChangeType.status);
3503
+ this.setModified();
3264
3504
  }
3265
- /** Show node status (ok, loading, error, noData) using styles and a dummy child node.
3266
- */
3505
+ /** Display node status (ok, loading, error, noData) using styles and a dummy child node. */
3267
3506
  setStatus(status, message, details) {
3268
- let tree = this.tree;
3507
+ const tree = this.tree;
3269
3508
  let statusNode = null;
3270
3509
  const _clearStatusNode = () => {
3271
3510
  // Remove dedicated dummy node, if any
@@ -3336,12 +3575,13 @@
3336
3575
  default:
3337
3576
  error("invalid node status " + status);
3338
3577
  }
3339
- tree.updateViewport();
3578
+ tree.setModified(ChangeType.structure);
3340
3579
  return statusNode;
3341
3580
  }
3581
+ /** Rename this node. */
3342
3582
  setTitle(title) {
3343
3583
  this.title = title;
3344
- this.setDirty(ChangeType.status);
3584
+ this.setModified();
3345
3585
  // this.triggerModify("rename"); // TODO
3346
3586
  }
3347
3587
  /**
@@ -3362,10 +3602,16 @@
3362
3602
  * @param {object} [extra]
3363
3603
  */
3364
3604
  triggerModify(operation, extra) {
3605
+ if (!this.parent) {
3606
+ return;
3607
+ }
3365
3608
  this.parent.triggerModifyChild(operation, this, extra);
3366
3609
  }
3367
- /** Call fn(node) for all child nodes in hierarchical order (depth-first).<br>
3368
- * Stop iteration, if fn() returns false. Skip current branch, if fn() returns "skip".<br>
3610
+ /**
3611
+ * Call fn(node) for all child nodes in hierarchical order (depth-first).
3612
+ *
3613
+ * Stop iteration, if fn() returns false. Skip current branch, if fn()
3614
+ * returns "skip".<br>
3369
3615
  * Return false if iteration was stopped.
3370
3616
  *
3371
3617
  * @param {function} callback the callback function.
@@ -3409,7 +3655,8 @@
3409
3655
  }
3410
3656
  return true;
3411
3657
  }
3412
- /** Call fn(node) for all sibling nodes.<br>
3658
+ /**
3659
+ * Call fn(node) for all sibling nodes.<br>
3413
3660
  * Stop iteration, if fn() returns false.<br>
3414
3661
  * Return false if iteration was stopped.
3415
3662
  *
@@ -3440,7 +3687,7 @@
3440
3687
  /*!
3441
3688
  * Wunderbaum - ext-edit
3442
3689
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
3443
- * v0.0.1-0, Thu, 31 Mar 2022 10:19:39 GMT (https://github.com/mar10/wunderbaum)
3690
+ * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
3444
3691
  */
3445
3692
  // const START_MARKER = "\uFFF7";
3446
3693
  class EditExtension extends WunderbaumExtension {
@@ -3558,7 +3805,7 @@
3558
3805
  break;
3559
3806
  case "F2":
3560
3807
  if (trigger.indexOf("F2") >= 0) {
3561
- // tree.setCellMode(NavigationMode.cellEdit);
3808
+ // tree.setNavigationMode(NavigationMode.cellEdit);
3562
3809
  this.startEditTitle();
3563
3810
  return false;
3564
3811
  }
@@ -3599,6 +3846,7 @@
3599
3846
  if (validity) {
3600
3847
  // Permanently apply input validations (CSS and tooltip)
3601
3848
  inputElem.addEventListener("keydown", (e) => {
3849
+ inputElem.setCustomValidity("");
3602
3850
  if (!inputElem.reportValidity()) ;
3603
3851
  });
3604
3852
  }
@@ -3639,6 +3887,11 @@
3639
3887
  }
3640
3888
  node.logDebug(`stopEditTitle(${apply})`, opts, focusElem, newValue);
3641
3889
  if (apply && newValue !== null && newValue !== node.title) {
3890
+ const errMsg = focusElem.validationMessage;
3891
+ if (errMsg) {
3892
+ // input element's native validation failed
3893
+ throw new Error(`Input validation failed for "${newValue}": ${errMsg}.`);
3894
+ }
3642
3895
  const colElem = node.getColElem(0);
3643
3896
  this._applyChange("edit.apply", node, colElem, {
3644
3897
  oldValue: node.title,
@@ -3725,12 +3978,12 @@
3725
3978
  * Copyright (c) 2021-2022, Martin Wendt (https://wwWendt.de).
3726
3979
  * Released under the MIT license.
3727
3980
  *
3728
- * @version v0.0.1-0
3729
- * @date Thu, 31 Mar 2022 10:19:39 GMT
3981
+ * @version v0.0.3
3982
+ * @date Mon, 18 Apr 2022 11:52:44 GMT
3730
3983
  */
3731
3984
  // const class_prefix = "wb-";
3732
3985
  // const node_props: string[] = ["title", "key", "refKey"];
3733
- const MAX_CHANGED_NODES = 10;
3986
+ // const MAX_CHANGED_NODES = 10;
3734
3987
  /**
3735
3988
  * A persistent plain object or array.
3736
3989
  *
@@ -3742,36 +3995,43 @@
3742
3995
  this.extensions = {};
3743
3996
  this.keyMap = new Map();
3744
3997
  this.refKeyMap = new Map();
3745
- this.viewNodes = new Set();
3746
- // protected rows: WunderbaumNode[] = [];
3747
- // protected _rowCount = 0;
3998
+ // protected viewNodes = new Set<WunderbaumNode>();
3999
+ this.treeRowCount = 0;
4000
+ this._disableUpdateCount = 0;
3748
4001
  // protected eventHandlers : Array<function> = [];
4002
+ /** Currently active node if any. */
3749
4003
  this.activeNode = null;
4004
+ /** Current node hat has keyboard focus if any. */
3750
4005
  this.focusNode = null;
3751
- this._disableUpdate = 0;
3752
- this._disableUpdateCount = 0;
3753
4006
  /** Shared properties, referenced by `node.type`. */
3754
4007
  this.types = {};
3755
4008
  /** List of column definitions. */
3756
4009
  this.columns = [];
3757
4010
  this._columnsById = {};
3758
4011
  // Modification Status
3759
- this.changedSince = 0;
3760
- this.changes = new Set();
3761
- this.changedNodes = new Set();
4012
+ // protected changedSince = 0;
4013
+ // protected changes = new Set<ChangeType>();
4014
+ // protected changedNodes = new Set<WunderbaumNode>();
4015
+ this.changeRedrawRequestPending = false;
4016
+ /** Expose some useful methods of the util.ts module as `tree._util`. */
4017
+ this._util = util;
3762
4018
  // --- FILTER ---
3763
4019
  this.filterMode = null;
3764
4020
  // --- KEYNAV ---
4021
+ /** @internal Use `setColumn()`/`getActiveColElem()`*/
3765
4022
  this.activeColIdx = 0;
4023
+ /** @internal */
3766
4024
  this.navMode = NavigationMode.row;
4025
+ /** @internal */
3767
4026
  this.lastQuicksearchTime = 0;
4027
+ /** @internal */
3768
4028
  this.lastQuicksearchTerm = "";
3769
4029
  // --- EDIT ---
3770
4030
  this.lastClickTime = 0;
3771
- // TODO: make accessible in compiled JS like this?
3772
- this._util = util;
3773
- /** Alias for `logDebug` */
3774
- this.log = this.logDebug; // Alias
4031
+ /** Alias for {@link Wunderbaum.logDebug}.
4032
+ * @alias Wunderbaum.logDebug
4033
+ */
4034
+ this.log = this.logDebug;
3775
4035
  let opts = (this.options = extend({
3776
4036
  id: null,
3777
4037
  source: null,
@@ -3782,7 +4042,7 @@
3782
4042
  rowHeightPx: ROW_HEIGHT,
3783
4043
  columns: null,
3784
4044
  types: null,
3785
- escapeTitles: true,
4045
+ // escapeTitles: true,
3786
4046
  showSpinner: false,
3787
4047
  checkbox: true,
3788
4048
  minExpandLevel: 0,
@@ -3839,6 +4099,7 @@
3839
4099
  this._registerExtension(new EditExtension(this));
3840
4100
  this._registerExtension(new FilterExtension(this));
3841
4101
  this._registerExtension(new DndExtension(this));
4102
+ this._registerExtension(new GridExtension(this));
3842
4103
  this._registerExtension(new LoggerExtension(this));
3843
4104
  // --- Evaluate options
3844
4105
  this.columns = opts.columns;
@@ -3932,11 +4193,9 @@
3932
4193
  var _a;
3933
4194
  (_a = this.element.querySelector("progress.spinner")) === null || _a === void 0 ? void 0 : _a.remove();
3934
4195
  this.element.classList.remove("wb-initializing");
3935
- // this.updateViewport();
3936
4196
  });
3937
4197
  }
3938
4198
  else {
3939
- // this.updateViewport();
3940
4199
  readyDeferred.resolve();
3941
4200
  }
3942
4201
  // TODO: This is sometimes required, because this.element.clientWidth
@@ -3946,13 +4205,10 @@
3946
4205
  }, 50);
3947
4206
  // --- Bind listeners
3948
4207
  this.scrollContainer.addEventListener("scroll", (e) => {
3949
- this.updateViewport();
4208
+ this.setModified(ChangeType.vscroll);
3950
4209
  });
3951
- // window.addEventListener("resize", (e: Event) => {
3952
- // this.updateViewport();
3953
- // });
3954
4210
  this.resizeObserver = new ResizeObserver((entries) => {
3955
- this.updateViewport();
4211
+ this.setModified(ChangeType.vscroll);
3956
4212
  console.log("ResizeObserver: Size changed", entries);
3957
4213
  });
3958
4214
  this.resizeObserver.observe(this.element);
@@ -4009,37 +4265,18 @@
4009
4265
  forceClose: true,
4010
4266
  });
4011
4267
  }
4012
- // if (flag && !this.activeNode ) {
4013
- // setTimeout(() => {
4014
- // if (!this.activeNode) {
4015
- // const firstNode = this.getFirstChild();
4016
- // if (firstNode && !firstNode?.isStatusNode()) {
4017
- // firstNode.logInfo("Activate on focus", e);
4018
- // firstNode.setActive(true, { event: e });
4019
- // }
4020
- // }
4021
- // }, 10);
4022
- // }
4023
4268
  });
4024
4269
  }
4025
- /** */
4026
- // _renderHeader(){
4027
- // const coldivs = "<span class='wb-col'></span>".repeat(this.columns.length);
4028
- // this.element.innerHTML = `
4029
- // <div class='wb-header'>
4030
- // <div class='wb-row'>
4031
- // ${coldivs}
4032
- // </div>
4033
- // </div>`;
4034
- // }
4035
- /** Return a Wunderbaum instance, from element, index, or event.
4270
+ /**
4271
+ * Return a Wunderbaum instance, from element, id, index, or event.
4036
4272
  *
4037
- * @example
4038
- * getTree(); // Get first Wunderbaum instance on page
4039
- * getTree(1); // Get second Wunderbaum instance on page
4040
- * getTree(event); // Get tree for this mouse- or keyboard event
4041
- * getTree("foo"); // Get tree for this `tree.options.id`
4273
+ * ```js
4274
+ * getTree(); // Get first Wunderbaum instance on page
4275
+ * getTree(1); // Get second Wunderbaum instance on page
4276
+ * getTree(event); // Get tree for this mouse- or keyboard event
4277
+ * getTree("foo"); // Get tree for this `tree.options.id`
4042
4278
  * getTree("#tree"); // Get tree for this matching element
4279
+ * ```
4043
4280
  */
4044
4281
  static getTree(el) {
4045
4282
  if (el instanceof Wunderbaum) {
@@ -4080,9 +4317,8 @@
4080
4317
  }
4081
4318
  return null;
4082
4319
  }
4083
- /** Return a WunderbaumNode instance from element, event.
4084
- *
4085
- * @param el
4320
+ /**
4321
+ * Return a WunderbaumNode instance from element or event.
4086
4322
  */
4087
4323
  static getNode(el) {
4088
4324
  if (!el) {
@@ -4104,7 +4340,7 @@
4104
4340
  }
4105
4341
  return null;
4106
4342
  }
4107
- /** */
4343
+ /** @internal */
4108
4344
  _registerExtension(extension) {
4109
4345
  this.extensionList.push(extension);
4110
4346
  this.extensions[extension.id] = extension;
@@ -4146,7 +4382,7 @@
4146
4382
  node.tree = null;
4147
4383
  node.parent = null;
4148
4384
  // node.title = "DISPOSED: " + node.title
4149
- this.viewNodes.delete(node);
4385
+ // this.viewNodes.delete(node);
4150
4386
  node.removeMarkup();
4151
4387
  }
4152
4388
  /** Call all hook methods of all registered extensions.*/
@@ -4164,7 +4400,9 @@
4164
4400
  }
4165
4401
  return res;
4166
4402
  }
4167
- /** Call tree method or extension method if defined.
4403
+ /**
4404
+ * Call tree method or extension method if defined.
4405
+ *
4168
4406
  * Example:
4169
4407
  * ```js
4170
4408
  * tree._callMethod("edit.startEdit", "arg1", "arg2")
@@ -4181,7 +4419,9 @@
4181
4419
  this.logError(`Calling undefined method '${name}()'.`);
4182
4420
  }
4183
4421
  }
4184
- /** Call event handler if defined in tree.options.
4422
+ /**
4423
+ * Call event handler if defined in tree or tree.EXTENSION options.
4424
+ *
4185
4425
  * Example:
4186
4426
  * ```js
4187
4427
  * tree._callEvent("edit.beforeEdit", {foo: 42})
@@ -4197,27 +4437,33 @@
4197
4437
  // this.logError(`Triggering undefined event '${name}'.`)
4198
4438
  }
4199
4439
  }
4200
- /** Return the topmost visible node in the viewport */
4201
- _firstNodeInView(complete = true) {
4202
- let topIdx, node;
4203
- if (complete) {
4204
- topIdx = Math.ceil(this.scrollContainer.scrollTop / ROW_HEIGHT);
4205
- }
4206
- else {
4207
- topIdx = Math.floor(this.scrollContainer.scrollTop / ROW_HEIGHT);
4208
- }
4440
+ /** Return the node for given row index. */
4441
+ _getNodeByRowIdx(idx) {
4209
4442
  // TODO: start searching from active node (reverse)
4443
+ let node = null;
4210
4444
  this.visitRows((n) => {
4211
- if (n._rowIdx === topIdx) {
4445
+ if (n._rowIdx === idx) {
4212
4446
  node = n;
4213
4447
  return false;
4214
4448
  }
4215
4449
  });
4216
4450
  return node;
4217
4451
  }
4218
- /** Return the lowest visible node in the viewport */
4452
+ /** Return the topmost visible node in the viewport. */
4453
+ _firstNodeInView(complete = true) {
4454
+ let topIdx;
4455
+ const gracePy = 1; // ignore subpixel scrolling
4456
+ if (complete) {
4457
+ topIdx = Math.ceil((this.scrollContainer.scrollTop - gracePy) / ROW_HEIGHT);
4458
+ }
4459
+ else {
4460
+ topIdx = Math.floor(this.scrollContainer.scrollTop / ROW_HEIGHT);
4461
+ }
4462
+ return this._getNodeByRowIdx(topIdx);
4463
+ }
4464
+ /** Return the lowest visible node in the viewport. */
4219
4465
  _lastNodeInView(complete = true) {
4220
- let bottomIdx, node;
4466
+ let bottomIdx;
4221
4467
  if (complete) {
4222
4468
  bottomIdx =
4223
4469
  Math.floor((this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
@@ -4228,16 +4474,10 @@
4228
4474
  Math.ceil((this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
4229
4475
  ROW_HEIGHT) - 1;
4230
4476
  }
4231
- // TODO: start searching from active node
4232
- this.visitRows((n) => {
4233
- if (n._rowIdx === bottomIdx) {
4234
- node = n;
4235
- return false;
4236
- }
4237
- });
4238
- return node;
4477
+ bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
4478
+ return this._getNodeByRowIdx(bottomIdx);
4239
4479
  }
4240
- /** Return preceeding visible node in the viewport */
4480
+ /** Return preceeding visible node in the viewport. */
4241
4481
  _getPrevNodeInView(node, ofs = 1) {
4242
4482
  this.visitRows((n) => {
4243
4483
  node = n;
@@ -4247,7 +4487,7 @@
4247
4487
  }, { reverse: true, start: node || this.getActiveNode() });
4248
4488
  return node;
4249
4489
  }
4250
- /** Return following visible node in the viewport */
4490
+ /** Return following visible node in the viewport. */
4251
4491
  _getNextNodeInView(node, ofs = 1) {
4252
4492
  this.visitRows((n) => {
4253
4493
  node = n;
@@ -4257,10 +4497,15 @@
4257
4497
  }, { reverse: false, start: node || this.getActiveNode() });
4258
4498
  return node;
4259
4499
  }
4500
+ /**
4501
+ * Append (or insert) a list of toplevel nodes.
4502
+ *
4503
+ * @see {@link WunderbaumNode.addChildren}
4504
+ */
4260
4505
  addChildren(nodeData, options) {
4261
4506
  return this.root.addChildren(nodeData, options);
4262
4507
  }
4263
- /*
4508
+ /**
4264
4509
  * Apply a modification or navigation operation.
4265
4510
  *
4266
4511
  * Most of these commands simply map to a node or tree method.
@@ -4385,16 +4630,17 @@
4385
4630
  this.root.children = null;
4386
4631
  this.keyMap.clear();
4387
4632
  this.refKeyMap.clear();
4388
- this.viewNodes.clear();
4633
+ // this.viewNodes.clear();
4634
+ this.treeRowCount = 0;
4389
4635
  this.activeNode = null;
4390
4636
  this.focusNode = null;
4391
4637
  // this.types = {};
4392
4638
  // this. columns =[];
4393
4639
  // this._columnsById = {};
4394
4640
  // Modification Status
4395
- this.changedSince = 0;
4396
- this.changes.clear();
4397
- this.changedNodes.clear();
4641
+ // this.changedSince = 0;
4642
+ // this.changes.clear();
4643
+ // this.changedNodes.clear();
4398
4644
  // // --- FILTER ---
4399
4645
  // public filterMode: FilterModeType = null;
4400
4646
  // // --- KEYNAV ---
@@ -4402,7 +4648,7 @@
4402
4648
  // public cellNavMode = false;
4403
4649
  // public lastQuicksearchTime = 0;
4404
4650
  // public lastQuicksearchTerm = "";
4405
- this.updateViewport();
4651
+ this.setModified(ChangeType.structure);
4406
4652
  }
4407
4653
  /**
4408
4654
  * Clear nodes and markup and detach events and observers.
@@ -4422,10 +4668,11 @@
4422
4668
  /**
4423
4669
  * Return `tree.option.NAME` (also resolving if this is a callback).
4424
4670
  *
4425
- * See also [[WunderbaumNode.getOption()]] to consider `node.NAME` setting and
4426
- * `tree.types[node.type].NAME`.
4671
+ * See also {@link WunderbaumNode.getOption|WunderbaumNode.getOption()}
4672
+ * to consider `node.NAME` setting and `tree.types[node.type].NAME`.
4427
4673
  *
4428
- * @param name option name (use dot notation to access extension option, e.g. `filter.mode`)
4674
+ * @param name option name (use dot notation to access extension option, e.g.
4675
+ * `filter.mode`)
4429
4676
  */
4430
4677
  getOption(name, defaultValue) {
4431
4678
  let ext;
@@ -4469,18 +4716,14 @@
4469
4716
  }
4470
4717
  /** Run code, but defer `updateViewport()` until done. */
4471
4718
  runWithoutUpdate(func, hint = null) {
4472
- // const prev = this._disableUpdate;
4473
- // const start = Date.now();
4474
- // this._disableUpdate = Date.now();
4475
4719
  try {
4476
4720
  this.enableUpdate(false);
4477
- return func();
4721
+ const res = func();
4722
+ assert(!(res instanceof Promise));
4723
+ return res;
4478
4724
  }
4479
4725
  finally {
4480
4726
  this.enableUpdate(true);
4481
- // if (!prev && this._disableUpdate === start) {
4482
- // this._disableUpdate = 0;
4483
- // }
4484
4727
  }
4485
4728
  }
4486
4729
  /** Recursively expand all expandable nodes (triggers lazy load id needed). */
@@ -4498,11 +4741,12 @@
4498
4741
  /** Return the number of nodes in the data model.*/
4499
4742
  count(visible = false) {
4500
4743
  if (visible) {
4501
- return this.viewNodes.size;
4744
+ return this.treeRowCount;
4745
+ // return this.viewNodes.size;
4502
4746
  }
4503
4747
  return this.keyMap.size;
4504
4748
  }
4505
- /* Internal sanity check. */
4749
+ /** @internal sanity check. */
4506
4750
  _check() {
4507
4751
  let i = 0;
4508
4752
  this.visit((n) => {
@@ -4513,25 +4757,30 @@
4513
4757
  }
4514
4758
  // util.assert(this.keyMap.size === i);
4515
4759
  }
4516
- /**Find all nodes that matches condition.
4760
+ /**
4761
+ * Find all nodes that matches condition.
4517
4762
  *
4518
4763
  * @param match title string to search for, or a
4519
4764
  * callback function that returns `true` if a node is matched.
4520
- * @see [[WunderbaumNode.findAll]]
4765
+ *
4766
+ * @see {@link WunderbaumNode.findAll}
4521
4767
  */
4522
4768
  findAll(match) {
4523
4769
  return this.root.findAll(match);
4524
4770
  }
4525
- /**Find first node that matches condition.
4771
+ /**
4772
+ * Find first node that matches condition.
4526
4773
  *
4527
4774
  * @param match title string to search for, or a
4528
4775
  * callback function that returns `true` if a node is matched.
4529
- * @see [[WunderbaumNode.findFirst]]
4776
+ * @see {@link WunderbaumNode.findFirst}
4777
+ *
4530
4778
  */
4531
4779
  findFirst(match) {
4532
4780
  return this.root.findFirst(match);
4533
4781
  }
4534
- /** Find the next visible node that starts with `match`, starting at `startNode`
4782
+ /**
4783
+ * Find the next visible node that starts with `match`, starting at `startNode`
4535
4784
  * and wrap-around at the end.
4536
4785
  */
4537
4786
  findNextNode(match, startNode) {
@@ -4561,7 +4810,8 @@
4561
4810
  }
4562
4811
  return res;
4563
4812
  }
4564
- /** Find a node relative to another node.
4813
+ /**
4814
+ * Find a node relative to another node.
4565
4815
  *
4566
4816
  * @param node
4567
4817
  * @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'.
@@ -4571,7 +4821,7 @@
4571
4821
  */
4572
4822
  findRelatedNode(node, where, includeHidden = false) {
4573
4823
  let res = null;
4574
- let pageSize = Math.floor(this.scrollContainer.clientHeight / ROW_HEIGHT);
4824
+ const pageSize = Math.floor(this.scrollContainer.clientHeight / ROW_HEIGHT);
4575
4825
  switch (where) {
4576
4826
  case "parent":
4577
4827
  if (node.parent && node.parent.parent) {
@@ -4627,9 +4877,9 @@
4627
4877
  res = this._getNextNodeInView(node);
4628
4878
  break;
4629
4879
  case "pageDown":
4630
- let bottomNode = this._lastNodeInView();
4631
- // this.logDebug(where, this.focusNode, bottomNode);
4632
- if (this.focusNode !== bottomNode) {
4880
+ const bottomNode = this._lastNodeInView();
4881
+ // this.logDebug(`${where}(${node}) -> ${bottomNode}`);
4882
+ if (node._rowIdx < bottomNode._rowIdx) {
4633
4883
  res = bottomNode;
4634
4884
  }
4635
4885
  else {
@@ -4637,12 +4887,13 @@
4637
4887
  }
4638
4888
  break;
4639
4889
  case "pageUp":
4640
- if (this.focusNode && this.focusNode._rowIdx === 0) {
4641
- res = this.focusNode;
4890
+ if (node._rowIdx === 0) {
4891
+ res = node;
4642
4892
  }
4643
4893
  else {
4644
- let topNode = this._firstNodeInView();
4645
- if (this.focusNode !== topNode) {
4894
+ const topNode = this._firstNodeInView();
4895
+ // this.logDebug(`${where}(${node}) -> ${topNode}`);
4896
+ if (node._rowIdx > topNode._rowIdx) {
4646
4897
  res = topNode;
4647
4898
  }
4648
4899
  else {
@@ -4656,7 +4907,7 @@
4656
4907
  return res;
4657
4908
  }
4658
4909
  /**
4659
- * Return the active cell of the currently active node or null.
4910
+ * Return the active cell (`span.wb-col`) of the currently active node or null.
4660
4911
  */
4661
4912
  getActiveColElem() {
4662
4913
  if (this.activeNode && this.activeColIdx >= 0) {
@@ -4670,8 +4921,8 @@
4670
4921
  getActiveNode() {
4671
4922
  return this.activeNode;
4672
4923
  }
4673
- /** Return the first top level node if any (not the invisible root node).
4674
- * @returns {FancytreeNode | null}
4924
+ /**
4925
+ * Return the first top level node if any (not the invisible root node).
4675
4926
  */
4676
4927
  getFirstChild() {
4677
4928
  return this.root.getFirstChild();
@@ -4682,14 +4933,15 @@
4682
4933
  getFocusNode() {
4683
4934
  return this.focusNode;
4684
4935
  }
4685
- /** Return a {node: FancytreeNode, region: TYPE} object for a mouse event.
4936
+ /** Return a {node: WunderbaumNode, region: TYPE} object for a mouse event.
4686
4937
  *
4687
4938
  * @param {Event} event Mouse event, e.g. click, ...
4688
- * @returns {object} Return a {node: FancytreeNode, region: TYPE} object
4939
+ * @returns {object} Return a {node: WunderbaumNode, region: TYPE} object
4689
4940
  * TYPE: 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined
4690
4941
  */
4691
4942
  static getEventInfo(event) {
4692
- let target = event.target, cl = target.classList, parentCol = target.closest(".wb-col"), node = Wunderbaum.getNode(target), res = {
4943
+ 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 = {
4944
+ tree: tree,
4693
4945
  node: node,
4694
4946
  region: TargetType.unknown,
4695
4947
  colDef: undefined,
@@ -4721,13 +4973,15 @@
4721
4973
  }
4722
4974
  else {
4723
4975
  // Somewhere near the title
4724
- console.warn("getEventInfo(): not found", event, res);
4976
+ if (event.type !== "mousemove" && !(event instanceof KeyboardEvent)) {
4977
+ console.warn("getEventInfo(): not found", event, res);
4978
+ }
4725
4979
  return res;
4726
4980
  }
4727
4981
  if (res.colIdx === -1) {
4728
4982
  res.colIdx = 0;
4729
4983
  }
4730
- res.colDef = node.tree.columns[res.colIdx];
4984
+ res.colDef = tree === null || tree === void 0 ? void 0 : tree.columns[res.colIdx];
4731
4985
  res.colDef != null ? (res.colId = res.colDef.id) : 0;
4732
4986
  // this.log("Event", event, res);
4733
4987
  return res;
@@ -4751,7 +5005,8 @@
4751
5005
  isEditing() {
4752
5006
  return this._callMethod("edit.isEditingTitle");
4753
5007
  }
4754
- /** Return true if any node is currently beeing loaded, i.e. a Ajax request is pending.
5008
+ /**
5009
+ * Return true if any node is currently beeing loaded, i.e. a Ajax request is pending.
4755
5010
  */
4756
5011
  isLoading() {
4757
5012
  var res = false;
@@ -4778,7 +5033,7 @@
4778
5033
  console.error.apply(console, args);
4779
5034
  }
4780
5035
  }
4781
- /* Log to console if opts.debugLevel >= 3 */
5036
+ /** Log to console if opts.debugLevel >= 3 */
4782
5037
  logInfo(...args) {
4783
5038
  if (this.options.debugLevel >= 3) {
4784
5039
  Array.prototype.unshift.call(args, this.toString());
@@ -4805,77 +5060,13 @@
4805
5060
  console.warn.apply(console, args);
4806
5061
  }
4807
5062
  }
4808
- /** */
4809
- render(opts) {
4810
- const label = this.logTime("render");
4811
- let idx = 0;
4812
- let top = 0;
4813
- const height = ROW_HEIGHT;
4814
- let modified = false;
4815
- let start = opts === null || opts === void 0 ? void 0 : opts.startIdx;
4816
- let end = opts === null || opts === void 0 ? void 0 : opts.endIdx;
4817
- const obsoleteViewNodes = this.viewNodes;
4818
- this.viewNodes = new Set();
4819
- let viewNodes = this.viewNodes;
4820
- // this.debug("render", opts);
4821
- assert(start != null && end != null);
4822
- // Make sure start is always even, so the alternating row colors don't
4823
- // change when scrolling:
4824
- if (start % 2) {
4825
- start--;
4826
- }
4827
- this.visitRows(function (node) {
4828
- const prevIdx = node._rowIdx;
4829
- viewNodes.add(node);
4830
- obsoleteViewNodes.delete(node);
4831
- if (prevIdx !== idx) {
4832
- node._rowIdx = idx;
4833
- modified = true;
4834
- }
4835
- if (idx < start || idx > end) {
4836
- node._callEvent("discard");
4837
- node.removeMarkup();
4838
- }
4839
- else {
4840
- // if (!node._rowElem || prevIdx != idx) {
4841
- node.render({ top: top });
4842
- }
4843
- idx++;
4844
- top += height;
4845
- });
4846
- for (const prevNode of obsoleteViewNodes) {
4847
- prevNode._callEvent("discard");
4848
- prevNode.removeMarkup();
4849
- }
4850
- // Resize tree container
4851
- this.nodeListElement.style.height = "" + top + "px";
4852
- // this.log("render()", this.nodeListElement.style.height);
4853
- this.logTimeEnd(label);
4854
- return modified;
4855
- }
4856
- /**Recalc and apply header columns from `this.columns`. */
4857
- renderHeader() {
4858
- if (!this.headerElement) {
4859
- return;
4860
- }
4861
- const headerRow = this.headerElement.querySelector(".wb-row");
4862
- assert(headerRow);
4863
- headerRow.innerHTML = "<span class='wb-col'></span>".repeat(this.columns.length);
4864
- for (let i = 0; i < this.columns.length; i++) {
4865
- let col = this.columns[i];
4866
- let colElem = headerRow.children[i];
4867
- colElem.style.left = col._ofsPx + "px";
4868
- colElem.style.width = col._widthPx + "px";
4869
- colElem.textContent = col.title || col.id;
4870
- }
4871
- }
4872
5063
  /**
5064
+ * Make sure that this node is scrolled into the viewport.
4873
5065
  *
4874
5066
  * @param {boolean | PlainObject} [effects=false] animation options.
4875
5067
  * @param {object} [options=null] {topNode: null, effects: ..., parent: ...}
4876
5068
  * this node will remain visible in
4877
5069
  * any case, even if `this` is outside the scroll pane.
4878
- * Make sure that a node is scrolled into the viewport.
4879
5070
  */
4880
5071
  scrollTo(opts) {
4881
5072
  const MARGIN = 1;
@@ -4896,30 +5087,18 @@
4896
5087
  // Node is above viewport
4897
5088
  newTop = nodeOfs + MARGIN;
4898
5089
  }
4899
- this.log("scrollTo(" + nodeOfs + "): " + curTop + " => " + newTop, height);
4900
5090
  if (newTop != null) {
5091
+ this.log("scrollTo(" + nodeOfs + "): " + curTop + " => " + newTop, height);
4901
5092
  this.scrollContainer.scrollTop = newTop;
4902
- this.updateViewport();
4903
- }
4904
- }
4905
- /** */
4906
- setCellMode(mode) {
4907
- // util.assert(this.cellNavMode);
4908
- // util.assert(0 <= colIdx && colIdx < this.columns.length);
4909
- if (mode === this.navMode) {
4910
- return;
4911
- }
4912
- const prevMode = this.navMode;
4913
- const cellMode = mode !== NavigationMode.row;
4914
- this.navMode = mode;
4915
- if (cellMode && prevMode === NavigationMode.row) {
4916
- this.setColumn(0);
5093
+ this.setModified(ChangeType.vscroll);
4917
5094
  }
4918
- this.element.classList.toggle("wb-cell-mode", cellMode);
4919
- this.element.classList.toggle("wb-cell-edit-mode", mode === NavigationMode.cellEdit);
4920
- this.setModified(ChangeType.row, this.activeNode);
4921
5095
  }
4922
- /** */
5096
+ /**
5097
+ * Set column #colIdx to 'active'.
5098
+ *
5099
+ * This higlights the column header and -cells by adding the `wb-active` class.
5100
+ * Available in cell-nav and cell-edit mode, not in row-mode.
5101
+ */
4923
5102
  setColumn(colIdx) {
4924
5103
  assert(this.navMode !== NavigationMode.row);
4925
5104
  assert(0 <= colIdx && colIdx < this.columns.length);
@@ -4944,7 +5123,7 @@
4944
5123
  }
4945
5124
  }
4946
5125
  }
4947
- /** */
5126
+ /** Set or remove keybaord focus to the tree container. */
4948
5127
  setFocus(flag = true) {
4949
5128
  if (flag) {
4950
5129
  this.element.focus();
@@ -4953,88 +5132,145 @@
4953
5132
  this.element.blur();
4954
5133
  }
4955
5134
  }
4956
- /** */
4957
5135
  setModified(change, node, options) {
4958
- if (!this.changedSince) {
4959
- this.changedSince = Date.now();
5136
+ if (this._disableUpdateCount) {
5137
+ // Assuming that we redraw all when enableUpdate() is re-enabled.
5138
+ // this.log(
5139
+ // `IGNORED setModified(${change}) node=${node} (disable level ${this._disableUpdateCount})`
5140
+ // );
5141
+ return;
4960
5142
  }
4961
- this.changes.add(change);
4962
- if (change === ChangeType.structure) {
4963
- this.changedNodes.clear();
5143
+ // this.log(`setModified(${change}) node=${node}`);
5144
+ if (!(node instanceof WunderbaumNode)) {
5145
+ options = node;
5146
+ }
5147
+ const immediate = !!getOption(options, "immediate");
5148
+ switch (change) {
5149
+ case ChangeType.any:
5150
+ case ChangeType.structure:
5151
+ case ChangeType.header:
5152
+ this.changeRedrawRequestPending = true;
5153
+ this.updateViewport(immediate);
5154
+ break;
5155
+ case ChangeType.vscroll:
5156
+ this.updateViewport(immediate);
5157
+ break;
5158
+ case ChangeType.row:
5159
+ case ChangeType.status:
5160
+ // Single nodes are immedialtely updated if already inside the viewport
5161
+ // (otherwise we can ignore)
5162
+ if (node._rowElem) {
5163
+ node.render();
5164
+ }
5165
+ break;
5166
+ default:
5167
+ error(`Invalid change type ${change}`);
4964
5168
  }
4965
- else if (node && !this.changes.has(ChangeType.structure)) {
4966
- if (this.changedNodes.size < MAX_CHANGED_NODES) {
4967
- this.changedNodes.add(node);
4968
- }
4969
- else {
4970
- this.changes.add(ChangeType.structure);
4971
- this.changedNodes.clear();
4972
- }
5169
+ }
5170
+ /** Set the tree's navigation mode. */
5171
+ setNavigationMode(mode) {
5172
+ // util.assert(this.cellNavMode);
5173
+ // util.assert(0 <= colIdx && colIdx < this.columns.length);
5174
+ if (mode === this.navMode) {
5175
+ return;
4973
5176
  }
4974
- // this.log("setModified(" + change + ")", node);
5177
+ const prevMode = this.navMode;
5178
+ const cellMode = mode !== NavigationMode.row;
5179
+ this.navMode = mode;
5180
+ if (cellMode && prevMode === NavigationMode.row) {
5181
+ this.setColumn(0);
5182
+ }
5183
+ this.element.classList.toggle("wb-cell-mode", cellMode);
5184
+ this.element.classList.toggle("wb-cell-edit-mode", mode === NavigationMode.cellEdit);
5185
+ this.setModified(ChangeType.row, this.activeNode);
4975
5186
  }
5187
+ /** Display tree status (ok, loading, error, noData) using styles and a dummy root node. */
4976
5188
  setStatus(status, message, details) {
4977
5189
  return this.root.setStatus(status, message, details);
4978
5190
  }
4979
5191
  /** Update column headers and width. */
4980
5192
  updateColumns(opts) {
4981
- let modified = false;
4982
- let minWidth = 4;
4983
- let vpWidth = this.element.clientWidth;
5193
+ opts = Object.assign({ calculateCols: true, updateRows: true }, opts);
5194
+ const minWidth = 4;
5195
+ const vpWidth = this.element.clientWidth;
4984
5196
  let totalWeight = 0;
4985
5197
  let fixedWidth = 0;
4986
- // Gather width requests
4987
- this._columnsById = {};
4988
- for (let col of this.columns) {
4989
- this._columnsById[col.id] = col;
4990
- let cw = col.width;
4991
- if (!cw || cw === "*") {
4992
- col._weight = 1.0;
4993
- totalWeight += 1.0;
4994
- }
4995
- else if (typeof cw === "number") {
4996
- col._weight = cw;
4997
- totalWeight += cw;
4998
- }
4999
- else if (typeof cw === "string" && cw.endsWith("px")) {
5000
- col._weight = 0;
5001
- let px = parseFloat(cw.slice(0, -2));
5002
- if (col._widthPx != px) {
5003
- modified = true;
5004
- col._widthPx = px;
5198
+ let modified = false;
5199
+ if (opts.calculateCols) {
5200
+ // Gather width requests
5201
+ this._columnsById = {};
5202
+ for (let col of this.columns) {
5203
+ this._columnsById[col.id] = col;
5204
+ let cw = col.width;
5205
+ if (!cw || cw === "*") {
5206
+ col._weight = 1.0;
5207
+ totalWeight += 1.0;
5208
+ }
5209
+ else if (typeof cw === "number") {
5210
+ col._weight = cw;
5211
+ totalWeight += cw;
5212
+ }
5213
+ else if (typeof cw === "string" && cw.endsWith("px")) {
5214
+ col._weight = 0;
5215
+ let px = parseFloat(cw.slice(0, -2));
5216
+ if (col._widthPx != px) {
5217
+ modified = true;
5218
+ col._widthPx = px;
5219
+ }
5220
+ fixedWidth += px;
5221
+ }
5222
+ else {
5223
+ error("Invalid column width: " + cw);
5005
5224
  }
5006
- fixedWidth += px;
5007
5225
  }
5008
- else {
5009
- error("Invalid column width: " + cw);
5010
- }
5011
- }
5012
- // Share remaining space between non-fixed columns
5013
- let restPx = Math.max(0, vpWidth - fixedWidth);
5014
- let ofsPx = 0;
5015
- for (let col of this.columns) {
5016
- if (col._weight) {
5017
- let px = Math.max(minWidth, (restPx * col._weight) / totalWeight);
5018
- if (col._widthPx != px) {
5019
- modified = true;
5020
- col._widthPx = px;
5226
+ // Share remaining space between non-fixed columns
5227
+ const restPx = Math.max(0, vpWidth - fixedWidth);
5228
+ let ofsPx = 0;
5229
+ for (let col of this.columns) {
5230
+ if (col._weight) {
5231
+ const px = Math.max(minWidth, (restPx * col._weight) / totalWeight);
5232
+ if (col._widthPx != px) {
5233
+ modified = true;
5234
+ col._widthPx = px;
5235
+ }
5021
5236
  }
5237
+ col._ofsPx = ofsPx;
5238
+ ofsPx += col._widthPx;
5022
5239
  }
5023
- col._ofsPx = ofsPx;
5024
- ofsPx += col._widthPx;
5025
5240
  }
5026
5241
  // Every column has now a calculated `_ofsPx` and `_widthPx`
5027
5242
  // this.logInfo("UC", this.columns, vpWidth, this.element.clientWidth, this.element);
5028
5243
  // console.trace();
5029
5244
  // util.error("BREAK");
5030
5245
  if (modified) {
5031
- this.renderHeader();
5032
- if (opts.render !== false) {
5033
- this.render();
5246
+ this._renderHeaderMarkup();
5247
+ if (opts.updateRows) {
5248
+ this._updateRows();
5034
5249
  }
5035
5250
  }
5036
5251
  }
5037
- /** Render all rows that are visible in the viewport. */
5252
+ /** Create/update header markup from `this.columns` definition.
5253
+ * @internal
5254
+ */
5255
+ _renderHeaderMarkup() {
5256
+ if (!this.headerElement) {
5257
+ return;
5258
+ }
5259
+ const headerRow = this.headerElement.querySelector(".wb-row");
5260
+ assert(headerRow);
5261
+ headerRow.innerHTML = "<span class='wb-col'></span>".repeat(this.columns.length);
5262
+ for (let i = 0; i < this.columns.length; i++) {
5263
+ const col = this.columns[i];
5264
+ const colElem = headerRow.children[i];
5265
+ colElem.style.left = col._ofsPx + "px";
5266
+ colElem.style.width = col._widthPx + "px";
5267
+ // colElem.textContent = col.title || col.id;
5268
+ const title = escapeHtml(col.title || col.id);
5269
+ colElem.innerHTML = `<span class="wb-col-title">${title}</span> <span class="wb-col-resizer"></span>`;
5270
+ // colElem.innerHTML = `${title} <span class="wb-col-resizer"></span>`;
5271
+ }
5272
+ }
5273
+ /** Render header and all rows that are visible in the viewport (async, throttled). */
5038
5274
  updateViewport(immediate = false) {
5039
5275
  // Call the `throttle` wrapper for `this._updateViewport()` which will
5040
5276
  // execute immediately on the leading edge of a sequence:
@@ -5043,39 +5279,163 @@
5043
5279
  this._updateViewportThrottled.flush();
5044
5280
  }
5045
5281
  }
5282
+ /**
5283
+ * This is the actual update method, which is wrapped inside a throttle method.
5284
+ * This protected method should not be called directly but via
5285
+ * `tree.updateViewport()` or `tree.setModified()`.
5286
+ * It calls `updateColumns()` and `_updateRows()`.
5287
+ * @internal
5288
+ */
5046
5289
  _updateViewport() {
5047
- if (this._disableUpdate) {
5290
+ if (this._disableUpdateCount) {
5291
+ this.log(`IGNORED _updateViewport() disable level: ${this._disableUpdateCount}`);
5048
5292
  return;
5049
5293
  }
5294
+ const newNodesOnly = !this.changeRedrawRequestPending;
5295
+ this.changeRedrawRequestPending = false;
5050
5296
  let height = this.scrollContainer.clientHeight;
5051
- // We cannot get the height for abolut positioned parent, so look at first col
5297
+ // We cannot get the height for absolute positioned parent, so look at first col
5052
5298
  // let headerHeight = this.headerElement.clientHeight
5053
5299
  // let headerHeight = this.headerElement.children[0].children[0].clientHeight;
5054
5300
  const headerHeight = this.options.headerHeightPx;
5055
- let wantHeight = this.element.clientHeight - headerHeight;
5056
- let ofs = this.scrollContainer.scrollTop;
5301
+ const wantHeight = this.element.clientHeight - headerHeight;
5057
5302
  if (Math.abs(height - wantHeight) > 1.0) {
5058
5303
  // this.log("resize", height, wantHeight);
5059
5304
  this.scrollContainer.style.height = wantHeight + "px";
5060
5305
  height = wantHeight;
5061
5306
  }
5062
- this.updateColumns({ render: false });
5063
- this.render({
5064
- startIdx: Math.max(0, ofs / ROW_HEIGHT - RENDER_MAX_PREFETCH),
5065
- endIdx: Math.max(0, (ofs + height) / ROW_HEIGHT + RENDER_MAX_PREFETCH),
5066
- });
5307
+ this.updateColumns({ updateRows: false });
5308
+ this._updateRows({ newNodesOnly: newNodesOnly });
5067
5309
  this._callEvent("update");
5068
5310
  }
5069
- /** Call callback(node) for all nodes in hierarchical order (depth-first).
5311
+ /**
5312
+ * Assert that TR order matches the natural node order
5313
+ * @internal
5314
+ */
5315
+ _validateRows() {
5316
+ let trs = this.nodeListElement.childNodes;
5317
+ let i = 0;
5318
+ let prev = -1;
5319
+ let ok = true;
5320
+ trs.forEach((element) => {
5321
+ const tr = element;
5322
+ const top = Number.parseInt(tr.style.top);
5323
+ const n = tr._wb_node;
5324
+ // if (i < 4) {
5325
+ // console.info(
5326
+ // `TR#${i}, rowIdx=${n._rowIdx} , top=${top}px: '${n.title}'`
5327
+ // );
5328
+ // }
5329
+ if (top <= prev) {
5330
+ console.warn(`TR order mismatch at index ${i}: top=${top}px, node=${n}`);
5331
+ // throw new Error("fault");
5332
+ ok = false;
5333
+ }
5334
+ prev = top;
5335
+ i++;
5336
+ });
5337
+ return ok;
5338
+ }
5339
+ /*
5340
+ * - Traverse all *visible* of the whole tree, i.e. skip collapsed nodes.
5341
+ * - Store count of rows to `tree.treeRowCount`.
5342
+ * - Renumber `node._rowIdx` for all visible nodes.
5343
+ * - Calculate the index range that must be rendered to fill the viewport
5344
+ * (including upper and lower prefetch)
5345
+ * -
5346
+ */
5347
+ _updateRows(opts) {
5348
+ const label = this.logTime("_updateRows");
5349
+ opts = Object.assign({ newNodesOnly: false }, opts);
5350
+ const newNodesOnly = !!opts.newNodesOnly;
5351
+ const row_height = ROW_HEIGHT;
5352
+ const vp_height = this.scrollContainer.clientHeight;
5353
+ const prefetch = RENDER_MAX_PREFETCH;
5354
+ const ofs = this.scrollContainer.scrollTop;
5355
+ let startIdx = Math.max(0, ofs / row_height - prefetch);
5356
+ startIdx = Math.floor(startIdx);
5357
+ // Make sure start is always even, so the alternating row colors don't
5358
+ // change when scrolling:
5359
+ if (startIdx % 2) {
5360
+ startIdx--;
5361
+ }
5362
+ let endIdx = Math.max(0, (ofs + vp_height) / row_height + prefetch);
5363
+ endIdx = Math.ceil(endIdx);
5364
+ // const obsoleteViewNodes = this.viewNodes;
5365
+ // this.viewNodes = new Set();
5366
+ // const viewNodes = this.viewNodes;
5367
+ // this.debug("render", opts);
5368
+ const obsoleteNodes = new Set();
5369
+ this.nodeListElement.childNodes.forEach((elem) => {
5370
+ const tr = elem;
5371
+ obsoleteNodes.add(tr._wb_node);
5372
+ });
5373
+ let idx = 0;
5374
+ let top = 0;
5375
+ let modified = false;
5376
+ let prevElem = "first";
5377
+ this.visitRows(function (node) {
5378
+ // console.log("visit", node)
5379
+ const rowDiv = node._rowElem;
5380
+ // Renumber all expanded nodes
5381
+ if (node._rowIdx !== idx) {
5382
+ node._rowIdx = idx;
5383
+ modified = true;
5384
+ }
5385
+ if (idx < startIdx || idx > endIdx) {
5386
+ // row is outside viewport bounds
5387
+ if (rowDiv) {
5388
+ prevElem = rowDiv;
5389
+ }
5390
+ }
5391
+ else if (rowDiv && newNodesOnly) {
5392
+ obsoleteNodes.delete(node);
5393
+ // no need to update existing node markup
5394
+ rowDiv.style.top = idx * ROW_HEIGHT + "px";
5395
+ prevElem = rowDiv;
5396
+ }
5397
+ else {
5398
+ obsoleteNodes.delete(node);
5399
+ // Create new markup
5400
+ node.render({ top: top, after: prevElem });
5401
+ // console.log("render", top, prevElem, "=>", node._rowElem);
5402
+ prevElem = node._rowElem;
5403
+ }
5404
+ idx++;
5405
+ top += row_height;
5406
+ });
5407
+ this.treeRowCount = idx;
5408
+ for (const n of obsoleteNodes) {
5409
+ n._callEvent("discard");
5410
+ n.removeMarkup();
5411
+ }
5412
+ // Resize tree container
5413
+ this.nodeListElement.style.height = `${top}px`;
5414
+ // this.log(
5415
+ // `render(scrollOfs:${ofs}, ${startIdx}..${endIdx})`,
5416
+ // this.nodeListElement.style.height
5417
+ // );
5418
+ this.logTimeEnd(label);
5419
+ this._validateRows();
5420
+ return modified;
5421
+ }
5422
+ /**
5423
+ * Call callback(node) for all nodes in hierarchical order (depth-first).
5070
5424
  *
5071
5425
  * @param {function} callback the callback function.
5072
- * Return false to stop iteration, return "skip" to skip this node and children only.
5426
+ * Return false to stop iteration, return "skip" to skip this node and
5427
+ * children only.
5073
5428
  * @returns {boolean} false, if the iterator was stopped.
5074
5429
  */
5075
5430
  visit(callback) {
5076
5431
  return this.root.visit(callback, false);
5077
5432
  }
5078
- /** Call fn(node) for all nodes in vertical order, top down (or bottom up).<br>
5433
+ /**
5434
+ * Call fn(node) for all nodes in vertical order, top down (or bottom up).
5435
+ *
5436
+ * Note that this considers expansion state, i.e. children of collapsed nodes
5437
+ * are skipped.
5438
+ *
5079
5439
  * Stop iteration, if fn() returns false.<br>
5080
5440
  * Return false if iteration was stopped.
5081
5441
  *
@@ -5155,7 +5515,8 @@
5155
5515
  }
5156
5516
  return true;
5157
5517
  }
5158
- /** Call fn(node) for all nodes in vertical order, bottom up.
5518
+ /**
5519
+ * Call fn(node) for all nodes in vertical order, bottom up.
5159
5520
  * @internal
5160
5521
  */
5161
5522
  _visitRowsUp(callback, opts) {
@@ -5199,19 +5560,36 @@
5199
5560
  }
5200
5561
  return true;
5201
5562
  }
5202
- /** . */
5563
+ /**
5564
+ * Reload the tree with a new source.
5565
+ *
5566
+ * Previous data is cleared.
5567
+ * Pass `options.columns` to define a header (may also be part of `source.columns`).
5568
+ */
5203
5569
  load(source, options = {}) {
5204
5570
  this.clear();
5205
5571
  const columns = options.columns || source.columns;
5206
5572
  if (columns) {
5207
5573
  this.columns = options.columns;
5208
- this.renderHeader();
5209
- // this.updateColumns({ render: false });
5574
+ // this._renderHeaderMarkup();
5575
+ this.updateColumns({ calculateCols: false });
5210
5576
  }
5211
5577
  return this.root.load(source);
5212
5578
  }
5213
5579
  /**
5580
+ * Disable render requests during operations that would trigger many updates.
5214
5581
  *
5582
+ * ```js
5583
+ * try {
5584
+ * tree.enableUpdate(false);
5585
+ * // ... (long running operation that would trigger many updates)
5586
+ * foo();
5587
+ * // ... NOTE: make sure that async operations have finished
5588
+ * await foo();
5589
+ * } finally {
5590
+ * tree.enableUpdate(true);
5591
+ * }
5592
+ * ```
5215
5593
  */
5216
5594
  enableUpdate(flag) {
5217
5595
  /*
@@ -5219,20 +5597,22 @@
5219
5597
  1 >-------------------------------------<
5220
5598
  2 >--------------------<
5221
5599
  3 >--------------------------<
5222
-
5223
- 5
5224
-
5225
5600
  */
5226
- // this.logDebug( `enableUpdate(${flag}): count=${this._disableUpdateCount}...` );
5227
5601
  if (flag) {
5228
- assert(this._disableUpdateCount > 0);
5602
+ assert(this._disableUpdateCount > 0, "enableUpdate(true) was called too often");
5229
5603
  this._disableUpdateCount--;
5604
+ // this.logDebug(
5605
+ // `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...`
5606
+ // );
5230
5607
  if (this._disableUpdateCount === 0) {
5231
5608
  this.updateViewport();
5232
5609
  }
5233
5610
  }
5234
5611
  else {
5235
5612
  this._disableUpdateCount++;
5613
+ // this.logDebug(
5614
+ // `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...`
5615
+ // );
5236
5616
  // this._disableUpdate = Date.now();
5237
5617
  }
5238
5618
  // return !flag; // return previous value
@@ -5265,12 +5645,14 @@
5265
5645
  return this.extensions.filter.updateFilter();
5266
5646
  }
5267
5647
  }
5268
- Wunderbaum.version = "v0.0.1-0"; // Set to semver by 'grunt release'
5269
5648
  Wunderbaum.sequence = 0;
5649
+ /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
5650
+ Wunderbaum.version = "v0.0.3"; // Set to semver by 'grunt release'
5651
+ /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
5270
5652
  Wunderbaum.util = util;
5271
5653
 
5272
5654
  exports.Wunderbaum = Wunderbaum;
5273
5655
 
5274
5656
  Object.defineProperty(exports, '__esModule', { value: true });
5275
5657
 
5276
- })));
5658
+ }));