wunderbaum 0.0.3 → 0.0.6

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,7 +1,7 @@
1
1
  /*!
2
2
  * Wunderbaum - util
3
3
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
4
- * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
4
+ * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
5
5
  */
6
6
  /** @module util */
7
7
  /** Readable names for `MouseEvent.button` */
@@ -173,7 +173,7 @@ function extractHtmlText(s) {
173
173
  *
174
174
  * If a `<span class="wb-col">` is passed, the first child input is used.
175
175
  * Depending on the target element type, `value` is interpreted accordingly.
176
- * For example for a checkbox, a value of true, false, or null is returned if the
176
+ * For example for a checkbox, a value of true, false, or null is returned if
177
177
  * the element is checked, unchecked, or indeterminate.
178
178
  * For datetime input control a numerical value is assumed, etc.
179
179
  *
@@ -262,6 +262,7 @@ function setValueToElem(elem, value) {
262
262
  if (embeddedInput) {
263
263
  return setValueToElem(embeddedInput, value);
264
264
  }
265
+ // No embedded input: simply write as escaped html
265
266
  span.innerText = "" + value;
266
267
  }
267
268
  else if (tag === "INPUT") {
@@ -269,7 +270,9 @@ function setValueToElem(elem, value) {
269
270
  const type = input.type;
270
271
  switch (type) {
271
272
  case "checkbox":
272
- input.indeterminate = value == null;
273
+ // An explicit `null` value is interpreted as 'indeterminate'.
274
+ // `undefined` is interpreted as 'unchecked'
275
+ input.indeterminate = value === null;
273
276
  input.checked = !!value;
274
277
  break;
275
278
  case "date":
@@ -299,14 +302,30 @@ function setValueToElem(elem, value) {
299
302
  break;
300
303
  case "text":
301
304
  default:
302
- input.innerText = value;
305
+ input.value = value || "";
303
306
  }
304
307
  }
305
308
  else if (tag === "SELECT") {
306
309
  const select = elem;
307
- select.value = value;
310
+ if (value == null) {
311
+ select.selectedIndex = -1;
312
+ }
313
+ else {
314
+ select.value = value;
315
+ }
316
+ }
317
+ }
318
+ /** Show/hide element by setting the `display`style to 'none'. */
319
+ function setElemDisplay(elem, flag) {
320
+ const style = elemFromSelector(elem).style;
321
+ if (flag) {
322
+ if (style.display === "none") {
323
+ style.display = "";
324
+ }
325
+ }
326
+ else if (style.display === "") {
327
+ style.display = "none";
308
328
  }
309
- // return value;
310
329
  }
311
330
  /** Create and return an unconnected `HTMLElement` from a HTML string. */
312
331
  function elemFromHtml(html) {
@@ -479,7 +498,7 @@ function setTimeoutPromise(callback, ms) {
479
498
  return new Promise((resolve, reject) => {
480
499
  setTimeout(() => {
481
500
  try {
482
- resolve(callback.apply(self));
501
+ resolve(callback.apply(this));
483
502
  }
484
503
  catch (err) {
485
504
  reject(err);
@@ -566,13 +585,79 @@ function toSet(val) {
566
585
  }
567
586
  throw new Error("Cannot convert to Set<string>: " + val);
568
587
  }
569
- /**Return a canonical string representation for an object's type (e.g. 'array', 'number', ...) */
588
+ /** Return a canonical string representation for an object's type (e.g. 'array', 'number', ...). */
570
589
  function type(obj) {
571
590
  return Object.prototype.toString
572
591
  .call(obj)
573
592
  .replace(/^\[object (.+)\]$/, "$1")
574
593
  .toLowerCase();
575
594
  }
595
+ /**
596
+ * Return a function that can be called instead of `callback`, but guarantees
597
+ * a limited execution rate.
598
+ * The execution rate is calculated based on the runtime duration of the
599
+ * previous call.
600
+ * Example:
601
+ * ```js
602
+ * throttledFoo = util.adaptiveThrottle(foo.bind(this), {});
603
+ * throttledFoo();
604
+ * throttledFoo();
605
+ * ```
606
+ */
607
+ function adaptiveThrottle(callback, options) {
608
+ let waiting = 0; // Initially, we're not waiting
609
+ let pendingArgs = null;
610
+ const opts = Object.assign({
611
+ minDelay: 16,
612
+ defaultDelay: 200,
613
+ maxDelay: 5000,
614
+ delayFactor: 2.0,
615
+ }, options);
616
+ const minDelay = Math.max(16, +opts.minDelay);
617
+ const maxDelay = +opts.maxDelay;
618
+ const throttledFn = (...args) => {
619
+ if (waiting) {
620
+ pendingArgs = args;
621
+ // console.log(`adaptiveThrottle() queing request #${waiting}...`, args);
622
+ waiting += 1;
623
+ }
624
+ else {
625
+ // Prevent invocations while running or blocking
626
+ waiting = 1;
627
+ const useArgs = args; // pendingArgs || args;
628
+ pendingArgs = null;
629
+ // console.log(`adaptiveThrottle() execute...`, useArgs);
630
+ const start = Date.now();
631
+ try {
632
+ callback.apply(this, useArgs);
633
+ }
634
+ catch (error) {
635
+ console.error(error);
636
+ }
637
+ const elap = Date.now() - start;
638
+ const curDelay = Math.min(Math.max(minDelay, elap * opts.delayFactor), maxDelay);
639
+ const useDelay = Math.max(minDelay, curDelay - elap);
640
+ // console.log(
641
+ // `adaptiveThrottle() calling worker took ${elap}ms. delay = ${curDelay}ms, using ${useDelay}ms`,
642
+ // pendingArgs
643
+ // );
644
+ setTimeout(() => {
645
+ // Unblock, and trigger pending requests if any
646
+ // const skipped = waiting - 1;
647
+ waiting = 0; // And allow future invocations
648
+ if (pendingArgs != null) {
649
+ // There was another request while running or waiting
650
+ // console.log(
651
+ // `adaptiveThrottle() re-trigger (missed ${skipped})...`,
652
+ // pendingArgs
653
+ // );
654
+ throttledFn.apply(this, pendingArgs);
655
+ }
656
+ }, useDelay);
657
+ }
658
+ };
659
+ return throttledFn;
660
+ }
576
661
 
577
662
  var util = /*#__PURE__*/Object.freeze({
578
663
  __proto__: null,
@@ -591,6 +676,7 @@ var util = /*#__PURE__*/Object.freeze({
591
676
  extractHtmlText: extractHtmlText,
592
677
  getValueFromElem: getValueFromElem,
593
678
  setValueToElem: setValueToElem,
679
+ setElemDisplay: setElemDisplay,
594
680
  elemFromHtml: elemFromHtml,
595
681
  elemFromSelector: elemFromSelector,
596
682
  eventTargetFromSelector: eventTargetFromSelector,
@@ -608,29 +694,34 @@ var util = /*#__PURE__*/Object.freeze({
608
694
  toggleCheckbox: toggleCheckbox,
609
695
  getOption: getOption,
610
696
  toSet: toSet,
611
- type: type
697
+ type: type,
698
+ adaptiveThrottle: adaptiveThrottle
612
699
  });
613
700
 
614
701
  /*!
615
- * Wunderbaum - common
702
+ * Wunderbaum - types
616
703
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
617
- * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
704
+ * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
618
705
  */
619
- const DEFAULT_DEBUGLEVEL = 4; // Replaced by rollup script
620
- const ROW_HEIGHT = 22;
621
- const ICON_WIDTH = 20;
622
- const ROW_EXTRA_PAD = 7; // 2x $col-padding-x + 3px rounding errors
623
- const RENDER_MAX_PREFETCH = 5;
624
- const TEST_IMG = new RegExp(/\.|\//); // strings are considered image urls if they contain '.' or '/'
706
+ /** Possible values for `setModified()`. */
625
707
  var ChangeType;
626
708
  (function (ChangeType) {
709
+ /** Re-render the whole viewport, headers, and all rows. */
627
710
  ChangeType["any"] = "any";
711
+ /** Update current row title, icon, columns, and status. */
712
+ ChangeType["data"] = "data";
713
+ /** Redraw the header and update the width of all row columns. */
714
+ ChangeType["header"] = "header";
715
+ /** Re-render the whole current row. */
628
716
  ChangeType["row"] = "row";
717
+ /** Alias for 'any'. */
629
718
  ChangeType["structure"] = "structure";
719
+ /** Update current row's classes, to reflect active, selected, ... */
630
720
  ChangeType["status"] = "status";
721
+ /** Update the 'top' property of all rows. */
631
722
  ChangeType["vscroll"] = "vscroll";
632
- ChangeType["header"] = "header";
633
723
  })(ChangeType || (ChangeType = {}));
724
+ /** Possible values for `setStatus()`. */
634
725
  var NodeStatusType;
635
726
  (function (NodeStatusType) {
636
727
  NodeStatusType["ok"] = "ok";
@@ -639,7 +730,7 @@ var NodeStatusType;
639
730
  NodeStatusType["noData"] = "noData";
640
731
  // paging = "paging",
641
732
  })(NodeStatusType || (NodeStatusType = {}));
642
- /**Define the subregion of a node, where an event occurred. */
733
+ /** Define the subregion of a node, where an event occurred. */
643
734
  var TargetType;
644
735
  (function (TargetType) {
645
736
  TargetType["unknown"] = "";
@@ -650,87 +741,19 @@ var TargetType;
650
741
  TargetType["prefix"] = "prefix";
651
742
  TargetType["title"] = "title";
652
743
  })(TargetType || (TargetType = {}));
653
- let iconMap = {
654
- error: "bi bi-exclamation-triangle",
655
- // loading: "bi bi-hourglass-split",
656
- loading: "bi bi-arrow-repeat wb-spin",
657
- // loading: '<div class="spinner-border spinner-border-sm" role="status"> <span class="visually-hidden">Loading...</span> </div>',
658
- // noData: "bi bi-search",
659
- noData: "bi bi-question-circle",
660
- expanderExpanded: "bi bi-chevron-down",
661
- // expanderExpanded: "bi bi-dash-square",
662
- expanderCollapsed: "bi bi-chevron-right",
663
- // expanderCollapsed: "bi bi-plus-square",
664
- expanderLazy: "bi bi-chevron-right wb-helper-lazy-expander",
665
- // expanderLazy: "bi bi-chevron-bar-right",
666
- checkChecked: "bi bi-check-square",
667
- checkUnchecked: "bi bi-square",
668
- checkUnknown: "bi dash-square-dotted",
669
- radioChecked: "bi bi-circle-fill",
670
- radioUnchecked: "bi bi-circle",
671
- radioUnknown: "bi bi-circle-dotted",
672
- folder: "bi bi-folder2",
673
- folderOpen: "bi bi-folder2-open",
674
- doc: "bi bi-file-earmark",
675
- };
676
- var NavigationModeOption;
677
- (function (NavigationModeOption) {
678
- NavigationModeOption["startRow"] = "startRow";
679
- NavigationModeOption["cell"] = "cell";
680
- NavigationModeOption["startCell"] = "startCell";
681
- NavigationModeOption["row"] = "row";
682
- })(NavigationModeOption || (NavigationModeOption = {}));
683
- var NavigationMode;
684
- (function (NavigationMode) {
685
- NavigationMode["row"] = "row";
686
- NavigationMode["cellNav"] = "cellNav";
687
- NavigationMode["cellEdit"] = "cellEdit";
688
- })(NavigationMode || (NavigationMode = {}));
689
- /** Map `KeyEvent.key` to navigation action. */
690
- const KEY_TO_ACTION_DICT = {
691
- " ": "toggleSelect",
692
- "+": "expand",
693
- Add: "expand",
694
- ArrowDown: "down",
695
- ArrowLeft: "left",
696
- ArrowRight: "right",
697
- ArrowUp: "up",
698
- Backspace: "parent",
699
- "/": "collapseAll",
700
- Divide: "collapseAll",
701
- End: "lastCol",
702
- Home: "firstCol",
703
- "Control+End": "last",
704
- "Control+Home": "first",
705
- "Meta+ArrowDown": "last",
706
- "Meta+ArrowUp": "first",
707
- "*": "expandAll",
708
- Multiply: "expandAll",
709
- PageDown: "pageDown",
710
- PageUp: "pageUp",
711
- "-": "collapse",
712
- Subtract: "collapse",
713
- };
714
- /** */
715
- function makeNodeTitleMatcher(s) {
716
- s = escapeRegex(s.toLowerCase());
717
- return function (node) {
718
- return node.title.toLowerCase().indexOf(s) >= 0;
719
- };
720
- }
721
- /** */
722
- function makeNodeTitleStartMatcher(s) {
723
- s = escapeRegex(s);
724
- const reMatch = new RegExp("^" + s, "i");
725
- return function (node) {
726
- return reMatch.test(node.title);
727
- };
728
- }
744
+ /** Initial navigation mode and possible transition. */
745
+ var NavigationOptions;
746
+ (function (NavigationOptions) {
747
+ NavigationOptions["startRow"] = "startRow";
748
+ NavigationOptions["cell"] = "cell";
749
+ NavigationOptions["startCell"] = "startCell";
750
+ NavigationOptions["row"] = "row";
751
+ })(NavigationOptions || (NavigationOptions = {}));
729
752
 
730
753
  /*!
731
754
  * Wunderbaum - wb_extension_base
732
755
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
733
- * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
756
+ * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
734
757
  */
735
758
  class WunderbaumExtension {
736
759
  constructor(tree, id, defaults) {
@@ -753,14 +776,14 @@ class WunderbaumExtension {
753
776
  init() {
754
777
  this.tree.element.classList.add("wb-ext-" + this.id);
755
778
  }
756
- // protected callEvent(name: string, extra?: any): any {
757
- // let func = this.extensionOpts[name];
779
+ // protected callEvent(type: string, extra?: any): any {
780
+ // let func = this.extensionOpts[type];
758
781
  // if (func) {
759
782
  // return func.call(
760
783
  // this.tree,
761
784
  // util.extend(
762
785
  // {
763
- // event: this.id + "." + name,
786
+ // event: this.id + "." + type,
764
787
  // },
765
788
  // extra
766
789
  // )
@@ -1017,75 +1040,11 @@ function debounce(func, wait = 0, options = {}) {
1017
1040
  debounced.pending = pending;
1018
1041
  return debounced;
1019
1042
  }
1020
- /**
1021
- * Creates a throttled function that only invokes `func` at most once per
1022
- * every `wait` milliseconds (or once per browser frame). The throttled function
1023
- * comes with a `cancel` method to cancel delayed `func` invocations and a
1024
- * `flush` method to immediately invoke them. Provide `options` to indicate
1025
- * whether `func` should be invoked on the leading and/or trailing edge of the
1026
- * `wait` timeout. The `func` is invoked with the last arguments provided to the
1027
- * throttled function. Subsequent calls to the throttled function return the
1028
- * result of the last `func` invocation.
1029
- *
1030
- * **Note:** If `leading` and `trailing` options are `true`, `func` is
1031
- * invoked on the trailing edge of the timeout only if the throttled function
1032
- * is invoked more than once during the `wait` timeout.
1033
- *
1034
- * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
1035
- * until the next tick, similar to `setTimeout` with a timeout of `0`.
1036
- *
1037
- * If `wait` is omitted in an environment with `requestAnimationFrame`, `func`
1038
- * invocation will be deferred until the next frame is drawn (typically about
1039
- * 16ms).
1040
- *
1041
- * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
1042
- * for details over the differences between `throttle` and `debounce`.
1043
- *
1044
- * @since 0.1.0
1045
- * @category Function
1046
- * @param {Function} func The function to throttle.
1047
- * @param {number} [wait=0]
1048
- * The number of milliseconds to throttle invocations to; if omitted,
1049
- * `requestAnimationFrame` is used (if available).
1050
- * @param {Object} [options={}] The options object.
1051
- * @param {boolean} [options.leading=true]
1052
- * Specify invoking on the leading edge of the timeout.
1053
- * @param {boolean} [options.trailing=true]
1054
- * Specify invoking on the trailing edge of the timeout.
1055
- * @returns {Function} Returns the new throttled function.
1056
- * @example
1057
- *
1058
- * // Avoid excessively updating the position while scrolling.
1059
- * jQuery(window).on('scroll', throttle(updatePosition, 100))
1060
- *
1061
- * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.
1062
- * const throttled = throttle(renewToken, 300000, { 'trailing': false })
1063
- * jQuery(element).on('click', throttled)
1064
- *
1065
- * // Cancel the trailing throttled invocation.
1066
- * jQuery(window).on('popstate', throttled.cancel)
1067
- */
1068
- function throttle(func, wait = 0, options = {}) {
1069
- let leading = true;
1070
- let trailing = true;
1071
- if (typeof func !== "function") {
1072
- throw new TypeError("Expected a function");
1073
- }
1074
- if (isObject(options)) {
1075
- leading = "leading" in options ? !!options.leading : leading;
1076
- trailing = "trailing" in options ? !!options.trailing : trailing;
1077
- }
1078
- return debounce(func, wait, {
1079
- leading,
1080
- trailing,
1081
- maxWait: wait,
1082
- });
1083
- }
1084
1043
 
1085
1044
  /*!
1086
1045
  * Wunderbaum - ext-filter
1087
1046
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1088
- * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1047
+ * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
1089
1048
  */
1090
1049
  const START_MARKER = "\uFFF7";
1091
1050
  const END_MARKER = "\uFFF8";
@@ -1094,6 +1053,7 @@ const RE_END_MARTKER = new RegExp(escapeRegex(END_MARKER), "g");
1094
1053
  class FilterExtension extends WunderbaumExtension {
1095
1054
  constructor(tree) {
1096
1055
  super(tree, "filter", {
1056
+ connectInput: null,
1097
1057
  autoApply: true,
1098
1058
  autoExpand: false,
1099
1059
  counter: true,
@@ -1102,22 +1062,32 @@ class FilterExtension extends WunderbaumExtension {
1102
1062
  hideExpanders: false,
1103
1063
  highlight: true,
1104
1064
  leavesOnly: false,
1105
- mode: "hide",
1065
+ mode: "dim",
1106
1066
  noData: true, // Display a 'no data' status node if result is empty
1107
1067
  });
1108
1068
  this.lastFilterArgs = null;
1109
1069
  }
1110
1070
  init() {
1111
1071
  super.init();
1112
- let attachInput = this.getPluginOption("attachInput");
1113
- if (attachInput) {
1114
- this.queryInput = elemFromSelector(attachInput);
1072
+ const connectInput = this.getPluginOption("connectInput");
1073
+ if (connectInput) {
1074
+ this.queryInput = elemFromSelector(connectInput);
1115
1075
  onEvent(this.queryInput, "input", debounce((e) => {
1116
1076
  // this.tree.log("query", e);
1117
1077
  this.filterNodes(this.queryInput.value.trim(), {});
1118
1078
  }, 700));
1119
1079
  }
1120
1080
  }
1081
+ setPluginOption(name, value) {
1082
+ // alert("filter opt=" + name + ", " + value)
1083
+ super.setPluginOption(name, value);
1084
+ switch (name) {
1085
+ case "mode":
1086
+ this.tree.filterMode = value === "hide" ? "hide" : "dim";
1087
+ this.tree.updateFilter();
1088
+ break;
1089
+ }
1090
+ }
1121
1091
  _applyFilterNoUpdate(filter, branchMode, _opts) {
1122
1092
  return this.tree.runWithoutUpdate(() => {
1123
1093
  return this._applyFilterImpl(filter, branchMode, _opts);
@@ -1376,19 +1346,145 @@ function _markFuzzyMatchedChars(text, matches, escapeTitles = true) {
1376
1346
  return textPoses.join("");
1377
1347
  }
1378
1348
 
1349
+ /*!
1350
+ * Wunderbaum - common
1351
+ * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1352
+ * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
1353
+ */
1354
+ const DEFAULT_DEBUGLEVEL = 4; // Replaced by rollup script
1355
+ const ROW_HEIGHT = 22;
1356
+ // export const HEADER_HEIGHT = ROW_HEIGHT;
1357
+ const ICON_WIDTH = 20;
1358
+ const ROW_EXTRA_PAD = 7; // 2x $col-padding-x + 3px rounding errors
1359
+ const RENDER_MAX_PREFETCH = 5;
1360
+ const TEST_IMG = new RegExp(/\.|\//); // strings are considered image urls if they contain '.' or '/'
1361
+ // export const RECURSIVE_REQUEST_ERROR = "$recursive_request";
1362
+ // export const INVALID_REQUEST_TARGET_ERROR = "$request_target_invalid";
1363
+ let iconMap = {
1364
+ error: "bi bi-exclamation-triangle",
1365
+ // loading: "bi bi-hourglass-split wb-busy",
1366
+ loading: "bi bi-chevron-right wb-busy",
1367
+ // loading: "bi bi-arrow-repeat wb-spin",
1368
+ // loading: '<div class="spinner-border spinner-border-sm" role="status"> <span class="visually-hidden">Loading...</span> </div>',
1369
+ // noData: "bi bi-search",
1370
+ noData: "bi bi-question-circle",
1371
+ expanderExpanded: "bi bi-chevron-down",
1372
+ // expanderExpanded: "bi bi-dash-square",
1373
+ expanderCollapsed: "bi bi-chevron-right",
1374
+ // expanderCollapsed: "bi bi-plus-square",
1375
+ expanderLazy: "bi bi-chevron-right wb-helper-lazy-expander",
1376
+ // expanderLazy: "bi bi-chevron-bar-right",
1377
+ checkChecked: "bi bi-check-square",
1378
+ checkUnchecked: "bi bi-square",
1379
+ checkUnknown: "bi dash-square-dotted",
1380
+ radioChecked: "bi bi-circle-fill",
1381
+ radioUnchecked: "bi bi-circle",
1382
+ radioUnknown: "bi bi-circle-dotted",
1383
+ folder: "bi bi-folder2",
1384
+ folderOpen: "bi bi-folder2-open",
1385
+ doc: "bi bi-file-earmark",
1386
+ };
1387
+ /** Dict keys that are evaluated by source loader (others are added to `tree.data` instead). */
1388
+ const RESERVED_TREE_SOURCE_KEYS = new Set([
1389
+ "children",
1390
+ "columns",
1391
+ "format",
1392
+ "keyMap",
1393
+ "positional",
1394
+ "typeList",
1395
+ "types",
1396
+ "version", // reserved for future use
1397
+ ]);
1398
+ /** Key codes that trigger grid navigation, even when inside an input element. */
1399
+ const INPUT_BREAKOUT_KEYS = new Set([
1400
+ // "ArrowDown",
1401
+ // "ArrowUp",
1402
+ "Enter",
1403
+ "Escape",
1404
+ ]);
1405
+ /** Map `KeyEvent.key` to navigation action. */
1406
+ const KEY_TO_ACTION_DICT = {
1407
+ " ": "toggleSelect",
1408
+ "+": "expand",
1409
+ Add: "expand",
1410
+ ArrowDown: "down",
1411
+ ArrowLeft: "left",
1412
+ ArrowRight: "right",
1413
+ ArrowUp: "up",
1414
+ Backspace: "parent",
1415
+ "/": "collapseAll",
1416
+ Divide: "collapseAll",
1417
+ End: "lastCol",
1418
+ Home: "firstCol",
1419
+ "Control+End": "last",
1420
+ "Control+Home": "first",
1421
+ "Meta+ArrowDown": "last",
1422
+ "Meta+ArrowUp": "first",
1423
+ "*": "expandAll",
1424
+ Multiply: "expandAll",
1425
+ PageDown: "pageDown",
1426
+ PageUp: "pageUp",
1427
+ "-": "collapse",
1428
+ Subtract: "collapse",
1429
+ };
1430
+ /** Return a callback that returns true if the node title contains a substring (case-insensitive). */
1431
+ function makeNodeTitleMatcher(s) {
1432
+ s = escapeRegex(s.toLowerCase());
1433
+ return function (node) {
1434
+ return node.title.toLowerCase().indexOf(s) >= 0;
1435
+ };
1436
+ }
1437
+ /** Return a callback that returns true if the node title starts with a string (case-insensitive). */
1438
+ function makeNodeTitleStartMatcher(s) {
1439
+ s = escapeRegex(s);
1440
+ const reMatch = new RegExp("^" + s, "i");
1441
+ return function (node) {
1442
+ return reMatch.test(node.title);
1443
+ };
1444
+ }
1445
+
1379
1446
  /*!
1380
1447
  * Wunderbaum - ext-keynav
1381
1448
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1382
- * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1449
+ * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
1383
1450
  */
1451
+ const QUICKSEARCH_DELAY = 500;
1384
1452
  class KeynavExtension extends WunderbaumExtension {
1385
1453
  constructor(tree) {
1386
1454
  super(tree, "keynav", {});
1387
1455
  }
1456
+ _getEmbeddedInputElem(elem) {
1457
+ var _a;
1458
+ let input = null;
1459
+ if (elem && elem.type != null) {
1460
+ input = elem;
1461
+ }
1462
+ else {
1463
+ // ,[contenteditable]
1464
+ const ace = (_a = this.tree.getActiveColElem()) === null || _a === void 0 ? void 0 : _a.querySelector("input,select");
1465
+ if (ace) {
1466
+ input = ace;
1467
+ }
1468
+ }
1469
+ return input;
1470
+ }
1471
+ /* Return true if the current cell's embedded input has keyboard focus. */
1472
+ _isCurInputFocused() {
1473
+ var _a;
1474
+ const ace = (_a = this.tree
1475
+ .getActiveColElem()) === null || _a === void 0 ? void 0 : _a.querySelector("input:focus,select:focus");
1476
+ console.log(`_isCurInputFocused`, ace);
1477
+ return !!ace;
1478
+ }
1388
1479
  onKeyEvent(data) {
1389
- let event = data.event, eventName = eventToString(event), focusNode, node = data.node, tree = this.tree, opts = data.options, handled = true, activate = !event.ctrlKey || opts.autoActivate;
1390
- const navModeOption = opts.navigationMode;
1391
- tree.logDebug(`onKeyEvent: ${eventName}`);
1480
+ const event = data.event, tree = this.tree, opts = data.options, activate = !event.ctrlKey || opts.autoActivate, curInput = this._getEmbeddedInputElem(event.target), navModeOption = opts.navigationModeOption;
1481
+ // isCellEditMode = tree.navMode === NavigationMode.cellEdit;
1482
+ let focusNode, eventName = eventToString(event), node = data.node, handled = true;
1483
+ tree.log(`onKeyEvent: ${eventName}, curInput`, curInput);
1484
+ if (!tree.isEnabled()) {
1485
+ // tree.logDebug(`onKeyEvent ignored for disabled tree: ${eventName}`);
1486
+ return false;
1487
+ }
1392
1488
  // Let callback prevent default processing
1393
1489
  if (tree._callEvent("keydown", data) === false) {
1394
1490
  return false;
@@ -1399,30 +1495,33 @@ class KeynavExtension extends WunderbaumExtension {
1399
1495
  }
1400
1496
  // Set focus to active (or first node) if no other node has the focus yet
1401
1497
  if (!node) {
1402
- const activeNode = tree.getActiveNode();
1498
+ const currentNode = tree.getFocusNode() || tree.getActiveNode();
1403
1499
  const firstNode = tree.getFirstChild();
1404
- if (!activeNode && firstNode && eventName === "ArrowDown") {
1500
+ if (!currentNode && firstNode && eventName === "ArrowDown") {
1405
1501
  firstNode.logInfo("Keydown: activate first node.");
1406
1502
  firstNode.setActive();
1407
1503
  return;
1408
1504
  }
1409
- focusNode = activeNode || firstNode;
1505
+ focusNode = currentNode || firstNode;
1410
1506
  if (focusNode) {
1411
1507
  focusNode.setFocus();
1412
1508
  node = tree.getFocusNode();
1413
1509
  node.logInfo("Keydown: force focus on active node.");
1414
1510
  }
1415
1511
  }
1416
- if (tree.navMode === NavigationMode.row) {
1512
+ const isColspan = node.isColspan();
1513
+ if (tree.isRowNav()) {
1514
+ // -----------------------------------------------------------------------
1515
+ // --- Row Mode ---
1516
+ // -----------------------------------------------------------------------
1417
1517
  // --- Quick-Search
1418
1518
  if (opts.quicksearch &&
1419
1519
  eventName.length === 1 &&
1420
- /^\w$/.test(eventName)
1421
- // && !$target.is(":input:enabled")
1422
- ) {
1520
+ /^\w$/.test(eventName) &&
1521
+ !curInput) {
1423
1522
  // Allow to search for longer streaks if typed in quickly
1424
1523
  const stamp = Date.now();
1425
- if (stamp - tree.lastQuicksearchTime > 500) {
1524
+ if (stamp - tree.lastQuicksearchTime > QUICKSEARCH_DELAY) {
1426
1525
  tree.lastQuicksearchTerm = "";
1427
1526
  }
1428
1527
  tree.lastQuicksearchTime = stamp;
@@ -1445,8 +1544,8 @@ class KeynavExtension extends WunderbaumExtension {
1445
1544
  if (!node.expanded && (node.children || node.lazy)) {
1446
1545
  eventName = "Add"; // expand
1447
1546
  }
1448
- else if (navModeOption === NavigationModeOption.startRow) {
1449
- tree.setNavigationMode(NavigationMode.cellNav);
1547
+ else if (navModeOption === NavigationOptions.startRow) {
1548
+ tree.setCellNav();
1450
1549
  return;
1451
1550
  }
1452
1551
  break;
@@ -1496,48 +1595,103 @@ class KeynavExtension extends WunderbaumExtension {
1496
1595
  }
1497
1596
  }
1498
1597
  else {
1499
- // Standard navigation (cell mode)
1598
+ const curInput = this._getEmbeddedInputElem(null);
1599
+ const curInputType = curInput ? curInput.type || curInput.tagName : "";
1600
+ const inputHasFocus = curInput && this._isCurInputFocused();
1601
+ const inputCanFocus = curInput && curInputType !== "checkbox";
1602
+ if (inputHasFocus) {
1603
+ if (eventName === "Escape") {
1604
+ // Discard changes
1605
+ node.render();
1606
+ }
1607
+ else if (!INPUT_BREAKOUT_KEYS.has(eventName)) {
1608
+ // Let current `<input>` handle it
1609
+ node.logDebug(`Ignored ${eventName} inside input`);
1610
+ return;
1611
+ }
1612
+ // const curInputType = curInput.type || curInput.tagName;
1613
+ // const breakoutKeys = INPUT_KEYS[curInputType];
1614
+ // if (!breakoutKeys.includes(eventName)) {
1615
+ // node.logDebug(`Ignored ${eventName} inside ${curInputType} input`);
1616
+ // return;
1617
+ // }
1618
+ }
1619
+ else if (curInput) {
1620
+ // On a cell that has an embedded, unfocused <input>
1621
+ if (eventName.length === 1 && inputCanFocus) {
1622
+ curInput.focus();
1623
+ curInput.value = "";
1624
+ node.logDebug(`Focus imput: ${eventName}`);
1625
+ return false;
1626
+ }
1627
+ }
1628
+ if (eventName === "Tab") {
1629
+ eventName = "ArrowRight";
1630
+ handled = true;
1631
+ }
1632
+ else if (eventName === "Shift+Tab") {
1633
+ eventName = tree.activeColIdx > 0 ? "ArrowLeft" : "";
1634
+ handled = true;
1635
+ }
1636
+ else ;
1500
1637
  switch (eventName) {
1501
1638
  case " ":
1502
1639
  if (tree.activeColIdx === 0 && node.getOption("checkbox")) {
1503
1640
  node.setSelected(!node.isSelected());
1504
1641
  handled = true;
1505
1642
  }
1506
- else {
1507
- // [Space] key should trigger embedded checkbox
1508
- const elem = tree.getActiveColElem();
1509
- const cb = elem === null || elem === void 0 ? void 0 : elem.querySelector("input[type=checkbox]");
1510
- cb === null || cb === void 0 ? void 0 : cb.click();
1643
+ else if (curInput && curInputType === "checkbox") {
1644
+ curInput.click();
1645
+ // toggleCheckbox(curInput)
1646
+ // new Event("change")
1647
+ // curInput.change
1648
+ handled = true;
1649
+ }
1650
+ break;
1651
+ case "F2":
1652
+ if (curInput && !inputHasFocus && inputCanFocus) {
1653
+ curInput.focus();
1654
+ handled = true;
1511
1655
  }
1512
1656
  break;
1513
1657
  case "Enter":
1658
+ tree.setFocus(); // Blur prev. input if any
1514
1659
  if (tree.activeColIdx === 0 && node.isExpandable()) {
1515
1660
  node.setExpanded(!node.isExpanded());
1516
1661
  handled = true;
1517
1662
  }
1518
- break;
1519
- case "Escape":
1520
- if (tree.navMode === NavigationMode.cellEdit) {
1521
- tree.setNavigationMode(NavigationMode.cellNav);
1663
+ else if (curInput && !inputHasFocus && inputCanFocus) {
1664
+ curInput.focus();
1522
1665
  handled = true;
1523
1666
  }
1524
- else if (tree.navMode === NavigationMode.cellNav) {
1525
- tree.setNavigationMode(NavigationMode.row);
1667
+ break;
1668
+ case "Escape":
1669
+ tree.setFocus(); // Blur prev. input if any
1670
+ if (tree.isCellNav() && navModeOption !== NavigationOptions.cell) {
1671
+ tree.setCellNav(false); // row-nav mode
1526
1672
  handled = true;
1527
1673
  }
1528
1674
  break;
1529
1675
  case "ArrowLeft":
1530
- if (tree.activeColIdx > 0) {
1676
+ tree.setFocus(); // Blur prev. input if any
1677
+ if (isColspan && node.isExpanded()) {
1678
+ node.setExpanded(false);
1679
+ }
1680
+ else if (tree.activeColIdx > 0) {
1531
1681
  tree.setColumn(tree.activeColIdx - 1);
1532
1682
  handled = true;
1533
1683
  }
1534
- else if (navModeOption !== NavigationModeOption.cell) {
1535
- tree.setNavigationMode(NavigationMode.row);
1684
+ else if (navModeOption !== NavigationOptions.cell) {
1685
+ tree.setCellNav(false); // row-nav mode
1536
1686
  handled = true;
1537
1687
  }
1538
1688
  break;
1539
1689
  case "ArrowRight":
1540
- if (tree.activeColIdx < tree.columns.length - 1) {
1690
+ tree.setFocus(); // Blur prev. input if any
1691
+ if (isColspan && !node.isExpanded()) {
1692
+ node.setExpanded();
1693
+ }
1694
+ else if (tree.activeColIdx < tree.columns.length - 1) {
1541
1695
  tree.setColumn(tree.activeColIdx + 1);
1542
1696
  handled = true;
1543
1697
  }
@@ -1554,6 +1708,10 @@ class KeynavExtension extends WunderbaumExtension {
1554
1708
  case "PageDown":
1555
1709
  case "PageUp":
1556
1710
  node.navigate(eventName, { activate: activate, event: event });
1711
+ // if (isCellEditMode) {
1712
+ // this._getEmbeddedInputElem(null, true); // set focus to input
1713
+ // }
1714
+ handled = true;
1557
1715
  break;
1558
1716
  default:
1559
1717
  handled = false;
@@ -1569,7 +1727,7 @@ class KeynavExtension extends WunderbaumExtension {
1569
1727
  /*!
1570
1728
  * Wunderbaum - ext-logger
1571
1729
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1572
- * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1730
+ * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
1573
1731
  */
1574
1732
  class LoggerExtension extends WunderbaumExtension {
1575
1733
  constructor(tree) {
@@ -1609,7 +1767,7 @@ class LoggerExtension extends WunderbaumExtension {
1609
1767
  /*!
1610
1768
  * Wunderbaum - ext-dnd
1611
1769
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1612
- * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
1770
+ * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
1613
1771
  */
1614
1772
  const nodeMimeType = "application/x-wunderbaum-node";
1615
1773
  class DndExtension extends WunderbaumExtension {
@@ -1679,7 +1837,7 @@ class DndExtension extends WunderbaumExtension {
1679
1837
  const ltn = this.lastTargetNode;
1680
1838
  this.lastEnterStamp = 0;
1681
1839
  if (ltn) {
1682
- ltn.removeClass("wb-drop-target wb-drop-over wb-drop-after wb-drop-before");
1840
+ ltn.setClass("wb-drop-target wb-drop-over wb-drop-after wb-drop-before", false);
1683
1841
  this.lastTargetNode = null;
1684
1842
  }
1685
1843
  }
@@ -1723,7 +1881,7 @@ class DndExtension extends WunderbaumExtension {
1723
1881
  }
1724
1882
  /* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */
1725
1883
  autoScroll(event) {
1726
- let tree = this.tree, dndOpts = tree.options.dnd, sp = tree.scrollContainer, sensitivity = dndOpts.scrollSensitivity, speed = dndOpts.scrollSpeed, scrolled = 0;
1884
+ let tree = this.tree, dndOpts = tree.options.dnd, sp = tree.scrollContainerElement, sensitivity = dndOpts.scrollSensitivity, speed = dndOpts.scrollSpeed, scrolled = 0;
1727
1885
  const scrollTop = sp.offsetTop;
1728
1886
  if (scrollTop + sp.offsetHeight - event.pageY < sensitivity) {
1729
1887
  const delta = sp.scrollHeight - sp.clientHeight - scrollTop;
@@ -1773,13 +1931,13 @@ class DndExtension extends WunderbaumExtension {
1773
1931
  setTimeout(() => {
1774
1932
  // Decouple this call, so the CSS is applied to the node, but not to
1775
1933
  // the system generated drag image
1776
- srcNode.addClass("wb-drag-source");
1934
+ srcNode.setClass("wb-drag-source");
1777
1935
  }, 0);
1778
1936
  // --- drag ---
1779
1937
  }
1780
1938
  else if (e.type === "drag") ;
1781
1939
  else if (e.type === "dragend") {
1782
- srcNode.removeClass("wb-drag-source");
1940
+ srcNode.setClass("wb-drag-source", false);
1783
1941
  this.srcNode = null;
1784
1942
  if (this.lastTargetNode) {
1785
1943
  this._leaveNode();
@@ -1833,7 +1991,7 @@ class DndExtension extends WunderbaumExtension {
1833
1991
  }
1834
1992
  this.lastAllowedDropRegions = regionSet;
1835
1993
  this.lastDropEffect = dt.dropEffect;
1836
- targetNode.addClass("wb-drop-target");
1994
+ targetNode.setClass("wb-drop-target");
1837
1995
  e.preventDefault(); // Allow drop (Drop operation is denied by default)
1838
1996
  return false;
1839
1997
  // --- dragover ---
@@ -1852,9 +2010,9 @@ class DndExtension extends WunderbaumExtension {
1852
2010
  if (!region) {
1853
2011
  return; // We already rejected in dragenter
1854
2012
  }
1855
- targetNode.toggleClass("wb-drop-over", region === "over");
1856
- targetNode.toggleClass("wb-drop-before", region === "before");
1857
- targetNode.toggleClass("wb-drop-after", region === "after");
2013
+ targetNode.setClass("wb-drop-over", region === "over");
2014
+ targetNode.setClass("wb-drop-before", region === "before");
2015
+ targetNode.setClass("wb-drop-after", region === "after");
1858
2016
  // console.log("dragover", e);
1859
2017
  // dt.dropEffect = this.lastDropEffect!;
1860
2018
  e.preventDefault(); // Allow drop (Drop operation is denied by default)
@@ -1877,7 +2035,7 @@ class DndExtension extends WunderbaumExtension {
1877
2035
  /*!
1878
2036
  * Wunderbaum - drag_observer
1879
2037
  * 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)
2038
+ * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
1881
2039
  */
1882
2040
  /**
1883
2041
  * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'.
@@ -2011,7 +2169,7 @@ class DragObserver {
2011
2169
  /*!
2012
2170
  * Wunderbaum - ext-grid
2013
2171
  * 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)
2172
+ * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
2015
2173
  */
2016
2174
  class GridExtension extends WunderbaumExtension {
2017
2175
  constructor(tree) {
@@ -2048,12 +2206,22 @@ class GridExtension extends WunderbaumExtension {
2048
2206
  /*!
2049
2207
  * Wunderbaum - deferred
2050
2208
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
2051
- * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
2209
+ * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
2052
2210
  */
2053
2211
  /**
2054
- * Deferred is a ES6 Promise, that exposes the resolve() and reject()` method.
2212
+ * Implement a ES6 Promise, that exposes a resolve() and reject() method.
2055
2213
  *
2056
- * Loosely mimics [`jQuery.Deferred`](https://api.jquery.com/category/deferred-object/).
2214
+ * Loosely mimics {@link https://api.jquery.com/category/deferred-object/ | jQuery.Deferred}.
2215
+ * Example:
2216
+ * ```js
2217
+ * function foo() {
2218
+ * let dfd = new Deferred(),
2219
+ * ...
2220
+ * dfd.resolve('foo')
2221
+ * ...
2222
+ * return dfd.promise();
2223
+ * }
2224
+ * ```
2057
2225
  */
2058
2226
  class Deferred {
2059
2227
  constructor() {
@@ -2091,7 +2259,7 @@ class Deferred {
2091
2259
  /*!
2092
2260
  * Wunderbaum - wunderbaum_node
2093
2261
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
2094
- * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
2262
+ * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
2095
2263
  */
2096
2264
  /** Top-level properties that can be passed with `data`. */
2097
2265
  const NODE_PROPS = new Set([
@@ -2110,7 +2278,7 @@ const NODE_PROPS = new Set([
2110
2278
  const NODE_ATTRS = new Set([
2111
2279
  "checkbox",
2112
2280
  "expanded",
2113
- "extraClasses",
2281
+ "classes",
2114
2282
  "folder",
2115
2283
  "icon",
2116
2284
  "iconTooltip",
@@ -2152,8 +2320,8 @@ class WunderbaumNode {
2152
2320
  * @see {@link isSelected}, {@link setSelected}. */
2153
2321
  this.selected = false;
2154
2322
  /** Additional classes added to `div.wb-row`.
2155
- * @see {@link addClass}, {@link removeClass}, {@link toggleClass}. */
2156
- this.extraClasses = new Set();
2323
+ * @see {@link hasClass}, {@link setClass}. */
2324
+ this.classes = null; //new Set<string>();
2157
2325
  /** Custom data that was passed to the constructor */
2158
2326
  this.data = {};
2159
2327
  this._isLoading = false;
@@ -2182,9 +2350,7 @@ class WunderbaumNode {
2182
2350
  this.lazy = data.lazy === true;
2183
2351
  this.selected = data.selected === true;
2184
2352
  if (data.classes) {
2185
- for (const c of data.classes.split(" ")) {
2186
- this.extraClasses.add(c.trim());
2187
- }
2353
+ this.setClass(data.classes);
2188
2354
  }
2189
2355
  // Store custom fields as `node.data`
2190
2356
  for (const [key, value] of Object.entries(data)) {
@@ -2202,7 +2368,7 @@ class WunderbaumNode {
2202
2368
  * @internal
2203
2369
  */
2204
2370
  toString() {
2205
- return "WunderbaumNode@" + this.key + "<'" + this.title + "'>";
2371
+ return `WunderbaumNode@${this.key}<'${this.title}'>`;
2206
2372
  }
2207
2373
  // /** Return an option value. */
2208
2374
  // protected _getOpt(
@@ -2225,8 +2391,8 @@ class WunderbaumNode {
2225
2391
  * node._callEvent("edit.beforeEdit", {foo: 42})
2226
2392
  * ```
2227
2393
  */
2228
- _callEvent(name, extra) {
2229
- return this.tree._callEvent(name, extend({
2394
+ _callEvent(type, extra) {
2395
+ return this.tree._callEvent(type, extend({
2230
2396
  node: this,
2231
2397
  typeInfo: this.type ? this.tree.types[this.type] : {},
2232
2398
  }, extra));
@@ -2324,31 +2490,41 @@ class WunderbaumNode {
2324
2490
  applyCommand(cmd, opts) {
2325
2491
  return this.tree.applyCommand(cmd, this, opts);
2326
2492
  }
2327
- addClass(className) {
2328
- const cnSet = toSet(className);
2329
- cnSet.forEach((cn) => {
2330
- var _a;
2331
- this.extraClasses.add(cn);
2332
- (_a = this._rowElem) === null || _a === void 0 ? void 0 : _a.classList.add(cn);
2333
- });
2334
- }
2335
- removeClass(className) {
2336
- const cnSet = toSet(className);
2337
- cnSet.forEach((cn) => {
2338
- var _a;
2339
- this.extraClasses.delete(cn);
2340
- (_a = this._rowElem) === null || _a === void 0 ? void 0 : _a.classList.remove(cn);
2341
- });
2342
- }
2343
- toggleClass(className, flag) {
2493
+ /**
2494
+ * Add/remove one or more classes to `<div class='wb-row'>`.
2495
+ *
2496
+ * This also maintains `node.classes`, so the class will survive a re-render.
2497
+ *
2498
+ * @param className one or more class names. Multiple classes can be passed
2499
+ * as space-separated string, array of strings, or set of strings.
2500
+ */
2501
+ setClass(className, flag = true) {
2344
2502
  const cnSet = toSet(className);
2345
- cnSet.forEach((cn) => {
2346
- var _a;
2347
- flag ? this.extraClasses.add(cn) : this.extraClasses.delete(cn);
2348
- (_a = this._rowElem) === null || _a === void 0 ? void 0 : _a.classList.toggle(cn, flag);
2349
- });
2503
+ if (flag) {
2504
+ if (this.classes === null) {
2505
+ this.classes = new Set();
2506
+ }
2507
+ cnSet.forEach((cn) => {
2508
+ var _a;
2509
+ this.classes.add(cn);
2510
+ (_a = this._rowElem) === null || _a === void 0 ? void 0 : _a.classList.toggle(cn, flag);
2511
+ });
2512
+ }
2513
+ else {
2514
+ if (this.classes === null) {
2515
+ return;
2516
+ }
2517
+ cnSet.forEach((cn) => {
2518
+ var _a;
2519
+ this.classes.delete(cn);
2520
+ (_a = this._rowElem) === null || _a === void 0 ? void 0 : _a.classList.toggle(cn, flag);
2521
+ });
2522
+ if (this.classes.size === 0) {
2523
+ this.classes = null;
2524
+ }
2525
+ }
2350
2526
  }
2351
- /** */
2527
+ /** Call `setExpanded()` on al child nodes*/
2352
2528
  async expandAll(flag = true) {
2353
2529
  this.visit((node) => {
2354
2530
  node.setExpanded(flag);
@@ -2520,6 +2696,10 @@ class WunderbaumNode {
2520
2696
  }
2521
2697
  return !!(this.children && this.children.length);
2522
2698
  }
2699
+ /** Return true if node has className set. */
2700
+ hasClass(className) {
2701
+ return this.classes ? this.classes.has(className) : false;
2702
+ }
2523
2703
  /** Return true if this node is the currently active tree node. */
2524
2704
  isActive() {
2525
2705
  return this.tree.activeNode === this;
@@ -2530,6 +2710,12 @@ class WunderbaumNode {
2530
2710
  isChildOf(other) {
2531
2711
  return this.parent && this.parent === other;
2532
2712
  }
2713
+ /** Return true if this node's title spans all columns, i.e. the node has no
2714
+ * grid cells.
2715
+ */
2716
+ isColspan() {
2717
+ return !!this.getOption("colspan");
2718
+ }
2533
2719
  /** Return true if this node is a direct or indirect sub node of `other`.
2534
2720
  * (See also [[isChildOf]].)
2535
2721
  */
@@ -2665,29 +2851,46 @@ class WunderbaumNode {
2665
2851
  assert(isPlainObject(source));
2666
2852
  assert(source.children, "If `source` is an object, it must have a `children` property");
2667
2853
  if (source.types) {
2668
- // TODO: convert types.classes to Set()
2669
- extend(tree.types, source.types);
2854
+ tree.logInfo("Redefine types", source.columns);
2855
+ tree.setTypes(source.types, false);
2856
+ delete source.types;
2857
+ }
2858
+ if (source.columns) {
2859
+ tree.logInfo("Redefine columns", source.columns);
2860
+ tree.columns = source.columns;
2861
+ delete source.columns;
2862
+ tree.updateColumns({ calculateCols: false });
2670
2863
  }
2671
2864
  this.addChildren(source.children);
2865
+ delete source.columns;
2866
+ // Add extra data to `tree.data`
2867
+ for (const [key, value] of Object.entries(source)) {
2868
+ if (!RESERVED_TREE_SOURCE_KEYS.has(key)) {
2869
+ tree.data[key] = value;
2870
+ tree.logDebug(`Add source.${key} to tree.data.${key}`);
2871
+ }
2872
+ }
2672
2873
  this._callEvent("load");
2673
2874
  }
2674
2875
  /** Download data from the cloud, then call `.update()`. */
2675
2876
  async load(source) {
2676
2877
  const tree = this.tree;
2677
- // const opts = tree.options;
2678
2878
  const requestId = Date.now();
2679
2879
  const prevParent = this.parent;
2680
2880
  const url = typeof source === "string" ? source : source.url;
2881
+ const start = Date.now();
2882
+ let elap = 0, elapLoad = 0, elapProcess = 0;
2681
2883
  // Check for overlapping requests
2682
2884
  if (this._requestId) {
2683
2885
  this.logWarn(`Recursive load request #${requestId} while #${this._requestId} is pending.`);
2684
2886
  // node.debug("Send load request #" + requestId);
2685
2887
  }
2686
2888
  this._requestId = requestId;
2687
- const timerLabel = tree.logTime(this + ".load()");
2889
+ // const timerLabel = tree.logTime(this + ".load()");
2688
2890
  try {
2689
2891
  if (!url) {
2690
2892
  this._loadSourceObject(source);
2893
+ elapProcess = Date.now() - start;
2691
2894
  }
2692
2895
  else {
2693
2896
  this.setStatus(NodeStatusType.loading);
@@ -2696,6 +2899,7 @@ class WunderbaumNode {
2696
2899
  error(`GET ${url} returned ${response.status}, ${response}`);
2697
2900
  }
2698
2901
  const data = await response.json();
2902
+ elapLoad = Date.now() - start;
2699
2903
  if (this._requestId && this._requestId > requestId) {
2700
2904
  this.logWarn(`Ignored load response #${requestId} because #${this._requestId} is pending.`);
2701
2905
  return;
@@ -2708,25 +2912,30 @@ class WunderbaumNode {
2708
2912
  return;
2709
2913
  }
2710
2914
  this.setStatus(NodeStatusType.ok);
2711
- if (data.columns) {
2712
- tree.logInfo("Re-define columns", data.columns);
2713
- assert(!this.parent);
2714
- tree.columns = data.columns;
2715
- delete data.columns;
2716
- tree.updateColumns({ calculateCols: false });
2717
- }
2915
+ // if (data.columns) {
2916
+ // tree.logInfo("Re-define columns", data.columns);
2917
+ // util.assert(!this.parent);
2918
+ // tree.columns = data.columns;
2919
+ // delete data.columns;
2920
+ // tree.updateColumns({ calculateCols: false });
2921
+ // }
2922
+ const startProcess = Date.now();
2718
2923
  this._loadSourceObject(data);
2924
+ elapProcess = Date.now() - startProcess;
2719
2925
  }
2720
2926
  }
2721
2927
  catch (error) {
2722
2928
  this.logError("Error during load()", source, error);
2723
2929
  this._callEvent("error", { error: error });
2724
- this.setStatus(NodeStatusType.error, "" + error);
2930
+ this.setStatus(NodeStatusType.error, { message: "" + error });
2725
2931
  throw error;
2726
2932
  }
2727
2933
  finally {
2728
2934
  this._requestId = 0;
2729
- tree.logTimeEnd(timerLabel);
2935
+ elap = Date.now() - start;
2936
+ if (tree.options.debugLevel >= 3) {
2937
+ tree.logInfo(`Load source took ${elap / 1000} seconds (transfer: ${elapLoad / 1000}s, processing: ${elapProcess / 1000}s)`);
2938
+ }
2730
2939
  }
2731
2940
  }
2732
2941
  /**Load content of a lazy node. */
@@ -2762,7 +2971,7 @@ class WunderbaumNode {
2762
2971
  catch (e) {
2763
2972
  this.logError("Error during loadLazy()", e);
2764
2973
  this._callEvent("error", { error: e });
2765
- this.setStatus(NodeStatusType.error, "" + e);
2974
+ this.setStatus(NodeStatusType.error, { message: "" + e });
2766
2975
  }
2767
2976
  return;
2768
2977
  }
@@ -2804,17 +3013,23 @@ class WunderbaumNode {
2804
3013
  * Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true}
2805
3014
  */
2806
3015
  async makeVisible(opts) {
2807
- let i, dfd = new Deferred(), deferreds = [], parents = this.getParentList(false, false), len = parents.length, effects = !(opts && opts.noAnimation === true), scroll = !(opts && opts.scrollIntoView === false);
3016
+ let i, dfd = new Deferred(), deferreds = [], parents = this.getParentList(false, false), len = parents.length,
3017
+ // effects = !(opts && opts.noAnimation === true),
3018
+ scroll = !(opts && opts.scrollIntoView === false);
2808
3019
  // Expand bottom-up, so only the top node is animated
2809
3020
  for (i = len - 1; i >= 0; i--) {
2810
3021
  // self.debug("pushexpand" + parents[i]);
2811
- deferreds.push(parents[i].setExpanded(true, opts));
3022
+ const seOpts = { noAnimation: opts === null || opts === void 0 ? void 0 : opts.noAnimation };
3023
+ deferreds.push(parents[i].setExpanded(true, seOpts));
2812
3024
  }
2813
3025
  Promise.all(deferreds).then(() => {
2814
3026
  // All expands have finished
2815
3027
  // self.debug("expand DONE", scroll);
2816
- if (scroll) {
2817
- this.scrollIntoView(effects).then(() => {
3028
+ // Note: this.tree may be none when switching demo trees
3029
+ if (scroll && this.tree) {
3030
+ // Make sure markup and _rowIdx is updated before we do the scroll calculations
3031
+ this.tree.updatePendingModifications();
3032
+ this.scrollIntoView().then(() => {
2818
3033
  // self.debug("scroll DONE");
2819
3034
  dfd.resolve();
2820
3035
  });
@@ -2999,21 +3214,30 @@ class WunderbaumNode {
2999
3214
  }
3000
3215
  }
3001
3216
  _getRenderInfo() {
3002
- let colInfosById = {};
3003
- let idx = 0;
3004
- let colElems = this._rowElem
3217
+ const allColInfosById = {};
3218
+ const renderColInfosById = {};
3219
+ const isColspan = this.isColspan();
3220
+ const colElems = this._rowElem
3005
3221
  ? (this._rowElem.querySelectorAll("span.wb-col"))
3006
3222
  : null;
3223
+ let idx = 0;
3007
3224
  for (let col of this.tree.columns) {
3008
- colInfosById[col.id] = {
3225
+ allColInfosById[col.id] = {
3009
3226
  id: col.id,
3010
3227
  idx: idx,
3011
3228
  elem: colElems ? colElems[idx] : null,
3012
3229
  info: col,
3013
3230
  };
3231
+ // renderColInfosById only contains columns that need rendering:
3232
+ if (!isColspan && col.id !== "*") {
3233
+ renderColInfosById[col.id] = allColInfosById[col.id];
3234
+ }
3014
3235
  idx++;
3015
3236
  }
3016
- return colInfosById;
3237
+ return {
3238
+ allColInfosById: allColInfosById,
3239
+ renderColInfosById: renderColInfosById,
3240
+ };
3017
3241
  }
3018
3242
  _createIcon(parentElem, replaceChild) {
3019
3243
  let iconSpan;
@@ -3060,30 +3284,193 @@ class WunderbaumNode {
3060
3284
  else {
3061
3285
  parentElem.appendChild(iconSpan);
3062
3286
  }
3063
- // this.log("_createIcon: ", iconSpan);
3064
- return iconSpan;
3287
+ // this.log("_createIcon: ", iconSpan);
3288
+ return iconSpan;
3289
+ }
3290
+ /**
3291
+ * Create a whole new `<div class="wb-row">` element.
3292
+ * @see {@link WunderbaumNode.render}
3293
+ */
3294
+ _render_markup(opts) {
3295
+ const tree = this.tree;
3296
+ const treeOptions = tree.options;
3297
+ const checkbox = this.getOption("checkbox") !== false;
3298
+ const columns = tree.columns;
3299
+ const level = this.getLevel();
3300
+ let elem;
3301
+ let nodeElem;
3302
+ let rowDiv = this._rowElem;
3303
+ let titleSpan;
3304
+ let checkboxSpan = null;
3305
+ let iconSpan;
3306
+ let expanderSpan = null;
3307
+ const activeColIdx = tree.isRowNav() ? null : tree.activeColIdx;
3308
+ const isNew = !rowDiv;
3309
+ assert(isNew);
3310
+ assert(!isNew || (opts && opts.after), "opts.after expected, unless updating");
3311
+ assert(!this.isRootNode());
3312
+ rowDiv = document.createElement("div");
3313
+ rowDiv.classList.add("wb-row");
3314
+ rowDiv.style.top = this._rowIdx * ROW_HEIGHT + "px";
3315
+ this._rowElem = rowDiv;
3316
+ // Attach a node reference to the DOM Element:
3317
+ rowDiv._wb_node = this;
3318
+ nodeElem = document.createElement("span");
3319
+ nodeElem.classList.add("wb-node", "wb-col");
3320
+ rowDiv.appendChild(nodeElem);
3321
+ let ofsTitlePx = 0;
3322
+ if (checkbox) {
3323
+ checkboxSpan = document.createElement("i");
3324
+ checkboxSpan.classList.add("wb-checkbox");
3325
+ nodeElem.appendChild(checkboxSpan);
3326
+ ofsTitlePx += ICON_WIDTH;
3327
+ }
3328
+ for (let i = level - 1; i > 0; i--) {
3329
+ elem = document.createElement("i");
3330
+ elem.classList.add("wb-indent");
3331
+ nodeElem.appendChild(elem);
3332
+ ofsTitlePx += ICON_WIDTH;
3333
+ }
3334
+ if (!treeOptions.minExpandLevel || level > treeOptions.minExpandLevel) {
3335
+ expanderSpan = document.createElement("i");
3336
+ expanderSpan.classList.add("wb-expander");
3337
+ nodeElem.appendChild(expanderSpan);
3338
+ ofsTitlePx += ICON_WIDTH;
3339
+ }
3340
+ iconSpan = this._createIcon(nodeElem);
3341
+ if (iconSpan) {
3342
+ ofsTitlePx += ICON_WIDTH;
3343
+ }
3344
+ titleSpan = document.createElement("span");
3345
+ titleSpan.classList.add("wb-title");
3346
+ nodeElem.appendChild(titleSpan);
3347
+ this._callEvent("enhanceTitle", { titleSpan: titleSpan });
3348
+ // Store the width of leading icons with the node, so we can calculate
3349
+ // the width of the embedded title span later
3350
+ nodeElem._ofsTitlePx = ofsTitlePx;
3351
+ // Support HTML5 drag-n-drop
3352
+ if (tree.options.dnd.dragStart) {
3353
+ nodeElem.draggable = true;
3354
+ }
3355
+ // Render columns
3356
+ const isColspan = this.isColspan();
3357
+ if (!isColspan && columns.length > 1) {
3358
+ let colIdx = 0;
3359
+ for (let col of columns) {
3360
+ colIdx++;
3361
+ let colElem;
3362
+ if (col.id === "*") {
3363
+ colElem = nodeElem;
3364
+ }
3365
+ else {
3366
+ colElem = document.createElement("span");
3367
+ colElem.classList.add("wb-col");
3368
+ rowDiv.appendChild(colElem);
3369
+ }
3370
+ if (colIdx === activeColIdx) {
3371
+ colElem.classList.add("wb-active");
3372
+ }
3373
+ // Add classes from `columns` definition to `<div.wb-col>` cells
3374
+ col.classes ? colElem.classList.add(...col.classes.split(" ")) : 0;
3375
+ colElem.style.left = col._ofsPx + "px";
3376
+ colElem.style.width = col._widthPx + "px";
3377
+ if (isNew && col.html) {
3378
+ if (typeof col.html === "string") {
3379
+ colElem.innerHTML = col.html;
3380
+ }
3381
+ }
3382
+ }
3383
+ }
3384
+ // Attach to DOM as late as possible
3385
+ const after = opts ? opts.after : "last";
3386
+ switch (after) {
3387
+ case "first":
3388
+ tree.nodeListElement.prepend(rowDiv);
3389
+ break;
3390
+ case "last":
3391
+ tree.nodeListElement.appendChild(rowDiv);
3392
+ break;
3393
+ default:
3394
+ opts.after.after(rowDiv);
3395
+ }
3396
+ // Now go on and fill in data and update classes
3397
+ opts.isNew = true;
3398
+ this._render_data(opts);
3399
+ }
3400
+ /**
3401
+ * Render `node.title`, `.icon` into an existing row.
3402
+ *
3403
+ * @see {@link WunderbaumNode.render}
3404
+ */
3405
+ _render_data(opts) {
3406
+ assert(this._rowElem);
3407
+ const tree = this.tree;
3408
+ const treeOptions = tree.options;
3409
+ const rowDiv = this._rowElem;
3410
+ const isNew = !!opts.isNew; // Called by _render_markup()?
3411
+ const columns = tree.columns;
3412
+ const isColspan = this.isColspan();
3413
+ // Row markup already exists
3414
+ const nodeElem = rowDiv.querySelector("span.wb-node");
3415
+ const titleSpan = nodeElem.querySelector("span.wb-title");
3416
+ if (this.titleWithHighlight) {
3417
+ titleSpan.innerHTML = this.titleWithHighlight;
3418
+ }
3419
+ else {
3420
+ titleSpan.textContent = this.title;
3421
+ }
3422
+ // Set the width of the title span, so overflow ellipsis work
3423
+ if (!treeOptions.skeleton) {
3424
+ if (isColspan) {
3425
+ let vpWidth = tree.element.clientWidth;
3426
+ titleSpan.style.width =
3427
+ vpWidth - nodeElem._ofsTitlePx - ROW_EXTRA_PAD + "px";
3428
+ }
3429
+ else {
3430
+ titleSpan.style.width =
3431
+ columns[0]._widthPx -
3432
+ nodeElem._ofsTitlePx -
3433
+ ROW_EXTRA_PAD +
3434
+ "px";
3435
+ }
3436
+ }
3437
+ // Update row classes
3438
+ opts.isDataChange = true;
3439
+ this._render_status(opts);
3440
+ // Let user modify the result
3441
+ if (this.statusNodeType) {
3442
+ this._callEvent("renderStatusNode", {
3443
+ isNew: isNew,
3444
+ nodeElem: nodeElem,
3445
+ });
3446
+ }
3447
+ else if (this.parent) {
3448
+ // Skip root node
3449
+ const renderInfo = this._getRenderInfo();
3450
+ this._callEvent("render", {
3451
+ isNew: isNew,
3452
+ isColspan: isColspan,
3453
+ // isDataChange: true,
3454
+ nodeElem: nodeElem,
3455
+ allColInfosById: renderInfo.allColInfosById,
3456
+ renderColInfosById: renderInfo.renderColInfosById,
3457
+ });
3458
+ }
3065
3459
  }
3066
- /** Create HTML markup for this node, i.e. the whole row. */
3067
- render(opts) {
3460
+ /**
3461
+ * Update row classes to reflect active, focuses, etc.
3462
+ * @see {@link WunderbaumNode.render}
3463
+ */
3464
+ _render_status(opts) {
3465
+ // this.log("_render_status", opts);
3068
3466
  const tree = this.tree;
3069
3467
  const treeOptions = tree.options;
3070
- const checkbox = this.getOption("checkbox") !== false;
3071
- const columns = tree.columns;
3072
3468
  const typeInfo = this.type ? tree.types[this.type] : null;
3073
- const level = this.getLevel();
3074
- let elem;
3075
- let nodeElem;
3076
- let rowDiv = this._rowElem;
3077
- let titleSpan;
3078
- let checkboxSpan = null;
3079
- let iconSpan;
3080
- let expanderSpan = null;
3081
- const activeColIdx = tree.navMode === NavigationMode.row ? null : tree.activeColIdx;
3082
- // let colElems: HTMLElement[];
3083
- const isNew = !rowDiv;
3084
- assert(!isNew || (opts && opts.after), "opts.after expected, unless updating");
3085
- assert(!this.isRootNode());
3086
- //
3469
+ const rowDiv = this._rowElem;
3470
+ // Row markup already exists
3471
+ const nodeElem = rowDiv.querySelector("span.wb-node");
3472
+ const expanderSpan = nodeElem.querySelector("i.wb-expander");
3473
+ const checkboxSpan = nodeElem.querySelector("i.wb-checkbox");
3087
3474
  let rowClasses = ["wb-row"];
3088
3475
  this.expanded ? rowClasses.push("wb-expanded") : 0;
3089
3476
  this.lazy ? rowClasses.push("wb-lazy") : 0;
@@ -3092,108 +3479,21 @@ class WunderbaumNode {
3092
3479
  this === tree.focusNode ? rowClasses.push("wb-focus") : 0;
3093
3480
  this._errorInfo ? rowClasses.push("wb-error") : 0;
3094
3481
  this._isLoading ? rowClasses.push("wb-loading") : 0;
3482
+ this.isColspan() ? rowClasses.push("wb-colspan") : 0;
3095
3483
  this.statusNodeType
3096
3484
  ? rowClasses.push("wb-status-" + this.statusNodeType)
3097
3485
  : 0;
3098
3486
  this.match ? rowClasses.push("wb-match") : 0;
3099
3487
  this.subMatchCount ? rowClasses.push("wb-submatch") : 0;
3100
3488
  treeOptions.skeleton ? rowClasses.push("wb-skeleton") : 0;
3101
- // TODO: no need to hide!
3102
- // !(this.match || this.subMatchCount) ? rowClasses.push("wb-hide") : 0;
3103
- if (rowDiv) {
3104
- // Row markup already exists
3105
- nodeElem = rowDiv.querySelector("span.wb-node");
3106
- titleSpan = nodeElem.querySelector("span.wb-title");
3107
- expanderSpan = nodeElem.querySelector("i.wb-expander");
3108
- checkboxSpan = nodeElem.querySelector("i.wb-checkbox");
3109
- iconSpan = nodeElem.querySelector("i.wb-icon");
3110
- // TODO: we need this, when icons should be replacable
3111
- // iconSpan = this._createIcon(nodeElem, iconSpan);
3112
- // colElems = (<unknown>(
3113
- // rowDiv.querySelectorAll("span.wb-col")
3114
- // )) as HTMLElement[];
3115
- }
3116
- else {
3117
- rowDiv = document.createElement("div");
3118
- // rowDiv.classList.add("wb-row");
3119
- // Attach a node reference to the DOM Element:
3120
- rowDiv._wb_node = this;
3121
- nodeElem = document.createElement("span");
3122
- nodeElem.classList.add("wb-node", "wb-col");
3123
- rowDiv.appendChild(nodeElem);
3124
- let ofsTitlePx = 0;
3125
- if (checkbox) {
3126
- checkboxSpan = document.createElement("i");
3127
- nodeElem.appendChild(checkboxSpan);
3128
- ofsTitlePx += ICON_WIDTH;
3129
- }
3130
- for (let i = level - 1; i > 0; i--) {
3131
- elem = document.createElement("i");
3132
- elem.classList.add("wb-indent");
3133
- nodeElem.appendChild(elem);
3134
- ofsTitlePx += ICON_WIDTH;
3135
- }
3136
- if (!treeOptions.minExpandLevel || level > treeOptions.minExpandLevel) {
3137
- expanderSpan = document.createElement("i");
3138
- nodeElem.appendChild(expanderSpan);
3139
- ofsTitlePx += ICON_WIDTH;
3140
- }
3141
- iconSpan = this._createIcon(nodeElem);
3142
- if (iconSpan) {
3143
- ofsTitlePx += ICON_WIDTH;
3144
- }
3145
- titleSpan = document.createElement("span");
3146
- titleSpan.classList.add("wb-title");
3147
- nodeElem.appendChild(titleSpan);
3148
- this._callEvent("enhanceTitle", { titleSpan: titleSpan });
3149
- // Store the width of leading icons with the node, so we can calculate
3150
- // the width of the embedded title span later
3151
- nodeElem._ofsTitlePx = ofsTitlePx;
3152
- if (tree.options.dnd.dragStart) {
3153
- nodeElem.draggable = true;
3154
- }
3155
- // Render columns
3156
- // colElems = [];
3157
- if (!this.colspan && columns.length > 1) {
3158
- let colIdx = 0;
3159
- for (let col of columns) {
3160
- colIdx++;
3161
- let colElem;
3162
- if (col.id === "*") {
3163
- colElem = nodeElem;
3164
- }
3165
- else {
3166
- colElem = document.createElement("span");
3167
- colElem.classList.add("wb-col");
3168
- // colElem.textContent = "" + col.id;
3169
- rowDiv.appendChild(colElem);
3170
- }
3171
- if (colIdx === activeColIdx) {
3172
- colElem.classList.add("wb-active");
3173
- }
3174
- // Add classes from `columns` definition to `<div.wb-col>` cells
3175
- col.classes ? colElem.classList.add(...col.classes.split(" ")) : 0;
3176
- colElem.style.left = col._ofsPx + "px";
3177
- colElem.style.width = col._widthPx + "px";
3178
- // colElems.push(colElem);
3179
- if (isNew && col.html) {
3180
- if (typeof col.html === "string") {
3181
- colElem.innerHTML = col.html;
3182
- }
3183
- }
3184
- }
3185
- }
3186
- }
3187
- // --- From here common code starts (either new or existing markup):
3188
- rowDiv.className = rowClasses.join(" "); // Reset prev. classes
3189
- // Add classes from `node.extraClasses`
3190
- rowDiv.classList.add(...this.extraClasses);
3489
+ // Replace previous classes:
3490
+ rowDiv.className = rowClasses.join(" ");
3491
+ // Add classes from `node.classes`
3492
+ this.classes ? rowDiv.classList.add(...this.classes) : 0;
3191
3493
  // Add classes from `tree.types[node.type]`
3192
3494
  if (typeInfo && typeInfo.classes) {
3193
3495
  rowDiv.classList.add(...typeInfo.classes);
3194
3496
  }
3195
- // rowDiv.style.top = (this._rowIdx! * 1.1) + "em";
3196
- rowDiv.style.top = this._rowIdx * ROW_HEIGHT + "px";
3197
3497
  if (expanderSpan) {
3198
3498
  if (this.isExpandable(false)) {
3199
3499
  if (this.expanded) {
@@ -3221,59 +3521,46 @@ class WunderbaumNode {
3221
3521
  checkboxSpan.className = "wb-checkbox " + iconMap.checkUnchecked;
3222
3522
  }
3223
3523
  }
3224
- if (this.titleWithHighlight) {
3225
- titleSpan.innerHTML = this.titleWithHighlight;
3226
- }
3227
- else {
3228
- // } else if (tree.options.escapeTitles) {
3229
- titleSpan.textContent = this.title;
3230
- // } else {
3231
- // titleSpan.innerHTML = this.title;
3232
- }
3233
- // Set the width of the title span, so overflow ellipsis work
3234
- if (!treeOptions.skeleton) {
3235
- if (this.colspan) {
3236
- let vpWidth = tree.element.clientWidth;
3237
- titleSpan.style.width =
3238
- vpWidth - nodeElem._ofsTitlePx - ROW_EXTRA_PAD + "px";
3524
+ // Fix active cell in cell-nav mode
3525
+ if (!opts.isNew) {
3526
+ let i = 0;
3527
+ for (let colSpan of rowDiv.children) {
3528
+ colSpan.classList.toggle("wb-active", i++ === tree.activeColIdx);
3239
3529
  }
3240
- else {
3241
- titleSpan.style.width =
3242
- columns[0]._widthPx -
3243
- nodeElem._ofsTitlePx -
3244
- ROW_EXTRA_PAD +
3245
- "px";
3530
+ // Update icon (if not opts.isNew, which would rebuild markup anyway)
3531
+ const iconSpan = nodeElem.querySelector("i.wb-icon");
3532
+ if (iconSpan) {
3533
+ this._createIcon(nodeElem, iconSpan);
3246
3534
  }
3247
3535
  }
3248
- this._rowElem = rowDiv;
3249
- if (this.statusNodeType) {
3250
- this._callEvent("renderStatusNode", {
3251
- isNew: isNew,
3252
- nodeElem: nodeElem,
3253
- });
3254
- }
3255
- else if (this.parent) {
3256
- // Skip root node
3257
- this._callEvent("render", {
3258
- isNew: isNew,
3259
- nodeElem: nodeElem,
3260
- typeInfo: typeInfo,
3261
- colInfosById: this._getRenderInfo(),
3262
- });
3263
- }
3264
- // Attach to DOM as late as possible
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
- }
3536
+ }
3537
+ /**
3538
+ * Create or update node's markup.
3539
+ *
3540
+ * `options.change` defaults to ChangeType.data, which updates the title,
3541
+ * icon, and status. It also triggers the `render` event, that lets the user
3542
+ * create or update the content of embeded cell elements.<br>
3543
+ *
3544
+ * If only the status or other class-only modifications have changed,
3545
+ * `options.change` should be set to ChangeType.status instead for best
3546
+ * efficiency.
3547
+ */
3548
+ render(options) {
3549
+ // this.log("render", options);
3550
+ const opts = Object.assign({ change: ChangeType.data }, options);
3551
+ if (!this._rowElem) {
3552
+ opts.change = "row";
3553
+ }
3554
+ switch (opts.change) {
3555
+ case "status":
3556
+ this._render_status(opts);
3557
+ break;
3558
+ case "data":
3559
+ this._render_data(opts);
3560
+ break;
3561
+ default:
3562
+ this._render_markup(opts);
3563
+ break;
3277
3564
  }
3278
3565
  }
3279
3566
  /**
@@ -3395,7 +3682,8 @@ class WunderbaumNode {
3395
3682
  * @see {@link Wunderbaum.scrollTo|Wunderbaum.scrollTo()}
3396
3683
  */
3397
3684
  async scrollIntoView(options) {
3398
- return this.tree.scrollTo(this);
3685
+ const opts = Object.assign({ node: this }, options);
3686
+ return this.tree.scrollTo(opts);
3399
3687
  }
3400
3688
  /**
3401
3689
  * Activate this node, deactivate previous, send events, activate column and scroll int viewport.
@@ -3403,26 +3691,26 @@ class WunderbaumNode {
3403
3691
  async setActive(flag = true, options) {
3404
3692
  const tree = this.tree;
3405
3693
  const prev = tree.activeNode;
3406
- const retrigger = options === null || options === void 0 ? void 0 : options.retrigger;
3407
- const noEvents = options === null || options === void 0 ? void 0 : options.noEvents;
3694
+ const retrigger = options === null || options === void 0 ? void 0 : options.retrigger; // Default: false
3695
+ const focusTree = options === null || options === void 0 ? void 0 : options.focusTree; // Default: false
3696
+ const focusNode = (options === null || options === void 0 ? void 0 : options.focusNode) !== false; // Default: true
3697
+ const noEvents = options === null || options === void 0 ? void 0 : options.noEvents; // Default: false
3698
+ const orgEvent = options === null || options === void 0 ? void 0 : options.event; // Default: false
3408
3699
  if (!noEvents) {
3409
- let orgEvent = options === null || options === void 0 ? void 0 : options.event;
3410
3700
  if (flag) {
3411
3701
  if (prev !== this || retrigger) {
3412
3702
  if ((prev === null || prev === void 0 ? void 0 : prev._callEvent("deactivate", {
3413
3703
  nextNode: this,
3414
3704
  orgEvent: orgEvent,
3415
- })) === false) {
3416
- return;
3417
- }
3418
- if (this._callEvent("activate", {
3419
- prevNode: prev,
3420
- orgEvent: orgEvent,
3421
- }) === false) {
3422
- tree.activeNode = null;
3423
- prev === null || prev === void 0 ? void 0 : prev.setModified();
3705
+ })) === false ||
3706
+ this._callEvent("beforeActivate", {
3707
+ prevNode: prev,
3708
+ orgEvent: orgEvent,
3709
+ }) === false) {
3424
3710
  return;
3425
3711
  }
3712
+ tree.activeNode = null;
3713
+ prev === null || prev === void 0 ? void 0 : prev.setModified(ChangeType.status);
3426
3714
  }
3427
3715
  }
3428
3716
  else if (prev === this || retrigger) {
@@ -3430,44 +3718,54 @@ class WunderbaumNode {
3430
3718
  }
3431
3719
  }
3432
3720
  if (prev !== this) {
3433
- tree.activeNode = this;
3434
- prev === null || prev === void 0 ? void 0 : prev.setModified();
3435
- this.setModified();
3721
+ if (flag) {
3722
+ tree.activeNode = this;
3723
+ if (focusNode || focusTree)
3724
+ tree.focusNode = this;
3725
+ if (focusTree)
3726
+ tree.setFocus();
3727
+ }
3728
+ prev === null || prev === void 0 ? void 0 : prev.setModified(ChangeType.status);
3729
+ this.setModified(ChangeType.status);
3436
3730
  }
3437
3731
  if (options &&
3438
3732
  options.colIdx != null &&
3439
3733
  options.colIdx !== tree.activeColIdx &&
3440
- tree.navMode !== NavigationMode.row) {
3734
+ tree.isCellNav()) {
3441
3735
  tree.setColumn(options.colIdx);
3442
3736
  }
3443
- // requestAnimationFrame(() => {
3444
- // this.scrollIntoView();
3445
- // })
3446
- return this.scrollIntoView();
3737
+ if (flag && !noEvents) {
3738
+ this._callEvent("activate", { prevNode: prev, orgEvent: orgEvent });
3739
+ }
3740
+ return this.makeVisible();
3447
3741
  }
3448
3742
  /**
3449
3743
  * Expand or collapse this node.
3450
3744
  */
3451
3745
  async setExpanded(flag = true, options) {
3452
- // alert("" + this.getLevel() + ", "+ this.getOption("minExpandLevel");
3453
3746
  if (!flag &&
3454
3747
  this.isExpanded() &&
3455
- this.getLevel() < this.getOption("minExpandLevel") &&
3748
+ this.getLevel() <= this.tree.getOption("minExpandLevel") &&
3456
3749
  !getOption(options, "force")) {
3457
3750
  this.logDebug("Ignored collapse request below expandLevel.");
3458
3751
  return;
3459
3752
  }
3753
+ if (!flag === !this.expanded) {
3754
+ return; // Nothing to do
3755
+ }
3460
3756
  if (flag && this.lazy && this.children == null) {
3461
3757
  await this.loadLazy();
3462
3758
  }
3463
3759
  this.expanded = flag;
3464
- this.tree.setModified(ChangeType.structure);
3760
+ const updateOpts = { immediate: !!getOption(options, "immediate") };
3761
+ this.tree.setModified(ChangeType.structure, updateOpts);
3465
3762
  }
3466
3763
  /**
3467
3764
  * Set keyboard focus here.
3468
3765
  * @see {@link setActive}
3469
3766
  */
3470
- setFocus(flag = true, options) {
3767
+ setFocus(flag = true) {
3768
+ assert(!!flag, "blur is not yet implemented");
3471
3769
  const prev = this.tree.focusNode;
3472
3770
  this.tree.focusNode = this;
3473
3771
  prev === null || prev === void 0 ? void 0 : prev.setModified();
@@ -3482,10 +3780,16 @@ class WunderbaumNode {
3482
3780
  setKey(key, refKey) {
3483
3781
  throw new Error("Not yet implemented");
3484
3782
  }
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);
3783
+ /**
3784
+ * Schedule a render, typically called to update after a status or data change.
3785
+ *
3786
+ * `change` defaults to 'data', which handles modifcations of title, icon,
3787
+ * and column content. It can be reduced to 'ChangeType.status' if only
3788
+ * active/focus/selected state has changed.
3789
+ */
3790
+ setModified(change = ChangeType.data) {
3791
+ assert(change === ChangeType.status || change === ChangeType.data);
3792
+ this.tree.setModified(change, this);
3489
3793
  }
3490
3794
  /** Modify the check/uncheck state. */
3491
3795
  setSelected(flag = true, options) {
@@ -3497,8 +3801,10 @@ class WunderbaumNode {
3497
3801
  this.setModified();
3498
3802
  }
3499
3803
  /** Display node status (ok, loading, error, noData) using styles and a dummy child node. */
3500
- setStatus(status, message, details) {
3804
+ setStatus(status, options) {
3501
3805
  const tree = this.tree;
3806
+ const message = options === null || options === void 0 ? void 0 : options.message;
3807
+ const details = options === null || options === void 0 ? void 0 : options.details;
3502
3808
  let statusNode = null;
3503
3809
  const _clearStatusNode = () => {
3504
3810
  // Remove dedicated dummy node, if any
@@ -3596,9 +3902,9 @@ class WunderbaumNode {
3596
3902
  * @param {object} [extra]
3597
3903
  */
3598
3904
  triggerModify(operation, extra) {
3599
- if (!this.parent) {
3600
- return;
3601
- }
3905
+ // if (!this.parent) {
3906
+ // return;
3907
+ // }
3602
3908
  this.parent.triggerModifyChild(operation, this, extra);
3603
3909
  }
3604
3910
  /**
@@ -3681,7 +3987,7 @@ WunderbaumNode.sequence = 0;
3681
3987
  /*!
3682
3988
  * Wunderbaum - ext-edit
3683
3989
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
3684
- * v0.0.3, Mon, 18 Apr 2022 11:52:44 GMT (https://github.com/mar10/wunderbaum)
3990
+ * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
3685
3991
  */
3686
3992
  // const START_MARKER = "\uFFF7";
3687
3993
  class EditExtension extends WunderbaumExtension {
@@ -3711,7 +4017,7 @@ class EditExtension extends WunderbaumExtension {
3711
4017
  _applyChange(eventName, node, colElem, extra) {
3712
4018
  let res;
3713
4019
  node.log(`_applyChange(${eventName})`, extra);
3714
- colElem.classList.add("wb-dirty");
4020
+ colElem.classList.add("wb-busy");
3715
4021
  colElem.classList.remove("wb-error");
3716
4022
  try {
3717
4023
  res = node._callEvent(eventName, extra);
@@ -3719,7 +4025,7 @@ class EditExtension extends WunderbaumExtension {
3719
4025
  catch (err) {
3720
4026
  node.logError(`Error in ${eventName} event handler`, err);
3721
4027
  colElem.classList.add("wb-error");
3722
- colElem.classList.remove("wb-dirty");
4028
+ colElem.classList.remove("wb-busy");
3723
4029
  }
3724
4030
  // Convert scalar return value to a resolved promise
3725
4031
  if (!(res instanceof Promise)) {
@@ -3731,7 +4037,7 @@ class EditExtension extends WunderbaumExtension {
3731
4037
  colElem.classList.add("wb-error");
3732
4038
  })
3733
4039
  .finally(() => {
3734
- colElem.classList.remove("wb-dirty");
4040
+ colElem.classList.remove("wb-busy");
3735
4041
  });
3736
4042
  return res;
3737
4043
  }
@@ -3770,12 +4076,12 @@ class EditExtension extends WunderbaumExtension {
3770
4076
  const eventName = eventToString(event);
3771
4077
  const tree = this.tree;
3772
4078
  const trigger = this.getPluginOption("trigger");
3773
- const inputElem = event.target && event.target.closest("input,[contenteditable]");
3774
- // let handled = true;
3775
- tree.logDebug(`_preprocessKeyEvent: ${eventName}`);
4079
+ // const inputElem =
4080
+ // event.target && event.target.closest("input,[contenteditable]");
4081
+ // tree.logDebug(`_preprocessKeyEvent: ${eventName}`);
3776
4082
  // --- Title editing: apply/discard ---
3777
- if (inputElem) {
3778
- //this.isEditingTitle()) {
4083
+ // if (inputElem) {
4084
+ if (this.isEditingTitle()) {
3779
4085
  switch (eventName) {
3780
4086
  case "Enter":
3781
4087
  this._stopEditTitle(true, { event: event });
@@ -3789,7 +4095,7 @@ class EditExtension extends WunderbaumExtension {
3789
4095
  return false;
3790
4096
  }
3791
4097
  // --- Trigger title editing
3792
- if (tree.navMode === NavigationMode.row || tree.activeColIdx === 0) {
4098
+ if (tree.isRowNav() || tree.activeColIdx === 0) {
3793
4099
  switch (eventName) {
3794
4100
  case "Enter":
3795
4101
  if (trigger.indexOf("macEnter") >= 0 && isMac) {
@@ -3838,7 +4144,7 @@ class EditExtension extends WunderbaumExtension {
3838
4144
  titleSpan.innerHTML = inputHtml;
3839
4145
  const inputElem = titleSpan.firstElementChild;
3840
4146
  if (validity) {
3841
- // Permanently apply input validations (CSS and tooltip)
4147
+ // Permanently apply input validations (CSS and tooltip)
3842
4148
  inputElem.addEventListener("keydown", (e) => {
3843
4149
  inputElem.setCustomValidity("");
3844
4150
  if (!inputElem.reportValidity()) ;
@@ -3954,7 +4260,7 @@ class EditExtension extends WunderbaumExtension {
3954
4260
  return;
3955
4261
  }
3956
4262
  const newNode = node.addNode(init, mode);
3957
- newNode.addClass("wb-edit-new");
4263
+ newNode.setClass("wb-edit-new");
3958
4264
  this.relatedNode = node;
3959
4265
  // Don't filter new nodes:
3960
4266
  newNode.match = true;
@@ -3967,17 +4273,26 @@ class EditExtension extends WunderbaumExtension {
3967
4273
  /*!
3968
4274
  * wunderbaum.ts
3969
4275
  *
3970
- * A tree control.
4276
+ * A treegrid control.
3971
4277
  *
3972
4278
  * Copyright (c) 2021-2022, Martin Wendt (https://wwWendt.de).
3973
- * Released under the MIT license.
4279
+ * https://github.com/mar10/wunderbaum
3974
4280
  *
3975
- * @version v0.0.3
3976
- * @date Mon, 18 Apr 2022 11:52:44 GMT
4281
+ * Released under the MIT license.
4282
+ * @version v0.0.6
4283
+ * @date Sat, 10 Sep 2022 19:29:21 GMT
3977
4284
  */
3978
- // const class_prefix = "wb-";
3979
- // const node_props: string[] = ["title", "key", "refKey"];
3980
- // const MAX_CHANGED_NODES = 10;
4285
+ class WbSystemRoot extends WunderbaumNode {
4286
+ constructor(tree) {
4287
+ super(tree, null, {
4288
+ key: "__root__",
4289
+ title: tree.id,
4290
+ });
4291
+ }
4292
+ toString() {
4293
+ return `WbSystemRoot@${this.key}<'${this.tree.id}'>`;
4294
+ }
4295
+ }
3981
4296
  /**
3982
4297
  * A persistent plain object or array.
3983
4298
  *
@@ -3985,14 +4300,15 @@ class EditExtension extends WunderbaumExtension {
3985
4300
  */
3986
4301
  class Wunderbaum {
3987
4302
  constructor(options) {
4303
+ this.enabled = true;
4304
+ /** Contains additional data that was sent as response to an Ajax source load request. */
4305
+ this.data = {};
3988
4306
  this.extensionList = [];
3989
4307
  this.extensions = {};
3990
4308
  this.keyMap = new Map();
3991
4309
  this.refKeyMap = new Map();
3992
- // protected viewNodes = new Set<WunderbaumNode>();
3993
4310
  this.treeRowCount = 0;
3994
4311
  this._disableUpdateCount = 0;
3995
- // protected eventHandlers : Array<function> = [];
3996
4312
  /** Currently active node if any. */
3997
4313
  this.activeNode = null;
3998
4314
  /** Current node hat has keyboard focus if any. */
@@ -4000,13 +4316,11 @@ class Wunderbaum {
4000
4316
  /** Shared properties, referenced by `node.type`. */
4001
4317
  this.types = {};
4002
4318
  /** List of column definitions. */
4003
- this.columns = [];
4319
+ this.columns = []; // any[] = [];
4004
4320
  this._columnsById = {};
4005
4321
  // Modification Status
4006
- // protected changedSince = 0;
4007
- // protected changes = new Set<ChangeType>();
4008
- // protected changedNodes = new Set<WunderbaumNode>();
4009
4322
  this.changeRedrawRequestPending = false;
4323
+ this.changeScrollRequestPending = false;
4010
4324
  /** Expose some useful methods of the util.ts module as `tree._util`. */
4011
4325
  this._util = util;
4012
4326
  // --- FILTER ---
@@ -4015,7 +4329,7 @@ class Wunderbaum {
4015
4329
  /** @internal Use `setColumn()`/`getActiveColElem()`*/
4016
4330
  this.activeColIdx = 0;
4017
4331
  /** @internal */
4018
- this.navMode = NavigationMode.row;
4332
+ this._cellNavMode = false;
4019
4333
  /** @internal */
4020
4334
  this.lastQuicksearchTime = 0;
4021
4335
  /** @internal */
@@ -4032,18 +4346,21 @@ class Wunderbaum {
4032
4346
  element: null,
4033
4347
  debugLevel: DEFAULT_DEBUGLEVEL,
4034
4348
  header: null,
4035
- headerHeightPx: ROW_HEIGHT,
4349
+ // headerHeightPx: ROW_HEIGHT,
4036
4350
  rowHeightPx: ROW_HEIGHT,
4037
4351
  columns: null,
4038
4352
  types: null,
4039
4353
  // escapeTitles: true,
4354
+ enabled: true,
4355
+ fixedCol: false,
4040
4356
  showSpinner: false,
4041
- checkbox: true,
4357
+ checkbox: false,
4042
4358
  minExpandLevel: 0,
4043
4359
  updateThrottleWait: 200,
4044
4360
  skeleton: false,
4361
+ connectTopBreadcrumb: null,
4045
4362
  // --- KeyNav ---
4046
- navigationMode: NavigationModeOption.startRow,
4363
+ navigationModeOption: null,
4047
4364
  quicksearch: true,
4048
4365
  // --- Events ---
4049
4366
  change: noop,
@@ -4085,41 +4402,25 @@ class Wunderbaum {
4085
4402
  }
4086
4403
  });
4087
4404
  this.id = opts.id || "wb_" + ++Wunderbaum.sequence;
4088
- this.root = new WunderbaumNode(this, null, {
4089
- key: "__root__",
4090
- // title: "__root__",
4091
- });
4405
+ this.root = new WbSystemRoot(this);
4092
4406
  this._registerExtension(new KeynavExtension(this));
4093
4407
  this._registerExtension(new EditExtension(this));
4094
4408
  this._registerExtension(new FilterExtension(this));
4095
4409
  this._registerExtension(new DndExtension(this));
4096
4410
  this._registerExtension(new GridExtension(this));
4097
4411
  this._registerExtension(new LoggerExtension(this));
4412
+ this._updateViewportThrottled = adaptiveThrottle(this._updateViewportImmediately.bind(this), {});
4098
4413
  // --- Evaluate options
4099
4414
  this.columns = opts.columns;
4100
4415
  delete opts.columns;
4101
- if (!this.columns) {
4102
- let defaultName = typeof opts.header === "string" ? opts.header : this.id;
4103
- this.columns = [{ id: "*", title: defaultName, width: "*" }];
4104
- }
4105
- this.types = opts.types || {};
4106
- delete opts.types;
4107
- // Convert `TYPE.classes` to a Set
4108
- for (let t of Object.values(this.types)) {
4109
- if (t.classes) {
4110
- t.classes = toSet(t.classes);
4111
- }
4416
+ if (!this.columns || !this.columns.length) {
4417
+ const title = typeof opts.header === "string" ? opts.header : this.id;
4418
+ this.columns = [{ id: "*", title: title, width: "*" }];
4112
4419
  }
4113
- if (this.columns.length === 1) {
4114
- opts.navigationMode = NavigationModeOption.row;
4420
+ if (opts.types) {
4421
+ this.setTypes(opts.types, true);
4115
4422
  }
4116
- if (opts.navigationMode === NavigationModeOption.cell ||
4117
- opts.navigationMode === NavigationModeOption.startCell) {
4118
- this.navMode = NavigationMode.cellNav;
4119
- }
4120
- this._updateViewportThrottled = throttle(() => {
4121
- this._updateViewport();
4122
- }, opts.updateThrottleWait, { leading: true, trailing: true });
4423
+ delete opts.types;
4123
4424
  // --- Create Markup
4124
4425
  this.element = elemFromSelector(opts.element);
4125
4426
  assert(!!this.element, `Invalid 'element' option: ${opts.element}`);
@@ -4139,12 +4440,15 @@ class Wunderbaum {
4139
4440
  const rowElement = this.headerElement.querySelector("div.wb-row");
4140
4441
  for (const colDiv of rowElement.querySelectorAll("div")) {
4141
4442
  this.columns.push({
4142
- id: colDiv.dataset.id || null,
4143
- text: "" + colDiv.textContent,
4443
+ id: colDiv.dataset.id || `col_${this.columns.length}`,
4444
+ // id: colDiv.dataset.id || null,
4445
+ title: "" + colDiv.textContent,
4446
+ // text: "" + colDiv.textContent,
4447
+ width: "*", // TODO: read from header span
4144
4448
  });
4145
4449
  }
4146
4450
  }
4147
- else if (wantHeader) {
4451
+ else {
4148
4452
  // We need a row div, the rest will be computed from `this.columns`
4149
4453
  const coldivs = "<span class='wb-col'></span>".repeat(this.columns.length);
4150
4454
  this.element.innerHTML = `
@@ -4153,23 +4457,27 @@ class Wunderbaum {
4153
4457
  ${coldivs}
4154
4458
  </div>
4155
4459
  </div>`;
4156
- // this.updateColumns({ render: false });
4157
- }
4158
- else {
4159
- this.element.innerHTML = "";
4460
+ if (!wantHeader) {
4461
+ const he = this.element.querySelector("div.wb-header");
4462
+ he.style.display = "none";
4463
+ }
4160
4464
  }
4161
4465
  //
4162
4466
  this.element.innerHTML += `
4163
4467
  <div class="wb-scroll-container">
4164
4468
  <div class="wb-node-list"></div>
4165
4469
  </div>`;
4166
- this.scrollContainer = this.element.querySelector("div.wb-scroll-container");
4167
- this.nodeListElement = this.scrollContainer.querySelector("div.wb-node-list");
4470
+ this.scrollContainerElement = this.element.querySelector("div.wb-scroll-container");
4471
+ this.nodeListElement = this.scrollContainerElement.querySelector("div.wb-node-list");
4168
4472
  this.headerElement = this.element.querySelector("div.wb-header");
4169
- if (this.columns.length > 1) {
4170
- this.element.classList.add("wb-grid");
4171
- }
4473
+ this.element.classList.toggle("wb-grid", this.columns.length > 1);
4172
4474
  this._initExtensions();
4475
+ // --- apply initial options
4476
+ ["enabled", "fixedCol"].forEach((optName) => {
4477
+ if (opts[optName] != null) {
4478
+ this.setOption(optName, opts[optName]);
4479
+ }
4480
+ });
4173
4481
  // --- Load initial data
4174
4482
  if (opts.source) {
4175
4483
  if (opts.showSpinner) {
@@ -4178,6 +4486,18 @@ class Wunderbaum {
4178
4486
  }
4179
4487
  this.load(opts.source)
4180
4488
  .then(() => {
4489
+ // The source may have defined columns, so we may adjust the nav mode
4490
+ if (opts.navigationModeOption == null) {
4491
+ if (this.isGrid()) {
4492
+ this.setNavigationOption(NavigationOptions.cell);
4493
+ }
4494
+ else {
4495
+ this.setNavigationOption(NavigationOptions.row);
4496
+ }
4497
+ }
4498
+ else {
4499
+ this.setNavigationOption(opts.navigationModeOption);
4500
+ }
4181
4501
  readyDeferred.resolve();
4182
4502
  })
4183
4503
  .catch((error) => {
@@ -4192,23 +4512,37 @@ class Wunderbaum {
4192
4512
  else {
4193
4513
  readyDeferred.resolve();
4194
4514
  }
4195
- // TODO: This is sometimes required, because this.element.clientWidth
4196
- // has a wrong value at start???
4197
- setTimeout(() => {
4198
- this.updateViewport();
4199
- }, 50);
4515
+ // Async mode is sometimes required, because this.element.clientWidth
4516
+ // has a wrong value at start???
4517
+ this.setModified(ChangeType.any);
4200
4518
  // --- Bind listeners
4201
- this.scrollContainer.addEventListener("scroll", (e) => {
4519
+ this.element.addEventListener("scroll", (e) => {
4520
+ // this.log("scroll", e);
4202
4521
  this.setModified(ChangeType.vscroll);
4203
4522
  });
4523
+ // this.scrollContainerElement.addEventListener("scroll", (e: Event) => {
4524
+ // this.log("scroll", e)
4525
+ // this.setModified(ChangeType.vscroll);
4526
+ // });
4204
4527
  this.resizeObserver = new ResizeObserver((entries) => {
4205
4528
  this.setModified(ChangeType.vscroll);
4206
- console.log("ResizeObserver: Size changed", entries);
4529
+ // this.log("ResizeObserver: Size changed", entries);
4207
4530
  });
4208
4531
  this.resizeObserver.observe(this.element);
4209
4532
  onEvent(this.nodeListElement, "click", "div.wb-row", (e) => {
4210
4533
  const info = Wunderbaum.getEventInfo(e);
4211
4534
  const node = info.node;
4535
+ // this.log("click", info, e);
4536
+ // if( (e.target as HTMLElement).matches("input[type=checkbox]")){
4537
+ // // Click on an embedded checkbox triggers a change event.
4538
+ // // We return here, before the `setActive()` performs a render
4539
+ // this.log("click - cb", info, e);
4540
+ // // e.preventDefault()
4541
+ // setTimeout(()=>{
4542
+ // // (e.target as HTMLElement).click()
4543
+ // }, 50)
4544
+ // // return
4545
+ // }
4212
4546
  if (this._callEvent("click", { event: e, node: node, info: info }) === false) {
4213
4547
  this.lastClickTime = Date.now();
4214
4548
  return false;
@@ -4236,16 +4570,15 @@ class Wunderbaum {
4236
4570
  node.setSelected(!node.isSelected());
4237
4571
  }
4238
4572
  }
4239
- // if(e.target.classList.)
4240
- // this.log("click", info);
4241
4573
  this.lastClickTime = Date.now();
4242
4574
  });
4243
4575
  onEvent(this.element, "keydown", (e) => {
4244
4576
  const info = Wunderbaum.getEventInfo(e);
4245
4577
  const eventName = eventToString(e);
4578
+ const node = info.node || this.getFocusNode();
4246
4579
  this._callHook("onKeyEvent", {
4247
4580
  event: e,
4248
- node: info.node,
4581
+ node: node,
4249
4582
  info: info,
4250
4583
  eventName: eventName,
4251
4584
  });
@@ -4421,14 +4754,14 @@ class Wunderbaum {
4421
4754
  * tree._callEvent("edit.beforeEdit", {foo: 42})
4422
4755
  * ```
4423
4756
  */
4424
- _callEvent(name, extra) {
4425
- const [p, n] = name.split(".");
4757
+ _callEvent(type, extra) {
4758
+ const [p, n] = type.split(".");
4426
4759
  const opts = this.options;
4427
4760
  const func = n ? opts[p][n] : opts[p];
4428
4761
  if (func) {
4429
- return func.call(this, extend({ name: name, tree: this, util: this._util }, extra));
4762
+ return func.call(this, extend({ type: type, tree: this, util: this._util }, extra));
4430
4763
  // } else {
4431
- // this.logError(`Triggering undefined event '${name}'.`)
4764
+ // this.logError(`Triggering undefined event '${type}'.`)
4432
4765
  }
4433
4766
  }
4434
4767
  /** Return the node for given row index. */
@@ -4444,29 +4777,32 @@ class Wunderbaum {
4444
4777
  return node;
4445
4778
  }
4446
4779
  /** Return the topmost visible node in the viewport. */
4447
- _firstNodeInView(complete = true) {
4780
+ getTopmostVpNode(complete = true) {
4781
+ const gracePx = 1; // ignore subpixel scrolling
4782
+ const scrollParent = this.element;
4783
+ // const headerHeight = this.headerElement.clientHeight; // May be 0
4784
+ const scrollTop = scrollParent.scrollTop; // + headerHeight;
4448
4785
  let topIdx;
4449
- const gracePy = 1; // ignore subpixel scrolling
4450
4786
  if (complete) {
4451
- topIdx = Math.ceil((this.scrollContainer.scrollTop - gracePy) / ROW_HEIGHT);
4787
+ topIdx = Math.ceil((scrollTop - gracePx) / ROW_HEIGHT);
4452
4788
  }
4453
4789
  else {
4454
- topIdx = Math.floor(this.scrollContainer.scrollTop / ROW_HEIGHT);
4790
+ topIdx = Math.floor(scrollTop / ROW_HEIGHT);
4455
4791
  }
4456
4792
  return this._getNodeByRowIdx(topIdx);
4457
4793
  }
4458
4794
  /** Return the lowest visible node in the viewport. */
4459
- _lastNodeInView(complete = true) {
4795
+ getLowestVpNode(complete = true) {
4796
+ const scrollParent = this.element;
4797
+ const headerHeight = this.headerElement.clientHeight; // May be 0
4798
+ const scrollTop = scrollParent.scrollTop;
4799
+ const clientHeight = scrollParent.clientHeight - headerHeight;
4460
4800
  let bottomIdx;
4461
4801
  if (complete) {
4462
- bottomIdx =
4463
- Math.floor((this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
4464
- ROW_HEIGHT) - 1;
4802
+ bottomIdx = Math.floor((scrollTop + clientHeight) / ROW_HEIGHT) - 1;
4465
4803
  }
4466
4804
  else {
4467
- bottomIdx =
4468
- Math.ceil((this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
4469
- ROW_HEIGHT) - 1;
4805
+ bottomIdx = Math.ceil((scrollTop + clientHeight) / ROW_HEIGHT) - 1;
4470
4806
  }
4471
4807
  bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
4472
4808
  return this._getNodeByRowIdx(bottomIdx);
@@ -4663,7 +4999,7 @@ class Wunderbaum {
4663
4999
  * Return `tree.option.NAME` (also resolving if this is a callback).
4664
5000
  *
4665
5001
  * See also {@link WunderbaumNode.getOption|WunderbaumNode.getOption()}
4666
- * to consider `node.NAME` setting and `tree.types[node.type].NAME`.
5002
+ * to evaluate `node.NAME` setting and `tree.types[node.type].NAME`.
4667
5003
  *
4668
5004
  * @param name option name (use dot notation to access extension option, e.g.
4669
5005
  * `filter.mode`)
@@ -4682,33 +5018,48 @@ class Wunderbaum {
4682
5018
  value = value({ type: "resolve", tree: this });
4683
5019
  }
4684
5020
  // Use value from value options dict, fallback do default
5021
+ // console.info(name, value, opts)
4685
5022
  return value !== null && value !== void 0 ? value : defaultValue;
4686
5023
  }
4687
5024
  /**
4688
- *
4689
- * @param name
4690
- * @param value
5025
+ * Set tree option.
5026
+ * Use dot notation to set plugin option, e.g. "filter.mode".
4691
5027
  */
4692
5028
  setOption(name, value) {
4693
- if (name.indexOf(".") === -1) {
4694
- this.options[name] = value;
4695
- // switch (name) {
4696
- // case value:
4697
- // break;
4698
- // default:
4699
- // break;
4700
- // }
5029
+ // this.log(`setOption(${name}, ${value})`);
5030
+ if (name.indexOf(".") >= 0) {
5031
+ const parts = name.split(".");
5032
+ const ext = this.extensions[parts[0]];
5033
+ ext.setPluginOption(parts[1], value);
4701
5034
  return;
4702
5035
  }
4703
- const parts = name.split(".");
4704
- const ext = this.extensions[parts[0]];
4705
- ext.setPluginOption(parts[1], value);
5036
+ this.options[name] = value;
5037
+ switch (name) {
5038
+ case "checkbox":
5039
+ this.setModified(ChangeType.any, { removeMarkup: true });
5040
+ break;
5041
+ case "enabled":
5042
+ this.setEnabled(!!value);
5043
+ break;
5044
+ case "fixedCol":
5045
+ this.element.classList.toggle("wb-fixed-col", !!value);
5046
+ break;
5047
+ }
4706
5048
  }
4707
- /**Return true if the tree (or one of its nodes) has the input focus. */
5049
+ /** Return true if the tree (or one of its nodes) has the input focus. */
4708
5050
  hasFocus() {
4709
5051
  return this.element.contains(document.activeElement);
4710
5052
  }
4711
- /** Run code, but defer `updateViewport()` until done. */
5053
+ /**
5054
+ * Return true if the tree displays a header. Grids have a header unless the
5055
+ * `header` option is set to `false`. Plain trees have a header if the `header`
5056
+ * option is a string or `true`.
5057
+ */
5058
+ hasHeader() {
5059
+ const header = this.options.header;
5060
+ return this.isGrid() ? header !== false : !!header;
5061
+ }
5062
+ /** Run code, but defer rendering of viewport until done. */
4712
5063
  runWithoutUpdate(func, hint = null) {
4713
5064
  try {
4714
5065
  this.enableUpdate(false);
@@ -4732,6 +5083,18 @@ class Wunderbaum {
4732
5083
  this.logTimeEnd(tag);
4733
5084
  }
4734
5085
  }
5086
+ /** Recursively select all nodes. */
5087
+ selectAll(flag = true) {
5088
+ try {
5089
+ this.enableUpdate(false);
5090
+ this.visit((node) => {
5091
+ node.setSelected(flag);
5092
+ });
5093
+ }
5094
+ finally {
5095
+ this.enableUpdate(true);
5096
+ }
5097
+ }
4735
5098
  /** Return the number of nodes in the data model.*/
4736
5099
  count(visible = false) {
4737
5100
  if (visible) {
@@ -4773,6 +5136,17 @@ class Wunderbaum {
4773
5136
  findFirst(match) {
4774
5137
  return this.root.findFirst(match);
4775
5138
  }
5139
+ /**
5140
+ * Find first node that matches condition.
5141
+ *
5142
+ * @param match title string to search for, or a
5143
+ * callback function that returns `true` if a node is matched.
5144
+ * @see {@link WunderbaumNode.findFirst}
5145
+ *
5146
+ */
5147
+ findKey(key) {
5148
+ return this.keyMap.get(key);
5149
+ }
4776
5150
  /**
4777
5151
  * Find the next visible node that starts with `match`, starting at `startNode`
4778
5152
  * and wrap-around at the end.
@@ -4815,7 +5189,7 @@ class Wunderbaum {
4815
5189
  */
4816
5190
  findRelatedNode(node, where, includeHidden = false) {
4817
5191
  let res = null;
4818
- const pageSize = Math.floor(this.scrollContainer.clientHeight / ROW_HEIGHT);
5192
+ const pageSize = Math.floor(this.scrollContainerElement.clientHeight / ROW_HEIGHT);
4819
5193
  switch (where) {
4820
5194
  case "parent":
4821
5195
  if (node.parent && node.parent.parent) {
@@ -4871,7 +5245,7 @@ class Wunderbaum {
4871
5245
  res = this._getNextNodeInView(node);
4872
5246
  break;
4873
5247
  case "pageDown":
4874
- const bottomNode = this._lastNodeInView();
5248
+ const bottomNode = this.getLowestVpNode();
4875
5249
  // this.logDebug(`${where}(${node}) -> ${bottomNode}`);
4876
5250
  if (node._rowIdx < bottomNode._rowIdx) {
4877
5251
  res = bottomNode;
@@ -4885,7 +5259,7 @@ class Wunderbaum {
4885
5259
  res = node;
4886
5260
  }
4887
5261
  else {
4888
- const topNode = this._firstNodeInView();
5262
+ const topNode = this.getTopmostVpNode();
4889
5263
  // this.logDebug(`${where}(${node}) -> ${topNode}`);
4890
5264
  if (node._rowIdx > topNode._rowIdx) {
4891
5265
  res = topNode;
@@ -4965,6 +5339,10 @@ class Wunderbaum {
4965
5339
  const idx = Array.prototype.indexOf.call(parentCol.parentNode.children, parentCol);
4966
5340
  res.colIdx = idx;
4967
5341
  }
5342
+ else if (cl.contains("wb-row")) {
5343
+ // Plain tree
5344
+ res.region = TargetType.title;
5345
+ }
4968
5346
  else {
4969
5347
  // Somewhere near the title
4970
5348
  if (event.type !== "mousemove" && !(event instanceof KeyboardEvent)) {
@@ -4993,7 +5371,7 @@ class Wunderbaum {
4993
5371
  * @internal
4994
5372
  */
4995
5373
  toString() {
4996
- return "Wunderbaum<'" + this.id + "'>";
5374
+ return `Wunderbaum<'${this.id}'>`;
4997
5375
  }
4998
5376
  /** Return true if any node is currently in edit-title mode. */
4999
5377
  isEditing() {
@@ -5055,67 +5433,119 @@ class Wunderbaum {
5055
5433
  }
5056
5434
  }
5057
5435
  /**
5058
- * Make sure that this node is scrolled into the viewport.
5059
- *
5060
- * @param {boolean | PlainObject} [effects=false] animation options.
5061
- * @param {object} [options=null] {topNode: null, effects: ..., parent: ...}
5062
- * this node will remain visible in
5063
- * any case, even if `this` is outside the scroll pane.
5436
+ * Make sure that this node is vertically scrolled into the viewport.
5064
5437
  */
5065
- scrollTo(opts) {
5066
- const MARGIN = 1;
5067
- const node = opts.node || this.getActiveNode();
5068
- assert(node._rowIdx != null);
5069
- const curTop = this.scrollContainer.scrollTop;
5070
- const height = this.scrollContainer.clientHeight;
5071
- const nodeOfs = node._rowIdx * ROW_HEIGHT;
5072
- let newTop;
5073
- if (nodeOfs > curTop) {
5074
- if (nodeOfs + ROW_HEIGHT < curTop + height) ;
5438
+ scrollTo(nodeOrOpts) {
5439
+ const PADDING = 2; // leave some pixels between viewport bounds
5440
+ let node;
5441
+ let opts;
5442
+ if (nodeOrOpts instanceof WunderbaumNode) {
5443
+ node = nodeOrOpts;
5444
+ }
5445
+ else {
5446
+ opts = nodeOrOpts;
5447
+ node = opts.node;
5448
+ }
5449
+ assert(node && node._rowIdx != null);
5450
+ const scrollParent = this.element;
5451
+ const headerHeight = this.headerElement.clientHeight; // May be 0
5452
+ const scrollTop = scrollParent.scrollTop;
5453
+ const vpHeight = scrollParent.clientHeight;
5454
+ const rowTop = node._rowIdx * ROW_HEIGHT + headerHeight;
5455
+ const vpTop = headerHeight;
5456
+ const vpRowTop = rowTop - scrollTop;
5457
+ const vpRowBottom = vpRowTop + ROW_HEIGHT;
5458
+ // this.log( `scrollTo(${node.title}), vpTop:${vpTop}px, scrollTop:${scrollTop}, vpHeight:${vpHeight}, rowTop:${rowTop}, vpRowTop:${vpRowTop}`, nodeOrOpts );
5459
+ let newScrollTop = null;
5460
+ if (vpRowTop >= vpTop) {
5461
+ if (vpRowBottom <= vpHeight) ;
5075
5462
  else {
5076
5463
  // Node is below viewport
5077
- newTop = nodeOfs - height + ROW_HEIGHT - MARGIN;
5464
+ // this.log("Below viewport");
5465
+ newScrollTop = rowTop + ROW_HEIGHT - vpHeight + PADDING; // leave some pixels between vieeport bounds
5078
5466
  }
5079
5467
  }
5080
- else if (nodeOfs < curTop) {
5468
+ else {
5081
5469
  // Node is above viewport
5082
- newTop = nodeOfs + MARGIN;
5470
+ // this.log("Above viewport");
5471
+ newScrollTop = rowTop - vpTop - PADDING; // leave some pixels between vieeport bounds
5083
5472
  }
5084
- if (newTop != null) {
5085
- this.log("scrollTo(" + nodeOfs + "): " + curTop + " => " + newTop, height);
5086
- this.scrollContainer.scrollTop = newTop;
5087
- this.setModified(ChangeType.vscroll);
5473
+ if (newScrollTop != null) {
5474
+ this.log(`scrollTo(${rowTop}): ${scrollTop} => ${newScrollTop}`);
5475
+ scrollParent.scrollTop = newScrollTop;
5476
+ // this.setModified(ChangeType.vscroll);
5088
5477
  }
5089
5478
  }
5479
+ /**
5480
+ * Make sure that this node is horizontally scrolled into the viewport.
5481
+ * Called by {@link setColumn}.
5482
+ */
5483
+ scrollToHorz() {
5484
+ // const PADDING = 1;
5485
+ const fixedWidth = this.columns[0]._widthPx;
5486
+ const vpWidth = this.element.clientWidth;
5487
+ const scrollLeft = this.element.scrollLeft;
5488
+ // if (scrollLeft <= 0) {
5489
+ // return; // Not scrolled horizontally: Nothing to do
5490
+ // }
5491
+ const colElem = this.getActiveColElem();
5492
+ const colLeft = Number.parseInt(colElem === null || colElem === void 0 ? void 0 : colElem.style.left, 10);
5493
+ const colRight = colLeft + Number.parseInt(colElem === null || colElem === void 0 ? void 0 : colElem.style.width, 10);
5494
+ let newLeft = scrollLeft;
5495
+ if (colLeft - scrollLeft < fixedWidth) {
5496
+ // The current column is scrolled behind the left fixed column
5497
+ newLeft = colLeft - fixedWidth;
5498
+ }
5499
+ else if (colRight - scrollLeft > vpWidth) {
5500
+ // The current column is scrolled outside the right side
5501
+ newLeft = colRight - vpWidth;
5502
+ }
5503
+ // util.assert(node._rowIdx != null);
5504
+ // const curLeft = this.scrollContainer.scrollLeft;
5505
+ this.log(`scrollToHorz(${this.activeColIdx}): ${colLeft}..${colRight}, fixedOfs=${fixedWidth}, vpWidth=${vpWidth}, curLeft=${scrollLeft} -> ${newLeft}`);
5506
+ // const nodeOfs = node._rowIdx * ROW_HEIGHT;
5507
+ // let newLeft;
5508
+ this.element.scrollLeft = newLeft;
5509
+ // this.setModified(ChangeType.vscroll);
5510
+ // }
5511
+ }
5090
5512
  /**
5091
5513
  * Set column #colIdx to 'active'.
5092
5514
  *
5093
5515
  * 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.
5516
+ * Available in cell-nav mode only.
5095
5517
  */
5096
5518
  setColumn(colIdx) {
5097
- assert(this.navMode !== NavigationMode.row);
5519
+ var _a;
5520
+ assert(this.isCellNav());
5098
5521
  assert(0 <= colIdx && colIdx < this.columns.length);
5099
5522
  this.activeColIdx = colIdx;
5100
- // node.setActive(true, { column: tree.activeColIdx + 1 });
5101
- this.setModified(ChangeType.row, this.activeNode);
5102
5523
  // Update `wb-active` class for all headers
5103
- if (this.headerElement) {
5524
+ if (this.hasHeader()) {
5104
5525
  for (let rowDiv of this.headerElement.children) {
5105
- // for (let rowDiv of document.querySelector("div.wb-header").children) {
5106
5526
  let i = 0;
5107
5527
  for (let colDiv of rowDiv.children) {
5108
5528
  colDiv.classList.toggle("wb-active", i++ === colIdx);
5109
5529
  }
5110
5530
  }
5111
5531
  }
5112
- // Update `wb-active` class for all cell divs
5532
+ (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.setModified(ChangeType.status);
5533
+ // Update `wb-active` class for all cell spans
5113
5534
  for (let rowDiv of this.nodeListElement.children) {
5114
5535
  let i = 0;
5115
5536
  for (let colDiv of rowDiv.children) {
5116
5537
  colDiv.classList.toggle("wb-active", i++ === colIdx);
5117
5538
  }
5118
5539
  }
5540
+ // Vertical scroll into view
5541
+ // if (this.options.fixedCol) {
5542
+ this.scrollToHorz();
5543
+ // }
5544
+ }
5545
+ /** Set or remove keybaord focus to the tree container. */
5546
+ setActiveNode(key, flag = true, options) {
5547
+ var _a;
5548
+ (_a = this.findKey(key)) === null || _a === void 0 ? void 0 : _a.setActive(flag, options);
5119
5549
  }
5120
5550
  /** Set or remove keybaord focus to the tree container. */
5121
5551
  setFocus(flag = true) {
@@ -5139,59 +5569,147 @@ class Wunderbaum {
5139
5569
  options = node;
5140
5570
  }
5141
5571
  const immediate = !!getOption(options, "immediate");
5572
+ const removeMarkup = !!getOption(options, "removeMarkup");
5573
+ if (removeMarkup) {
5574
+ this.visit((n) => {
5575
+ n.removeMarkup();
5576
+ });
5577
+ }
5578
+ let callUpdate = false;
5142
5579
  switch (change) {
5143
5580
  case ChangeType.any:
5144
5581
  case ChangeType.structure:
5145
5582
  case ChangeType.header:
5146
5583
  this.changeRedrawRequestPending = true;
5147
- this.updateViewport(immediate);
5584
+ callUpdate = true;
5148
5585
  break;
5149
5586
  case ChangeType.vscroll:
5150
- this.updateViewport(immediate);
5587
+ this.changeScrollRequestPending = true;
5588
+ callUpdate = true;
5151
5589
  break;
5152
5590
  case ChangeType.row:
5591
+ case ChangeType.data:
5153
5592
  case ChangeType.status:
5154
- // Single nodes are immedialtely updated if already inside the viewport
5593
+ // Single nodes are immediately updated if already inside the viewport
5155
5594
  // (otherwise we can ignore)
5156
5595
  if (node._rowElem) {
5157
- node.render();
5596
+ node.render({ change: change });
5158
5597
  }
5159
5598
  break;
5160
5599
  default:
5161
5600
  error(`Invalid change type ${change}`);
5162
5601
  }
5602
+ if (callUpdate) {
5603
+ if (immediate) {
5604
+ this._updateViewportImmediately();
5605
+ }
5606
+ else {
5607
+ this._updateViewportThrottled();
5608
+ }
5609
+ }
5610
+ }
5611
+ /** Disable mouse and keyboard interaction (return prev. state). */
5612
+ setEnabled(flag = true) {
5613
+ const prev = this.enabled;
5614
+ this.enabled = !!flag;
5615
+ this.element.classList.toggle("wb-disabled", !flag);
5616
+ return prev;
5617
+ }
5618
+ /** Return false if tree is disabled. */
5619
+ isEnabled() {
5620
+ return this.enabled;
5621
+ }
5622
+ /** Return true if tree has more than one column, i.e. has additional data columns. */
5623
+ isGrid() {
5624
+ return this.columns && this.columns.length > 1;
5625
+ }
5626
+ /** Return true if cell-navigation mode is acive. */
5627
+ isCellNav() {
5628
+ return !!this._cellNavMode;
5629
+ }
5630
+ /** Return true if row-navigation mode is acive. */
5631
+ isRowNav() {
5632
+ return !this._cellNavMode;
5163
5633
  }
5164
5634
  /** 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) {
5635
+ setCellNav(flag = true) {
5636
+ var _a;
5637
+ const prev = this._cellNavMode;
5638
+ // if (flag === prev) {
5639
+ // return;
5640
+ // }
5641
+ this._cellNavMode = !!flag;
5642
+ if (flag && !prev) {
5643
+ // switch from row to cell mode
5644
+ this.setColumn(0);
5645
+ }
5646
+ this.element.classList.toggle("wb-cell-mode", flag);
5647
+ (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.setModified(ChangeType.status);
5648
+ }
5649
+ /** Set the tree's navigation mode option. */
5650
+ setNavigationOption(mode, reset = false) {
5651
+ if (!this.isGrid() && mode !== NavigationOptions.row) {
5652
+ this.logWarn("Plain trees only support row navigation mode.");
5169
5653
  return;
5170
5654
  }
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);
5655
+ this.options.navigationModeOption = mode;
5656
+ switch (mode) {
5657
+ case NavigationOptions.cell:
5658
+ this.setCellNav(true);
5659
+ break;
5660
+ case NavigationOptions.row:
5661
+ this.setCellNav(false);
5662
+ break;
5663
+ case NavigationOptions.startCell:
5664
+ if (reset) {
5665
+ this.setCellNav(true);
5666
+ }
5667
+ break;
5668
+ case NavigationOptions.startRow:
5669
+ if (reset) {
5670
+ this.setCellNav(false);
5671
+ }
5672
+ break;
5673
+ default:
5674
+ error(`Invalid mode '${mode}'`);
5176
5675
  }
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);
5180
5676
  }
5181
5677
  /** Display tree status (ok, loading, error, noData) using styles and a dummy root node. */
5182
- setStatus(status, message, details) {
5183
- return this.root.setStatus(status, message, details);
5678
+ setStatus(status, options) {
5679
+ return this.root.setStatus(status, options);
5680
+ }
5681
+ /** Add or redefine node type definitions. */
5682
+ setTypes(types, replace = true) {
5683
+ assert(isPlainObject(types));
5684
+ if (replace) {
5685
+ this.types = types;
5686
+ }
5687
+ else {
5688
+ extend(this.types, types);
5689
+ }
5690
+ // Convert `TYPE.classes` to a Set
5691
+ for (let t of Object.values(this.types)) {
5692
+ if (t.classes) {
5693
+ t.classes = toSet(t.classes);
5694
+ }
5695
+ }
5184
5696
  }
5185
5697
  /** Update column headers and width. */
5186
5698
  updateColumns(opts) {
5187
5699
  opts = Object.assign({ calculateCols: true, updateRows: true }, opts);
5188
- const minWidth = 4;
5700
+ const defaultMinWidth = 4;
5189
5701
  const vpWidth = this.element.clientWidth;
5702
+ const isGrid = this.isGrid();
5703
+ let totalWidth = 0;
5190
5704
  let totalWeight = 0;
5191
5705
  let fixedWidth = 0;
5192
5706
  let modified = false;
5707
+ this.element.classList.toggle("wb-grid", isGrid);
5708
+ if (!isGrid && this.isCellNav()) {
5709
+ this.setCellNav(false);
5710
+ }
5193
5711
  if (opts.calculateCols) {
5194
- // Gather width requests
5712
+ // Gather width definitions
5195
5713
  this._columnsById = {};
5196
5714
  for (let col of this.columns) {
5197
5715
  this._columnsById[col.id] = col;
@@ -5214,14 +5732,25 @@ class Wunderbaum {
5214
5732
  fixedWidth += px;
5215
5733
  }
5216
5734
  else {
5217
- error("Invalid column width: " + cw);
5735
+ error(`Invalid column width: ${cw}`);
5218
5736
  }
5219
5737
  }
5220
5738
  // Share remaining space between non-fixed columns
5221
5739
  const restPx = Math.max(0, vpWidth - fixedWidth);
5222
5740
  let ofsPx = 0;
5223
5741
  for (let col of this.columns) {
5742
+ let minWidth;
5224
5743
  if (col._weight) {
5744
+ const cmw = col.minWidth;
5745
+ if (typeof cmw === "number") {
5746
+ minWidth = cmw;
5747
+ }
5748
+ else if (typeof cmw === "string" && cmw.endsWith("px")) {
5749
+ minWidth = parseFloat(cmw.slice(0, -2));
5750
+ }
5751
+ else {
5752
+ minWidth = defaultMinWidth;
5753
+ }
5225
5754
  const px = Math.max(minWidth, (restPx * col._weight) / totalWeight);
5226
5755
  if (col._widthPx != px) {
5227
5756
  modified = true;
@@ -5231,7 +5760,14 @@ class Wunderbaum {
5231
5760
  col._ofsPx = ofsPx;
5232
5761
  ofsPx += col._widthPx;
5233
5762
  }
5763
+ totalWidth = ofsPx;
5234
5764
  }
5765
+ // if (this.options.fixedCol) {
5766
+ // 'position: fixed' requires that the content has the correct size
5767
+ const tw = `${totalWidth}px`;
5768
+ this.headerElement.style.width = tw;
5769
+ this.scrollContainerElement.style.width = tw;
5770
+ // }
5235
5771
  // Every column has now a calculated `_ofsPx` and `_widthPx`
5236
5772
  // this.logInfo("UC", this.columns, vpWidth, this.element.clientWidth, this.element);
5237
5773
  // console.trace();
@@ -5247,91 +5783,124 @@ class Wunderbaum {
5247
5783
  * @internal
5248
5784
  */
5249
5785
  _renderHeaderMarkup() {
5250
- if (!this.headerElement) {
5786
+ assert(this.headerElement);
5787
+ const wantHeader = this.hasHeader();
5788
+ setElemDisplay(this.headerElement, wantHeader);
5789
+ if (!wantHeader) {
5251
5790
  return;
5252
5791
  }
5792
+ const colCount = this.columns.length;
5253
5793
  const headerRow = this.headerElement.querySelector(".wb-row");
5254
5794
  assert(headerRow);
5255
- headerRow.innerHTML = "<span class='wb-col'></span>".repeat(this.columns.length);
5256
- for (let i = 0; i < this.columns.length; i++) {
5795
+ headerRow.innerHTML = "<span class='wb-col'></span>".repeat(colCount);
5796
+ for (let i = 0; i < colCount; i++) {
5257
5797
  const col = this.columns[i];
5258
5798
  const colElem = headerRow.children[i];
5259
5799
  colElem.style.left = col._ofsPx + "px";
5260
5800
  colElem.style.width = col._widthPx + "px";
5261
- // colElem.textContent = col.title || col.id;
5262
5801
  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>`;
5802
+ let tooltip = "";
5803
+ if (col.tooltip) {
5804
+ tooltip = escapeTooltip(col.tooltip);
5805
+ tooltip = ` title="${tooltip}"`;
5806
+ }
5807
+ let resizer = "";
5808
+ if (i < colCount - 1) {
5809
+ resizer = '<span class="wb-col-resizer"></span>';
5810
+ }
5811
+ colElem.innerHTML = `<span class="wb-col-title"${tooltip}>${title}</span>${resizer}`;
5265
5812
  }
5266
5813
  }
5267
- /** Render header and all rows that are visible in the viewport (async, throttled). */
5268
- updateViewport(immediate = false) {
5269
- // Call the `throttle` wrapper for `this._updateViewport()` which will
5270
- // execute immediately on the leading edge of a sequence:
5271
- this._updateViewportThrottled();
5272
- if (immediate) {
5273
- this._updateViewportThrottled.flush();
5814
+ /**
5815
+ * Render pending changes that were scheduled using {@link WunderbaumNode.setModified} if any.
5816
+ *
5817
+ * This is hardly ever neccessary, since we normally either
5818
+ * - call `setModified(ChangeType.TYPE)` (async, throttled), or
5819
+ * - call `setModified(ChangeType.TYPE, {immediate: true})` (synchronous)
5820
+ *
5821
+ * `updatePendingModifications()` will only force immediate execution of
5822
+ * pending async changes if any.
5823
+ */
5824
+ updatePendingModifications() {
5825
+ if (this.changeRedrawRequestPending || this.changeScrollRequestPending) {
5826
+ this._updateViewportImmediately();
5274
5827
  }
5275
5828
  }
5276
5829
  /**
5277
5830
  * 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
5831
  * It calls `updateColumns()` and `_updateRows()`.
5832
+ *
5833
+ * This protected method should not be called directly but via
5834
+ * {@link WunderbaumNode.setModified}`, {@link Wunderbaum.setModified},
5835
+ * or {@link Wunderbaum.updatePendingModifications}.
5281
5836
  * @internal
5282
5837
  */
5283
- _updateViewport() {
5838
+ _updateViewportImmediately() {
5839
+ var _a;
5284
5840
  if (this._disableUpdateCount) {
5285
- this.log(`IGNORED _updateViewport() disable level: ${this._disableUpdateCount}`);
5841
+ this.log(`IGNORED _updateViewportImmediately() disable level: ${this._disableUpdateCount}`);
5286
5842
  return;
5287
5843
  }
5288
5844
  const newNodesOnly = !this.changeRedrawRequestPending;
5289
5845
  this.changeRedrawRequestPending = false;
5290
- let height = this.scrollContainer.clientHeight;
5846
+ this.changeScrollRequestPending = false;
5847
+ let height = this.scrollContainerElement.clientHeight;
5291
5848
  // We cannot get the height for absolute positioned parent, so look at first col
5292
5849
  // let headerHeight = this.headerElement.clientHeight
5293
5850
  // let headerHeight = this.headerElement.children[0].children[0].clientHeight;
5294
- const headerHeight = this.options.headerHeightPx;
5851
+ // const headerHeight = this.options.headerHeightPx;
5852
+ const headerHeight = this.headerElement.clientHeight; // May be 0
5295
5853
  const wantHeight = this.element.clientHeight - headerHeight;
5296
5854
  if (Math.abs(height - wantHeight) > 1.0) {
5297
5855
  // this.log("resize", height, wantHeight);
5298
- this.scrollContainer.style.height = wantHeight + "px";
5856
+ this.scrollContainerElement.style.height = wantHeight + "px";
5299
5857
  height = wantHeight;
5300
5858
  }
5859
+ // console.profile(`_updateViewportImmediately()`)
5301
5860
  this.updateColumns({ updateRows: false });
5302
5861
  this._updateRows({ newNodesOnly: newNodesOnly });
5862
+ // console.profileEnd(`_updateViewportImmediately()`)
5863
+ if (this.options.connectTopBreadcrumb) {
5864
+ let path = (_a = this.getTopmostVpNode(true)) === null || _a === void 0 ? void 0 : _a.getPath(false, "title", " > ");
5865
+ path = path ? path + " >" : "";
5866
+ this.options.connectTopBreadcrumb.textContent = path;
5867
+ }
5303
5868
  this._callEvent("update");
5304
5869
  }
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
- }
5870
+ // /**
5871
+ // * Assert that TR order matches the natural node order
5872
+ // * @internal
5873
+ // */
5874
+ // protected _validateRows(): boolean {
5875
+ // let trs = this.nodeListElement.childNodes;
5876
+ // let i = 0;
5877
+ // let prev = -1;
5878
+ // let ok = true;
5879
+ // trs.forEach((element) => {
5880
+ // const tr = element as HTMLTableRowElement;
5881
+ // const top = Number.parseInt(tr.style.top);
5882
+ // const n = (<any>tr)._wb_node;
5883
+ // // if (i < 4) {
5884
+ // // console.info(
5885
+ // // `TR#${i}, rowIdx=${n._rowIdx} , top=${top}px: '${n.title}'`
5886
+ // // );
5887
+ // // }
5888
+ // if (prev >= 0 && top !== prev + ROW_HEIGHT) {
5889
+ // n.logWarn(
5890
+ // `TR order mismatch at index ${i}: top=${top}px != ${
5891
+ // prev + ROW_HEIGHT
5892
+ // }`
5893
+ // );
5894
+ // // throw new Error("fault");
5895
+ // ok = false;
5896
+ // }
5897
+ // prev = top;
5898
+ // i++;
5899
+ // });
5900
+ // return ok;
5901
+ // }
5333
5902
  /*
5334
- * - Traverse all *visible* of the whole tree, i.e. skip collapsed nodes.
5903
+ * - Traverse all *visible* nodes of the whole tree, i.e. skip collapsed nodes.
5335
5904
  * - Store count of rows to `tree.treeRowCount`.
5336
5905
  * - Renumber `node._rowIdx` for all visible nodes.
5337
5906
  * - Calculate the index range that must be rendered to fill the viewport
@@ -5339,13 +5908,14 @@ class Wunderbaum {
5339
5908
  * -
5340
5909
  */
5341
5910
  _updateRows(opts) {
5342
- const label = this.logTime("_updateRows");
5911
+ // const label = this.logTime("_updateRows");
5912
+ // this.log("_updateRows", opts)
5343
5913
  opts = Object.assign({ newNodesOnly: false }, opts);
5344
5914
  const newNodesOnly = !!opts.newNodesOnly;
5345
5915
  const row_height = ROW_HEIGHT;
5346
- const vp_height = this.scrollContainer.clientHeight;
5916
+ const vp_height = this.element.clientHeight;
5347
5917
  const prefetch = RENDER_MAX_PREFETCH;
5348
- const ofs = this.scrollContainer.scrollTop;
5918
+ const ofs = this.element.scrollTop;
5349
5919
  let startIdx = Math.max(0, ofs / row_height - prefetch);
5350
5920
  startIdx = Math.floor(startIdx);
5351
5921
  // Make sure start is always even, so the alternating row colors don't
@@ -5369,7 +5939,7 @@ class Wunderbaum {
5369
5939
  let modified = false;
5370
5940
  let prevElem = "first";
5371
5941
  this.visitRows(function (node) {
5372
- // console.log("visit", node)
5942
+ // node.log("visit")
5373
5943
  const rowDiv = node._rowElem;
5374
5944
  // Renumber all expanded nodes
5375
5945
  if (node._rowIdx !== idx) {
@@ -5391,8 +5961,11 @@ class Wunderbaum {
5391
5961
  else {
5392
5962
  obsoleteNodes.delete(node);
5393
5963
  // Create new markup
5964
+ if (rowDiv) {
5965
+ rowDiv.style.top = idx * ROW_HEIGHT + "px";
5966
+ }
5394
5967
  node.render({ top: top, after: prevElem });
5395
- // console.log("render", top, prevElem, "=>", node._rowElem);
5968
+ // node.log("render", top, prevElem, "=>", node._rowElem);
5396
5969
  prevElem = node._rowElem;
5397
5970
  }
5398
5971
  idx++;
@@ -5409,8 +5982,8 @@ class Wunderbaum {
5409
5982
  // `render(scrollOfs:${ofs}, ${startIdx}..${endIdx})`,
5410
5983
  // this.nodeListElement.style.height
5411
5984
  // );
5412
- this.logTimeEnd(label);
5413
- this._validateRows();
5985
+ // this.logTimeEnd(label);
5986
+ // this._validateRows();
5414
5987
  return modified;
5415
5988
  }
5416
5989
  /**
@@ -5557,17 +6130,11 @@ class Wunderbaum {
5557
6130
  /**
5558
6131
  * Reload the tree with a new source.
5559
6132
  *
5560
- * Previous data is cleared.
5561
- * Pass `options.columns` to define a header (may also be part of `source.columns`).
6133
+ * Previous data is cleared. Note that also column- and type defintions may
6134
+ * be passed with the `source` object.
5562
6135
  */
5563
- load(source, options = {}) {
6136
+ load(source) {
5564
6137
  this.clear();
5565
- const columns = options.columns || source.columns;
5566
- if (columns) {
5567
- this.columns = options.columns;
5568
- // this._renderHeaderMarkup();
5569
- this.updateColumns({ calculateCols: false });
5570
- }
5571
6138
  return this.root.load(source);
5572
6139
  }
5573
6140
  /**
@@ -5578,7 +6145,7 @@ class Wunderbaum {
5578
6145
  * tree.enableUpdate(false);
5579
6146
  * // ... (long running operation that would trigger many updates)
5580
6147
  * foo();
5581
- * // ... NOTE: make sure that async operations have finished
6148
+ * // ... NOTE: make sure that async operations have finished, e.g.
5582
6149
  * await foo();
5583
6150
  * } finally {
5584
6151
  * tree.enableUpdate(true);
@@ -5599,7 +6166,9 @@ class Wunderbaum {
5599
6166
  // `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...`
5600
6167
  // );
5601
6168
  if (this._disableUpdateCount === 0) {
5602
- this.updateViewport();
6169
+ // this.changeRedrawRequestPending = true; // make sure, we re-render all markup
6170
+ // this.updateViewport();
6171
+ this.setModified(ChangeType.any, { immediate: true });
5603
6172
  }
5604
6173
  }
5605
6174
  else {
@@ -5641,7 +6210,7 @@ class Wunderbaum {
5641
6210
  }
5642
6211
  Wunderbaum.sequence = 0;
5643
6212
  /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
5644
- Wunderbaum.version = "v0.0.3"; // Set to semver by 'grunt release'
6213
+ Wunderbaum.version = "v0.0.6"; // Set to semver by 'grunt release'
5645
6214
  /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
5646
6215
  Wunderbaum.util = util;
5647
6216