wunderbaum 0.0.1 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,9 @@
1
1
  /*!
2
2
  * Wunderbaum - util
3
3
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
4
- * v0.0.1-0, Thu, 31 Mar 2022 15:13:20 GMT (https://github.com/mar10/wunderbaum)
4
+ * v0.0.4, Tue, 03 May 2022 06:21:31 GMT (https://github.com/mar10/wunderbaum)
5
5
  */
6
+ /** @module util */
6
7
  /** Readable names for `MouseEvent.button` */
7
8
  const MOUSE_BUTTONS = {
8
9
  0: "",
@@ -14,7 +15,7 @@ const MOUSE_BUTTONS = {
14
15
  };
15
16
  const MAX_INT = 9007199254740991;
16
17
  const userInfo = _getUserInfo();
17
- /**True if the user is using a macOS platform. */
18
+ /**True if the client is using a macOS platform. */
18
19
  const isMac = userInfo.isMac;
19
20
  const REX_HTML = /[&<>"'/]/g; // Escape those characters
20
21
  const REX_TOOLTIP = /[<>"'/]/g; // Don't escape `&` in tooltips
@@ -162,7 +163,7 @@ function escapeTooltip(s) {
162
163
  /** TODO */
163
164
  function extractHtmlText(s) {
164
165
  if (s.indexOf(">") >= 0) {
165
- error("not implemented");
166
+ error("Not implemented");
166
167
  // return $("<div/>").html(s).text();
167
168
  }
168
169
  return s;
@@ -284,7 +285,7 @@ function setValueToElem(elem, value) {
284
285
  input.valueAsNumber = value;
285
286
  break;
286
287
  case "radio":
287
- assert(false, "not implemented");
288
+ error("Not implemented");
288
289
  // const name = input.name;
289
290
  // const checked = input.parentElement!.querySelector(
290
291
  // `input[name="${name}"]:checked`
@@ -298,16 +299,15 @@ function setValueToElem(elem, value) {
298
299
  break;
299
300
  case "text":
300
301
  default:
301
- input.innerText = value;
302
+ input.value = value || "";
302
303
  }
303
304
  }
304
305
  else if (tag === "SELECT") {
305
306
  const select = elem;
306
307
  select.value = value;
307
308
  }
308
- // return value;
309
309
  }
310
- /** Return an unconnected `HTMLElement` from a HTML string. */
310
+ /** Create and return an unconnected `HTMLElement` from a HTML string. */
311
311
  function elemFromHtml(html) {
312
312
  const t = document.createElement("template");
313
313
  t.innerHTML = html.trim();
@@ -324,11 +324,23 @@ function elemFromSelector(obj) {
324
324
  }
325
325
  return obj;
326
326
  }
327
+ /** Return a EventTarget from selector or cast an existing element. */
328
+ function eventTargetFromSelector(obj) {
329
+ if (!obj) {
330
+ return null;
331
+ }
332
+ if (typeof obj === "string") {
333
+ return document.querySelector(obj);
334
+ }
335
+ return obj;
336
+ }
327
337
  /**
328
- * Return a descriptive string for a keyboard or mouse event.
338
+ * Return a canonical descriptive string for a keyboard or mouse event.
329
339
  *
330
340
  * The result also contains a prefix for modifiers if any, for example
331
341
  * `"x"`, `"F2"`, `"Control+Home"`, or `"Shift+clickright"`.
342
+ * This is especially useful in `switch` statements, to make sure that modifier
343
+ * keys are considered and handled correctly.
332
344
  */
333
345
  function eventToString(event) {
334
346
  let key = event.key, et = event.type, s = [];
@@ -360,6 +372,13 @@ function eventToString(event) {
360
372
  }
361
373
  return s.join("+");
362
374
  }
375
+ /**
376
+ * Copy allproperties from one or more source objects to a target object.
377
+ *
378
+ * @returns the modified target object.
379
+ */
380
+ // TODO: use Object.assign()? --> https://stackoverflow.com/a/42740894
381
+ // TODO: support deep merge --> https://stackoverflow.com/a/42740894
363
382
  function extend(...args) {
364
383
  for (let i = 1; i < args.length; i++) {
365
384
  let arg = args[i];
@@ -374,23 +393,27 @@ function extend(...args) {
374
393
  }
375
394
  return args[0];
376
395
  }
396
+ /** Return true if `obj` is of type `array`. */
377
397
  function isArray(obj) {
378
398
  return Array.isArray(obj);
379
399
  }
400
+ /** Return true if `obj` is of type `Object` and has no propertied. */
380
401
  function isEmptyObject(obj) {
381
402
  return Object.keys(obj).length === 0 && obj.constructor === Object;
382
403
  }
404
+ /** Return true if `obj` is of type `function`. */
383
405
  function isFunction(obj) {
384
406
  return typeof obj === "function";
385
407
  }
408
+ /** Return true if `obj` is of type `Object`. */
386
409
  function isPlainObject(obj) {
387
410
  return Object.prototype.toString.call(obj) === "[object Object]";
388
411
  }
389
412
  /** A dummy function that does nothing ('no operation'). */
390
413
  function noop(...args) { }
391
- function onEvent(rootElem, eventNames, selectorOrHandler, handlerOrNone) {
414
+ function onEvent(rootTarget, eventNames, selectorOrHandler, handlerOrNone) {
392
415
  let selector, handler;
393
- rootElem = elemFromSelector(rootElem);
416
+ rootTarget = eventTargetFromSelector(rootTarget);
394
417
  if (handlerOrNone) {
395
418
  selector = selectorOrHandler;
396
419
  handler = handlerOrNone;
@@ -400,7 +423,7 @@ function onEvent(rootElem, eventNames, selectorOrHandler, handlerOrNone) {
400
423
  handler = selectorOrHandler;
401
424
  }
402
425
  eventNames.split(" ").forEach((evn) => {
403
- rootElem.addEventListener(evn, function (e) {
426
+ rootTarget.addEventListener(evn, function (e) {
404
427
  if (!selector) {
405
428
  return handler(e); // no event delegation
406
429
  }
@@ -455,7 +478,7 @@ function setTimeoutPromise(callback, ms) {
455
478
  return new Promise((resolve, reject) => {
456
479
  setTimeout(() => {
457
480
  try {
458
- resolve(callback.apply(self));
481
+ resolve(callback.apply(this));
459
482
  }
460
483
  catch (err) {
461
484
  reject(err);
@@ -478,6 +501,13 @@ async function sleep(ms) {
478
501
  }
479
502
  /**
480
503
  * Set or rotate checkbox status with support for tri-state.
504
+ *
505
+ * An initial 'indeterminate' state becomes 'checked' on the first call.
506
+ *
507
+ * If the input element has the class 'wb-tristate' assigned, the sequence is:<br>
508
+ * 'indeterminate' -> 'checked' -> 'unchecked' -> 'indeterminate' -> ...<br>
509
+ * Otherwise we toggle like <br>
510
+ * 'checked' -> 'unchecked' -> 'checked' -> ...
481
511
  */
482
512
  function toggleCheckbox(element, value, tristate) {
483
513
  const input = elemFromSelector(element);
@@ -535,12 +565,79 @@ function toSet(val) {
535
565
  }
536
566
  throw new Error("Cannot convert to Set<string>: " + val);
537
567
  }
568
+ /**Return a canonical string representation for an object's type (e.g. 'array', 'number', ...) */
538
569
  function type(obj) {
539
570
  return Object.prototype.toString
540
571
  .call(obj)
541
572
  .replace(/^\[object (.+)\]$/, "$1")
542
573
  .toLowerCase();
543
574
  }
575
+ /**
576
+ * Return a function that can be called instead of `callback`, but guarantees
577
+ * a limited execution rate.
578
+ * The execution rate is calculated based on the runtime duration of the
579
+ * previous call.
580
+ * Example:
581
+ * ```js
582
+ * throttledFoo = util.addaptiveThrottle(foo.bind(this), {});
583
+ * throttledFoo();
584
+ * throttledFoo();
585
+ * ```
586
+ */
587
+ function addaptiveThrottle(callback, options) {
588
+ let waiting = 0; // Initially, we're not waiting
589
+ let pendingArgs = null;
590
+ const opts = Object.assign({
591
+ minDelay: 16,
592
+ defaultDelay: 200,
593
+ maxDelay: 5000,
594
+ delayFactor: 2.0,
595
+ }, options);
596
+ const minDelay = Math.max(16, +opts.minDelay);
597
+ const maxDelay = +opts.maxDelay;
598
+ const throttledFn = (...args) => {
599
+ if (waiting) {
600
+ pendingArgs = args;
601
+ // console.log(`addaptiveThrottle() queing request #${waiting}...`, args);
602
+ waiting += 1;
603
+ }
604
+ else {
605
+ // Prevent invocations while running or blocking
606
+ waiting = 1;
607
+ const useArgs = args; // pendingArgs || args;
608
+ pendingArgs = null;
609
+ // console.log(`addaptiveThrottle() execute...`, useArgs);
610
+ const start = Date.now();
611
+ try {
612
+ callback.apply(this, useArgs);
613
+ }
614
+ catch (error) {
615
+ console.error(error);
616
+ }
617
+ const elap = Date.now() - start;
618
+ const curDelay = Math.min(Math.max(minDelay, elap * opts.delayFactor), maxDelay);
619
+ const useDelay = Math.max(minDelay, curDelay - elap);
620
+ // console.log(
621
+ // `addaptiveThrottle() calling worker took ${elap}ms. delay = ${curDelay}ms, using ${useDelay}ms`,
622
+ // pendingArgs
623
+ // );
624
+ setTimeout(() => {
625
+ // Unblock, and trigger pending requests if any
626
+ // const skipped = waiting - 1;
627
+ waiting = 0; // And allow future invocations
628
+ if (pendingArgs != null) {
629
+ // There was another request while running or waiting
630
+ // console.log(
631
+ // `addaptiveThrottle() re-trigger (missed ${skipped})...`,
632
+ // pendingArgs
633
+ // );
634
+ throttledFn.apply(this, pendingArgs);
635
+ }
636
+ }, useDelay);
637
+ }
638
+ };
639
+ return throttledFn;
640
+ }
544
641
 
545
642
  var util = /*#__PURE__*/Object.freeze({
546
643
  __proto__: null,
@@ -561,6 +658,7 @@ var util = /*#__PURE__*/Object.freeze({
561
658
  setValueToElem: setValueToElem,
562
659
  elemFromHtml: elemFromHtml,
563
660
  elemFromSelector: elemFromSelector,
661
+ eventTargetFromSelector: eventTargetFromSelector,
564
662
  eventToString: eventToString,
565
663
  extend: extend,
566
664
  isArray: isArray,
@@ -575,13 +673,14 @@ var util = /*#__PURE__*/Object.freeze({
575
673
  toggleCheckbox: toggleCheckbox,
576
674
  getOption: getOption,
577
675
  toSet: toSet,
578
- type: type
676
+ type: type,
677
+ addaptiveThrottle: addaptiveThrottle
579
678
  });
580
679
 
581
680
  /*!
582
681
  * Wunderbaum - common
583
682
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
584
- * v0.0.1-0, Thu, 31 Mar 2022 15:13:20 GMT (https://github.com/mar10/wunderbaum)
683
+ * v0.0.4, Tue, 03 May 2022 06:21:31 GMT (https://github.com/mar10/wunderbaum)
585
684
  */
586
685
  const DEFAULT_DEBUGLEVEL = 4; // Replaced by rollup script
587
686
  const ROW_HEIGHT = 22;
@@ -591,10 +690,20 @@ const RENDER_MAX_PREFETCH = 5;
591
690
  const TEST_IMG = new RegExp(/\.|\//); // strings are considered image urls if they contain '.' or '/'
592
691
  var ChangeType;
593
692
  (function (ChangeType) {
693
+ /** Re-render the whole viewport, headers, and all rows. */
594
694
  ChangeType["any"] = "any";
695
+ /** Update current row title, icon, columns, and status. */
696
+ ChangeType["data"] = "data";
697
+ /** Redraw the header and update the width of all row columns. */
698
+ ChangeType["header"] = "header";
699
+ /** Re-render the whole current row. */
595
700
  ChangeType["row"] = "row";
701
+ /** Alias for 'any'. */
596
702
  ChangeType["structure"] = "structure";
703
+ /** Update current row's classes, to reflect active, selected, ... */
597
704
  ChangeType["status"] = "status";
705
+ /** Update the 'top' property of all rows. */
706
+ ChangeType["vscroll"] = "vscroll";
598
707
  })(ChangeType || (ChangeType = {}));
599
708
  var NodeStatusType;
600
709
  (function (NodeStatusType) {
@@ -667,6 +776,8 @@ const KEY_TO_ACTION_DICT = {
667
776
  Home: "firstCol",
668
777
  "Control+End": "last",
669
778
  "Control+Home": "first",
779
+ "Meta+ArrowDown": "last",
780
+ "Meta+ArrowUp": "first",
670
781
  "*": "expandAll",
671
782
  Multiply: "expandAll",
672
783
  PageDown: "pageDown",
@@ -693,7 +804,7 @@ function makeNodeTitleStartMatcher(s) {
693
804
  /*!
694
805
  * Wunderbaum - wb_extension_base
695
806
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
696
- * v0.0.1-0, Thu, 31 Mar 2022 15:13:20 GMT (https://github.com/mar10/wunderbaum)
807
+ * v0.0.4, Tue, 03 May 2022 06:21:31 GMT (https://github.com/mar10/wunderbaum)
697
808
  */
698
809
  class WunderbaumExtension {
699
810
  constructor(tree, id, defaults) {
@@ -716,14 +827,14 @@ class WunderbaumExtension {
716
827
  init() {
717
828
  this.tree.element.classList.add("wb-ext-" + this.id);
718
829
  }
719
- // protected callEvent(name: string, extra?: any): any {
720
- // let func = this.extensionOpts[name];
830
+ // protected callEvent(type: string, extra?: any): any {
831
+ // let func = this.extensionOpts[type];
721
832
  // if (func) {
722
833
  // return func.call(
723
834
  // this.tree,
724
835
  // util.extend(
725
836
  // {
726
- // event: this.id + "." + name,
837
+ // event: this.id + "." + type,
727
838
  // },
728
839
  // extra
729
840
  // )
@@ -980,75 +1091,11 @@ function debounce(func, wait = 0, options = {}) {
980
1091
  debounced.pending = pending;
981
1092
  return debounced;
982
1093
  }
983
- /**
984
- * Creates a throttled function that only invokes `func` at most once per
985
- * every `wait` milliseconds (or once per browser frame). The throttled function
986
- * comes with a `cancel` method to cancel delayed `func` invocations and a
987
- * `flush` method to immediately invoke them. Provide `options` to indicate
988
- * whether `func` should be invoked on the leading and/or trailing edge of the
989
- * `wait` timeout. The `func` is invoked with the last arguments provided to the
990
- * throttled function. Subsequent calls to the throttled function return the
991
- * result of the last `func` invocation.
992
- *
993
- * **Note:** If `leading` and `trailing` options are `true`, `func` is
994
- * invoked on the trailing edge of the timeout only if the throttled function
995
- * is invoked more than once during the `wait` timeout.
996
- *
997
- * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
998
- * until the next tick, similar to `setTimeout` with a timeout of `0`.
999
- *
1000
- * If `wait` is omitted in an environment with `requestAnimationFrame`, `func`
1001
- * invocation will be deferred until the next frame is drawn (typically about
1002
- * 16ms).
1003
- *
1004
- * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
1005
- * for details over the differences between `throttle` and `debounce`.
1006
- *
1007
- * @since 0.1.0
1008
- * @category Function
1009
- * @param {Function} func The function to throttle.
1010
- * @param {number} [wait=0]
1011
- * The number of milliseconds to throttle invocations to; if omitted,
1012
- * `requestAnimationFrame` is used (if available).
1013
- * @param {Object} [options={}] The options object.
1014
- * @param {boolean} [options.leading=true]
1015
- * Specify invoking on the leading edge of the timeout.
1016
- * @param {boolean} [options.trailing=true]
1017
- * Specify invoking on the trailing edge of the timeout.
1018
- * @returns {Function} Returns the new throttled function.
1019
- * @example
1020
- *
1021
- * // Avoid excessively updating the position while scrolling.
1022
- * jQuery(window).on('scroll', throttle(updatePosition, 100))
1023
- *
1024
- * // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.
1025
- * const throttled = throttle(renewToken, 300000, { 'trailing': false })
1026
- * jQuery(element).on('click', throttled)
1027
- *
1028
- * // Cancel the trailing throttled invocation.
1029
- * jQuery(window).on('popstate', throttled.cancel)
1030
- */
1031
- function throttle(func, wait = 0, options = {}) {
1032
- let leading = true;
1033
- let trailing = true;
1034
- if (typeof func !== "function") {
1035
- throw new TypeError("Expected a function");
1036
- }
1037
- if (isObject(options)) {
1038
- leading = "leading" in options ? !!options.leading : leading;
1039
- trailing = "trailing" in options ? !!options.trailing : trailing;
1040
- }
1041
- return debounce(func, wait, {
1042
- leading,
1043
- trailing,
1044
- maxWait: wait,
1045
- });
1046
- }
1047
1094
 
1048
1095
  /*!
1049
1096
  * Wunderbaum - ext-filter
1050
1097
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1051
- * v0.0.1-0, Thu, 31 Mar 2022 15:13:20 GMT (https://github.com/mar10/wunderbaum)
1098
+ * v0.0.4, Tue, 03 May 2022 06:21:31 GMT (https://github.com/mar10/wunderbaum)
1052
1099
  */
1053
1100
  const START_MARKER = "\uFFF7";
1054
1101
  const END_MARKER = "\uFFF8";
@@ -1087,7 +1134,9 @@ class FilterExtension extends WunderbaumExtension {
1087
1134
  });
1088
1135
  }
1089
1136
  _applyFilterImpl(filter, branchMode, _opts) {
1090
- let match, temp, start = Date.now(), count = 0, tree = this.tree, treeOpts = tree.options, escapeTitles = treeOpts.escapeTitles, prevAutoCollapse = treeOpts.autoCollapse, opts = extend({}, treeOpts.filter, _opts), hideMode = opts.mode === "hide", leavesOnly = !!opts.leavesOnly && !branchMode;
1137
+ let match, temp, start = Date.now(), count = 0, tree = this.tree, treeOpts = tree.options,
1138
+ // escapeTitles = treeOpts.escapeTitles,
1139
+ prevAutoCollapse = treeOpts.autoCollapse, opts = extend({}, treeOpts.filter, _opts), hideMode = opts.mode === "hide", leavesOnly = !!opts.leavesOnly && !branchMode;
1091
1140
  // Default to 'match title substring (case insensitive)'
1092
1141
  if (typeof filter === "string") {
1093
1142
  if (filter === "") {
@@ -1120,37 +1169,36 @@ class FilterExtension extends WunderbaumExtension {
1120
1169
  if (!node.title) {
1121
1170
  return false;
1122
1171
  }
1123
- let text = escapeTitles ? node.title : extractHtmlText(node.title);
1172
+ // let text = escapeTitles ? node.title : extractHtmlText(node.title);
1173
+ let text = node.title;
1124
1174
  // `.match` instead of `.test` to get the capture groups
1125
1175
  let res = text.match(re);
1126
1176
  if (res && opts.highlight) {
1127
- if (escapeTitles) {
1128
- if (opts.fuzzy) {
1129
- temp = _markFuzzyMatchedChars(text, res, escapeTitles);
1130
- }
1131
- else {
1132
- // #740: we must not apply the marks to escaped entity names, e.g. `&quot;`
1133
- // Use some exotic characters to mark matches:
1134
- temp = text.replace(reHighlight, function (s) {
1135
- return START_MARKER + s + END_MARKER;
1136
- });
1137
- }
1138
- // now we can escape the title...
1139
- node.titleWithHighlight = escapeHtml(temp)
1140
- // ... and finally insert the desired `<mark>` tags
1141
- .replace(RE_START_MARKER, "<mark>")
1142
- .replace(RE_END_MARTKER, "</mark>");
1177
+ // if (escapeTitles) {
1178
+ if (opts.fuzzy) {
1179
+ temp = _markFuzzyMatchedChars(text, res, true);
1143
1180
  }
1144
1181
  else {
1145
- if (opts.fuzzy) {
1146
- node.titleWithHighlight = _markFuzzyMatchedChars(text, res);
1147
- }
1148
- else {
1149
- node.titleWithHighlight = text.replace(reHighlight, function (s) {
1150
- return "<mark>" + s + "</mark>";
1151
- });
1152
- }
1182
+ // #740: we must not apply the marks to escaped entity names, e.g. `&quot;`
1183
+ // Use some exotic characters to mark matches:
1184
+ temp = text.replace(reHighlight, function (s) {
1185
+ return START_MARKER + s + END_MARKER;
1186
+ });
1153
1187
  }
1188
+ // now we can escape the title...
1189
+ node.titleWithHighlight = escapeHtml(temp)
1190
+ // ... and finally insert the desired `<mark>` tags
1191
+ .replace(RE_START_MARKER, "<mark>")
1192
+ .replace(RE_END_MARTKER, "</mark>");
1193
+ // } else {
1194
+ // if (opts.fuzzy) {
1195
+ // node.titleWithHighlight = _markFuzzyMatchedChars(text, res);
1196
+ // } else {
1197
+ // node.titleWithHighlight = text.replace(reHighlight, function (s) {
1198
+ // return "<mark>" + s + "</mark>";
1199
+ // });
1200
+ // }
1201
+ // }
1154
1202
  // node.debug("filter", escapeTitles, text, node.titleWithHighlight);
1155
1203
  }
1156
1204
  return !!res;
@@ -1252,9 +1300,9 @@ class FilterExtension extends WunderbaumExtension {
1252
1300
  * [ext-filter] Reset the filter.
1253
1301
  */
1254
1302
  clearFilter() {
1255
- let tree = this.tree,
1303
+ let tree = this.tree;
1256
1304
  // statusNode = tree.root.findDirectChild(KEY_NODATA),
1257
- escapeTitles = tree.options.escapeTitles;
1305
+ // escapeTitles = tree.options.escapeTitles;
1258
1306
  // enhanceTitle = tree.options.enhanceTitle,
1259
1307
  tree.enableUpdate(false);
1260
1308
  // if (statusNode) {
@@ -1268,12 +1316,11 @@ class FilterExtension extends WunderbaumExtension {
1268
1316
  if (node.match && node._rowElem) {
1269
1317
  // #491, #601
1270
1318
  let titleElem = node._rowElem.querySelector("span.wb-title");
1271
- if (escapeTitles) {
1272
- titleElem.textContent = node.title;
1273
- }
1274
- else {
1275
- titleElem.innerHTML = node.title;
1276
- }
1319
+ // if (escapeTitles) {
1320
+ titleElem.textContent = node.title;
1321
+ // } else {
1322
+ // titleElem.innerHTML = node.title;
1323
+ // }
1277
1324
  node._callEvent("enhanceTitle", { titleElem: titleElem });
1278
1325
  }
1279
1326
  delete node.match;
@@ -1309,7 +1356,7 @@ class FilterExtension extends WunderbaumExtension {
1309
1356
  * @param {string} text
1310
1357
  * @param {RegExpMatchArray} matches
1311
1358
  */
1312
- function _markFuzzyMatchedChars(text, matches, escapeTitles = false) {
1359
+ function _markFuzzyMatchedChars(text, matches, escapeTitles = true) {
1313
1360
  let matchingIndices = [];
1314
1361
  // get the indices of matched characters (Iterate through `RegExpMatchArray`)
1315
1362
  for (let _matchingArrIdx = 1; _matchingArrIdx < matches.length; _matchingArrIdx++) {
@@ -1324,7 +1371,7 @@ function _markFuzzyMatchedChars(text, matches, escapeTitles = false) {
1324
1371
  // Map each `text` char to its position and store in `textPoses`.
1325
1372
  let textPoses = text.split("");
1326
1373
  if (escapeTitles) {
1327
- // If escaping the title, then wrap the matchng char within exotic chars
1374
+ // If escaping the title, then wrap the matching char within exotic chars
1328
1375
  matchingIndices.forEach(function (v) {
1329
1376
  textPoses[v] = START_MARKER + textPoses[v] + END_MARKER;
1330
1377
  });
@@ -1342,7 +1389,7 @@ function _markFuzzyMatchedChars(text, matches, escapeTitles = false) {
1342
1389
  /*!
1343
1390
  * Wunderbaum - ext-keynav
1344
1391
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1345
- * v0.0.1-0, Thu, 31 Mar 2022 15:13:20 GMT (https://github.com/mar10/wunderbaum)
1392
+ * v0.0.4, Tue, 03 May 2022 06:21:31 GMT (https://github.com/mar10/wunderbaum)
1346
1393
  */
1347
1394
  class KeynavExtension extends WunderbaumExtension {
1348
1395
  constructor(tree) {
@@ -1409,7 +1456,7 @@ class KeynavExtension extends WunderbaumExtension {
1409
1456
  eventName = "Add"; // expand
1410
1457
  }
1411
1458
  else if (navModeOption === NavigationModeOption.startRow) {
1412
- tree.setCellMode(NavigationMode.cellNav);
1459
+ tree.setNavigationMode(NavigationMode.cellNav);
1413
1460
  return;
1414
1461
  }
1415
1462
  break;
@@ -1448,6 +1495,8 @@ class KeynavExtension extends WunderbaumExtension {
1448
1495
  case "Home":
1449
1496
  case "Control+End":
1450
1497
  case "Control+Home":
1498
+ case "Meta+ArrowDown":
1499
+ case "Meta+ArrowUp":
1451
1500
  case "PageDown":
1452
1501
  case "PageUp":
1453
1502
  node.navigate(eventName, { activate: activate, event: event });
@@ -1479,11 +1528,11 @@ class KeynavExtension extends WunderbaumExtension {
1479
1528
  break;
1480
1529
  case "Escape":
1481
1530
  if (tree.navMode === NavigationMode.cellEdit) {
1482
- tree.setCellMode(NavigationMode.cellNav);
1531
+ tree.setNavigationMode(NavigationMode.cellNav);
1483
1532
  handled = true;
1484
1533
  }
1485
1534
  else if (tree.navMode === NavigationMode.cellNav) {
1486
- tree.setCellMode(NavigationMode.row);
1535
+ tree.setNavigationMode(NavigationMode.row);
1487
1536
  handled = true;
1488
1537
  }
1489
1538
  break;
@@ -1493,7 +1542,7 @@ class KeynavExtension extends WunderbaumExtension {
1493
1542
  handled = true;
1494
1543
  }
1495
1544
  else if (navModeOption !== NavigationModeOption.cell) {
1496
- tree.setCellMode(NavigationMode.row);
1545
+ tree.setNavigationMode(NavigationMode.row);
1497
1546
  handled = true;
1498
1547
  }
1499
1548
  break;
@@ -1510,6 +1559,8 @@ class KeynavExtension extends WunderbaumExtension {
1510
1559
  case "Home":
1511
1560
  case "Control+End":
1512
1561
  case "Control+Home":
1562
+ case "Meta+ArrowDown":
1563
+ case "Meta+ArrowUp":
1513
1564
  case "PageDown":
1514
1565
  case "PageUp":
1515
1566
  node.navigate(eventName, { activate: activate, event: event });
@@ -1528,7 +1579,7 @@ class KeynavExtension extends WunderbaumExtension {
1528
1579
  /*!
1529
1580
  * Wunderbaum - ext-logger
1530
1581
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1531
- * v0.0.1-0, Thu, 31 Mar 2022 15:13:20 GMT (https://github.com/mar10/wunderbaum)
1582
+ * v0.0.4, Tue, 03 May 2022 06:21:31 GMT (https://github.com/mar10/wunderbaum)
1532
1583
  */
1533
1584
  class LoggerExtension extends WunderbaumExtension {
1534
1585
  constructor(tree) {
@@ -1568,19 +1619,9 @@ class LoggerExtension extends WunderbaumExtension {
1568
1619
  /*!
1569
1620
  * Wunderbaum - ext-dnd
1570
1621
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1571
- * v0.0.1-0, Thu, 31 Mar 2022 15:13:20 GMT (https://github.com/mar10/wunderbaum)
1622
+ * v0.0.4, Tue, 03 May 2022 06:21:31 GMT (https://github.com/mar10/wunderbaum)
1572
1623
  */
1573
1624
  const nodeMimeType = "application/x-wunderbaum-node";
1574
- // type AllowedDropRegionType =
1575
- // | "after"
1576
- // | "afterBefore"
1577
- // // | "afterBeforeOver" // == all == true
1578
- // | "afterOver"
1579
- // | "all" // == true
1580
- // | "before"
1581
- // | "beforeOver"
1582
- // | "none" // == false == "" == null
1583
- // | "over"; // == "child"
1584
1625
  class DndExtension extends WunderbaumExtension {
1585
1626
  constructor(tree) {
1586
1627
  super(tree, "dnd", {
@@ -1843,15 +1884,196 @@ class DndExtension extends WunderbaumExtension {
1843
1884
  }
1844
1885
  }
1845
1886
 
1887
+ /*!
1888
+ * Wunderbaum - drag_observer
1889
+ * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1890
+ * v0.0.4, Tue, 03 May 2022 06:21:31 GMT (https://github.com/mar10/wunderbaum)
1891
+ */
1892
+ /**
1893
+ * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'.
1894
+ */
1895
+ class DragObserver {
1896
+ constructor(opts) {
1897
+ this.start = {
1898
+ x: 0,
1899
+ y: 0,
1900
+ altKey: false,
1901
+ ctrlKey: false,
1902
+ metaKey: false,
1903
+ shiftKey: false,
1904
+ };
1905
+ this.dragElem = null;
1906
+ this.dragging = false;
1907
+ // TODO: touch events
1908
+ this.events = ["mousedown", "mouseup", "mousemove", "keydown"];
1909
+ assert(opts.root);
1910
+ this.opts = extend({ thresh: 5 }, opts);
1911
+ this.root = opts.root;
1912
+ this._handler = this.handleEvent.bind(this);
1913
+ this.events.forEach((type) => {
1914
+ this.root.addEventListener(type, this._handler);
1915
+ });
1916
+ }
1917
+ /** Unregister all event listeners. */
1918
+ disconnect() {
1919
+ this.events.forEach((type) => {
1920
+ this.root.removeEventListener(type, this._handler);
1921
+ });
1922
+ }
1923
+ getDragElem() {
1924
+ return this.dragElem;
1925
+ }
1926
+ isDragging() {
1927
+ return this.dragging;
1928
+ }
1929
+ stopDrag(cb_event) {
1930
+ if (this.dragging && this.opts.dragstop && cb_event) {
1931
+ cb_event.type = "dragstop";
1932
+ this.opts.dragstop(cb_event);
1933
+ }
1934
+ this.dragElem = null;
1935
+ this.dragging = false;
1936
+ }
1937
+ handleEvent(e) {
1938
+ const type = e.type;
1939
+ const opts = this.opts;
1940
+ const cb_event = {
1941
+ type: e.type,
1942
+ event: e,
1943
+ dragElem: this.dragElem,
1944
+ dx: e.pageX - this.start.x,
1945
+ dy: e.pageY - this.start.y,
1946
+ apply: undefined,
1947
+ };
1948
+ switch (type) {
1949
+ case "keydown":
1950
+ this.stopDrag(cb_event);
1951
+ break;
1952
+ case "mousedown":
1953
+ if (this.dragElem) {
1954
+ this.stopDrag(cb_event);
1955
+ break;
1956
+ }
1957
+ if (opts.selector) {
1958
+ let elem = e.target;
1959
+ if (elem.matches(opts.selector)) {
1960
+ this.dragElem = elem;
1961
+ }
1962
+ else {
1963
+ elem = elem.closest(opts.selector);
1964
+ if (elem) {
1965
+ this.dragElem = elem;
1966
+ }
1967
+ else {
1968
+ break; // no event delegation selector matched
1969
+ }
1970
+ }
1971
+ }
1972
+ this.start.x = e.pageX;
1973
+ this.start.y = e.pageY;
1974
+ this.start.altKey = e.altKey;
1975
+ this.start.ctrlKey = e.ctrlKey;
1976
+ this.start.metaKey = e.metaKey;
1977
+ this.start.shiftKey = e.shiftKey;
1978
+ break;
1979
+ case "mousemove":
1980
+ // TODO: debounce/throttle?
1981
+ // TODO: horizontal mode: ignore if dx unchanged
1982
+ if (!this.dragElem) {
1983
+ break;
1984
+ }
1985
+ if (!this.dragging) {
1986
+ if (opts.thresh) {
1987
+ const dist2 = cb_event.dx * cb_event.dx + cb_event.dy * cb_event.dy;
1988
+ if (dist2 < opts.thresh * opts.thresh) {
1989
+ break;
1990
+ }
1991
+ }
1992
+ cb_event.type = "dragstart";
1993
+ if (opts.dragstart(cb_event) === false) {
1994
+ this.stopDrag(cb_event);
1995
+ break;
1996
+ }
1997
+ this.dragging = true;
1998
+ }
1999
+ if (this.dragging && this.opts.drag) {
2000
+ cb_event.type = "drag";
2001
+ this.opts.drag(cb_event);
2002
+ }
2003
+ break;
2004
+ case "mouseup":
2005
+ if (!this.dragging) {
2006
+ this.stopDrag(cb_event);
2007
+ break;
2008
+ }
2009
+ if (e.button === 0) {
2010
+ cb_event.apply = true;
2011
+ }
2012
+ else {
2013
+ cb_event.apply = false;
2014
+ }
2015
+ this.stopDrag(cb_event);
2016
+ break;
2017
+ }
2018
+ }
2019
+ }
2020
+
2021
+ /*!
2022
+ * Wunderbaum - ext-grid
2023
+ * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
2024
+ * v0.0.4, Tue, 03 May 2022 06:21:31 GMT (https://github.com/mar10/wunderbaum)
2025
+ */
2026
+ class GridExtension extends WunderbaumExtension {
2027
+ constructor(tree) {
2028
+ super(tree, "grid", {
2029
+ // throttle: 200,
2030
+ });
2031
+ this.observer = new DragObserver({
2032
+ root: window.document,
2033
+ selector: "span.wb-col-resizer",
2034
+ thresh: 4,
2035
+ // throttle: 400,
2036
+ dragstart: (e) => {
2037
+ return this.tree.element.contains(e.dragElem);
2038
+ },
2039
+ drag: (e) => {
2040
+ // TODO: throttle
2041
+ return this.handleDrag(e);
2042
+ },
2043
+ dragstop: (e) => {
2044
+ return this.handleDrag(e);
2045
+ },
2046
+ });
2047
+ }
2048
+ init() {
2049
+ super.init();
2050
+ }
2051
+ handleDrag(e) {
2052
+ const info = Wunderbaum.getEventInfo(e.event);
2053
+ // this.tree.options.
2054
+ this.tree.log(`${e.type}(${e.dx})`, e, info);
2055
+ }
2056
+ }
2057
+
1846
2058
  /*!
1847
2059
  * Wunderbaum - deferred
1848
2060
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1849
- * v0.0.1-0, Thu, 31 Mar 2022 15:13:20 GMT (https://github.com/mar10/wunderbaum)
2061
+ * v0.0.4, Tue, 03 May 2022 06:21:31 GMT (https://github.com/mar10/wunderbaum)
1850
2062
  */
1851
2063
  /**
1852
- * Deferred is a ES6 Promise, that exposes the resolve() and reject()` method.
2064
+ * Implement a ES6 Promise, that exposes a resolve() and reject() method.
1853
2065
  *
1854
- * Loosely mimics [`jQuery.Deferred`](https://api.jquery.com/category/deferred-object/).
2066
+ * Loosely mimics {@link https://api.jquery.com/category/deferred-object/ | jQuery.Deferred}.
2067
+ * Example:
2068
+ * ```js
2069
+ * function foo() {
2070
+ * let dfd = new Deferred(),
2071
+ * ...
2072
+ * dfd.resolve('foo')
2073
+ * ...
2074
+ * return dfd.promise();
2075
+ * }
2076
+ * ```
1855
2077
  */
1856
2078
  class Deferred {
1857
2079
  constructor() {
@@ -1889,7 +2111,7 @@ class Deferred {
1889
2111
  /*!
1890
2112
  * Wunderbaum - wunderbaum_node
1891
2113
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1892
- * v0.0.1-0, Thu, 31 Mar 2022 15:13:20 GMT (https://github.com/mar10/wunderbaum)
2114
+ * v0.0.4, Tue, 03 May 2022 06:21:31 GMT (https://github.com/mar10/wunderbaum)
1893
2115
  */
1894
2116
  /** Top-level properties that can be passed with `data`. */
1895
2117
  const NODE_PROPS = new Set([
@@ -1926,15 +2148,31 @@ const NODE_ATTRS = new Set([
1926
2148
  "unselectableIgnore",
1927
2149
  "unselectableStatus",
1928
2150
  ]);
2151
+ /**
2152
+ * A single tree node.
2153
+ *
2154
+ * **NOTE:** <br>
2155
+ * Generally you should not modify properties directly, since this may break
2156
+ * the internal bookkeeping.
2157
+ */
1929
2158
  class WunderbaumNode {
1930
2159
  constructor(tree, parent, data) {
1931
2160
  var _a, _b;
2161
+ /** Reference key. Unlike {@link key}, a `refKey` may occur multiple
2162
+ * times within a tree (in this case we have 'clone nodes').
2163
+ * @see Use {@link setKey} to modify.
2164
+ */
1932
2165
  this.refKey = undefined;
1933
2166
  this.children = null;
1934
2167
  this.lazy = false;
2168
+ /** Expansion state.
2169
+ * @see {@link isExpandable}, {@link isExpanded}, {@link setExpanded}. */
1935
2170
  this.expanded = false;
2171
+ /** Selection state.
2172
+ * @see {@link isSelected}, {@link setSelected}. */
1936
2173
  this.selected = false;
1937
- /** Additional classes added to `div.wb-row`. */
2174
+ /** Additional classes added to `div.wb-row`.
2175
+ * @see {@link addClass}, {@link removeClass}, {@link toggleClass}. */
1938
2176
  this.extraClasses = new Set();
1939
2177
  /** Custom data that was passed to the constructor */
1940
2178
  this.data = {};
@@ -2007,8 +2245,8 @@ class WunderbaumNode {
2007
2245
  * node._callEvent("edit.beforeEdit", {foo: 42})
2008
2246
  * ```
2009
2247
  */
2010
- _callEvent(name, extra) {
2011
- return this.tree._callEvent(name, extend({
2248
+ _callEvent(type, extra) {
2249
+ return this.tree._callEvent(type, extend({
2012
2250
  node: this,
2013
2251
  typeInfo: this.type ? this.tree.types[this.type] : {},
2014
2252
  }, extra));
@@ -2061,7 +2299,7 @@ class WunderbaumNode {
2061
2299
  // this.fixSelection3FromEndNodes();
2062
2300
  // }
2063
2301
  // this.triggerModifyChild("add", nodeList.length === 1 ? nodeList[0] : null);
2064
- this.tree.setModified(ChangeType.structure, this);
2302
+ this.tree.setModified(ChangeType.structure);
2065
2303
  return nodeList[0];
2066
2304
  }
2067
2305
  finally {
@@ -2100,7 +2338,8 @@ class WunderbaumNode {
2100
2338
  }
2101
2339
  /**
2102
2340
  * Apply a modification (or navigation) operation.
2103
- * @see Wunderbaum#applyCommand
2341
+ *
2342
+ * @see {@link Wunderbaum.applyCommand}
2104
2343
  */
2105
2344
  applyCommand(cmd, opts) {
2106
2345
  return this.tree.applyCommand(cmd, this, opts);
@@ -2193,8 +2432,7 @@ class WunderbaumNode {
2193
2432
  }
2194
2433
  /** Find a node relative to self.
2195
2434
  *
2196
- * @param where The keyCode that would normally trigger this move,
2197
- * or a keyword ('down', 'first', 'last', 'left', 'parent', 'right', 'up').
2435
+ * @see {@link Wunderbaum.findRelatedNode|tree.findRelatedNode()}
2198
2436
  */
2199
2437
  findRelatedNode(where, includeHidden = false) {
2200
2438
  return this.tree.findRelatedNode(this, where, includeHidden);
@@ -2495,7 +2733,7 @@ class WunderbaumNode {
2495
2733
  assert(!this.parent);
2496
2734
  tree.columns = data.columns;
2497
2735
  delete data.columns;
2498
- tree.renderHeader();
2736
+ tree.updateColumns({ calculateCols: false });
2499
2737
  }
2500
2738
  this._loadSourceObject(data);
2501
2739
  }
@@ -2531,19 +2769,20 @@ class WunderbaumNode {
2531
2769
  this.setStatus(NodeStatusType.ok);
2532
2770
  return;
2533
2771
  }
2534
- assert(isArray(source) || (source && source.url), "The lazyLoad event must return a node list, `{url: ...}` or false.");
2772
+ assert(isArray(source) || (source && source.url), "The lazyLoad event must return a node list, `{url: ...}`, or false.");
2535
2773
  await this.load(source); // also calls setStatus('ok')
2536
2774
  if (wasExpanded) {
2537
2775
  this.expanded = true;
2538
- this.tree.updateViewport();
2776
+ this.tree.setModified(ChangeType.structure);
2539
2777
  }
2540
2778
  else {
2541
- this.render(); // Fix expander icon to 'loaded'
2779
+ this.setModified(); // Fix expander icon to 'loaded'
2542
2780
  }
2543
2781
  }
2544
2782
  catch (e) {
2783
+ this.logError("Error during loadLazy()", e);
2784
+ this._callEvent("error", { error: e });
2545
2785
  this.setStatus(NodeStatusType.error, "" + e);
2546
- // } finally {
2547
2786
  }
2548
2787
  return;
2549
2788
  }
@@ -2691,7 +2930,7 @@ class WunderbaumNode {
2691
2930
  n.tree = targetNode.tree;
2692
2931
  }, true);
2693
2932
  }
2694
- tree.updateViewport();
2933
+ tree.setModified(ChangeType.structure);
2695
2934
  // TODO: fix selection state
2696
2935
  // TODO: fix active state
2697
2936
  }
@@ -2711,31 +2950,33 @@ class WunderbaumNode {
2711
2950
  // Allow to pass 'ArrowLeft' instead of 'left'
2712
2951
  where = KEY_TO_ACTION_DICT[where] || where;
2713
2952
  // Otherwise activate or focus the related node
2714
- let node = this.findRelatedNode(where);
2715
- if (node) {
2716
- // setFocus/setActive will scroll later (if autoScroll is specified)
2717
- try {
2718
- node.makeVisible({ scrollIntoView: false });
2719
- }
2720
- catch (e) { } // #272
2721
- node.setFocus();
2722
- if ((options === null || options === void 0 ? void 0 : options.activate) === false) {
2723
- return Promise.resolve(this);
2724
- }
2725
- return node.setActive(true, { event: options === null || options === void 0 ? void 0 : options.event });
2953
+ const node = this.findRelatedNode(where);
2954
+ if (!node) {
2955
+ this.logWarn(`Could not find related node '${where}'.`);
2956
+ return Promise.resolve(this);
2957
+ }
2958
+ // setFocus/setActive will scroll later (if autoScroll is specified)
2959
+ try {
2960
+ node.makeVisible({ scrollIntoView: false });
2726
2961
  }
2727
- this.logWarn("Could not find related node '" + where + "'.");
2728
- return Promise.resolve(this);
2962
+ catch (e) { } // #272
2963
+ node.setFocus();
2964
+ if ((options === null || options === void 0 ? void 0 : options.activate) === false) {
2965
+ return Promise.resolve(this);
2966
+ }
2967
+ return node.setActive(true, { event: options === null || options === void 0 ? void 0 : options.event });
2729
2968
  }
2730
2969
  /** Delete this node and all descendants. */
2731
2970
  remove() {
2732
2971
  const tree = this.tree;
2733
2972
  const pos = this.parent.children.indexOf(this);
2973
+ this.triggerModify("remove");
2734
2974
  this.parent.children.splice(pos, 1);
2735
2975
  this.visit((n) => {
2736
2976
  n.removeMarkup();
2737
2977
  tree._unregisterNode(n);
2738
2978
  }, true);
2979
+ tree.setModified(ChangeType.structure);
2739
2980
  }
2740
2981
  /** Remove all descendants of this node. */
2741
2982
  removeChildren() {
@@ -2767,7 +3008,7 @@ class WunderbaumNode {
2767
3008
  if (!this.isRootNode()) {
2768
3009
  this.expanded = false;
2769
3010
  }
2770
- this.tree.updateViewport();
3011
+ this.tree.setModified(ChangeType.structure);
2771
3012
  }
2772
3013
  /** Remove all HTML markup from the DOM. */
2773
3014
  removeMarkup() {
@@ -2842,13 +3083,15 @@ class WunderbaumNode {
2842
3083
  // this.log("_createIcon: ", iconSpan);
2843
3084
  return iconSpan;
2844
3085
  }
2845
- /** Create HTML markup for this node, i.e. the whole row. */
2846
- render(opts) {
3086
+ /**
3087
+ * Create a whole new `<div class="wb-row">` element.
3088
+ * @see {@link Wunderbaumode.render}
3089
+ */
3090
+ _render_markup(opts) {
2847
3091
  const tree = this.tree;
2848
3092
  const treeOptions = tree.options;
2849
3093
  const checkbox = this.getOption("checkbox") !== false;
2850
3094
  const columns = tree.columns;
2851
- const typeInfo = this.type ? tree.types[this.type] : null;
2852
3095
  const level = this.getLevel();
2853
3096
  let elem;
2854
3097
  let nodeElem;
@@ -2858,10 +3101,171 @@ class WunderbaumNode {
2858
3101
  let iconSpan;
2859
3102
  let expanderSpan = null;
2860
3103
  const activeColIdx = tree.navMode === NavigationMode.row ? null : tree.activeColIdx;
2861
- // let colElems: HTMLElement[];
2862
3104
  const isNew = !rowDiv;
3105
+ assert(isNew);
3106
+ assert(!isNew || (opts && opts.after), "opts.after expected, unless updating");
2863
3107
  assert(!this.isRootNode());
2864
- //
3108
+ rowDiv = document.createElement("div");
3109
+ rowDiv.classList.add("wb-row");
3110
+ rowDiv.style.top = this._rowIdx * ROW_HEIGHT + "px";
3111
+ this._rowElem = rowDiv;
3112
+ // Attach a node reference to the DOM Element:
3113
+ rowDiv._wb_node = this;
3114
+ nodeElem = document.createElement("span");
3115
+ nodeElem.classList.add("wb-node", "wb-col");
3116
+ rowDiv.appendChild(nodeElem);
3117
+ let ofsTitlePx = 0;
3118
+ if (checkbox) {
3119
+ checkboxSpan = document.createElement("i");
3120
+ checkboxSpan.classList.add("wb-checkbox");
3121
+ nodeElem.appendChild(checkboxSpan);
3122
+ ofsTitlePx += ICON_WIDTH;
3123
+ }
3124
+ for (let i = level - 1; i > 0; i--) {
3125
+ elem = document.createElement("i");
3126
+ elem.classList.add("wb-indent");
3127
+ nodeElem.appendChild(elem);
3128
+ ofsTitlePx += ICON_WIDTH;
3129
+ }
3130
+ if (!treeOptions.minExpandLevel || level > treeOptions.minExpandLevel) {
3131
+ expanderSpan = document.createElement("i");
3132
+ expanderSpan.classList.add("wb-expander");
3133
+ nodeElem.appendChild(expanderSpan);
3134
+ ofsTitlePx += ICON_WIDTH;
3135
+ }
3136
+ iconSpan = this._createIcon(nodeElem);
3137
+ if (iconSpan) {
3138
+ ofsTitlePx += ICON_WIDTH;
3139
+ }
3140
+ titleSpan = document.createElement("span");
3141
+ titleSpan.classList.add("wb-title");
3142
+ nodeElem.appendChild(titleSpan);
3143
+ this._callEvent("enhanceTitle", { titleSpan: titleSpan });
3144
+ // Store the width of leading icons with the node, so we can calculate
3145
+ // the width of the embedded title span later
3146
+ nodeElem._ofsTitlePx = ofsTitlePx;
3147
+ // Support HTML5 drag-n-drop
3148
+ if (tree.options.dnd.dragStart) {
3149
+ nodeElem.draggable = true;
3150
+ }
3151
+ // Render columns
3152
+ if (!this.colspan && columns.length > 1) {
3153
+ let colIdx = 0;
3154
+ for (let col of columns) {
3155
+ colIdx++;
3156
+ let colElem;
3157
+ if (col.id === "*") {
3158
+ colElem = nodeElem;
3159
+ }
3160
+ else {
3161
+ colElem = document.createElement("span");
3162
+ colElem.classList.add("wb-col");
3163
+ rowDiv.appendChild(colElem);
3164
+ }
3165
+ if (colIdx === activeColIdx) {
3166
+ colElem.classList.add("wb-active");
3167
+ }
3168
+ // Add classes from `columns` definition to `<div.wb-col>` cells
3169
+ col.classes ? colElem.classList.add(...col.classes.split(" ")) : 0;
3170
+ colElem.style.left = col._ofsPx + "px";
3171
+ colElem.style.width = col._widthPx + "px";
3172
+ if (isNew && col.html) {
3173
+ if (typeof col.html === "string") {
3174
+ colElem.innerHTML = col.html;
3175
+ }
3176
+ }
3177
+ }
3178
+ }
3179
+ // Now go on and fill in data and update classes
3180
+ opts.isNew = true;
3181
+ this._render_data(opts);
3182
+ // Attach to DOM as late as possible
3183
+ const after = opts ? opts.after : "last";
3184
+ switch (after) {
3185
+ case "first":
3186
+ tree.nodeListElement.prepend(rowDiv);
3187
+ break;
3188
+ case "last":
3189
+ tree.nodeListElement.appendChild(rowDiv);
3190
+ break;
3191
+ default:
3192
+ opts.after.after(rowDiv);
3193
+ }
3194
+ }
3195
+ /**
3196
+ * Render `node.title`, `.icon` into an existing row.
3197
+ *
3198
+ * @see {@link Wunderbaumode.render}
3199
+ */
3200
+ _render_data(opts) {
3201
+ assert(this._rowElem);
3202
+ const tree = this.tree;
3203
+ const treeOptions = tree.options;
3204
+ const rowDiv = this._rowElem;
3205
+ const isNew = !!opts.isNew; // Called by _render_markup()?
3206
+ const columns = tree.columns;
3207
+ const typeInfo = this.type ? tree.types[this.type] : null;
3208
+ // Row markup already exists
3209
+ const nodeElem = rowDiv.querySelector("span.wb-node");
3210
+ const titleSpan = nodeElem.querySelector("span.wb-title");
3211
+ if (this.titleWithHighlight) {
3212
+ titleSpan.innerHTML = this.titleWithHighlight;
3213
+ }
3214
+ else {
3215
+ titleSpan.textContent = this.title;
3216
+ }
3217
+ // Set the width of the title span, so overflow ellipsis work
3218
+ if (!treeOptions.skeleton) {
3219
+ if (this.colspan) {
3220
+ let vpWidth = tree.element.clientWidth;
3221
+ titleSpan.style.width =
3222
+ vpWidth - nodeElem._ofsTitlePx - ROW_EXTRA_PAD + "px";
3223
+ }
3224
+ else {
3225
+ titleSpan.style.width =
3226
+ columns[0]._widthPx -
3227
+ nodeElem._ofsTitlePx -
3228
+ ROW_EXTRA_PAD +
3229
+ "px";
3230
+ }
3231
+ }
3232
+ // Update row classes
3233
+ opts.isDataChange = true;
3234
+ this._render_status(opts);
3235
+ // Let user modify the result
3236
+ if (this.statusNodeType) {
3237
+ this._callEvent("renderStatusNode", {
3238
+ isNew: isNew,
3239
+ nodeElem: nodeElem,
3240
+ });
3241
+ }
3242
+ else if (this.parent) {
3243
+ // Skip root node
3244
+ this._callEvent("render", {
3245
+ isNew: isNew,
3246
+ isDataChange: true,
3247
+ nodeElem: nodeElem,
3248
+ typeInfo: typeInfo,
3249
+ colInfosById: this._getRenderInfo(),
3250
+ });
3251
+ }
3252
+ }
3253
+ /**
3254
+ * Update row classes to reflect active, focuses, etc.
3255
+ * @see {@link Wunderbaumode.render}
3256
+ */
3257
+ _render_status(opts) {
3258
+ // this.log("_render_status", opts);
3259
+ const tree = this.tree;
3260
+ const treeOptions = tree.options;
3261
+ const typeInfo = this.type ? tree.types[this.type] : null;
3262
+ const rowDiv = this._rowElem;
3263
+ // Row markup already exists
3264
+ const nodeElem = rowDiv.querySelector("span.wb-node");
3265
+ const expanderSpan = nodeElem.querySelector("i.wb-expander");
3266
+ const checkboxSpan = nodeElem.querySelector("i.wb-checkbox");
3267
+ // TODO: update icon (if not opts.isNew)
3268
+ // const iconSpan = nodeElem.querySelector("i.wb-icon") as HTMLElement;
2865
3269
  let rowClasses = ["wb-row"];
2866
3270
  this.expanded ? rowClasses.push("wb-expanded") : 0;
2867
3271
  this.lazy ? rowClasses.push("wb-lazy") : 0;
@@ -2876,102 +3280,14 @@ class WunderbaumNode {
2876
3280
  this.match ? rowClasses.push("wb-match") : 0;
2877
3281
  this.subMatchCount ? rowClasses.push("wb-submatch") : 0;
2878
3282
  treeOptions.skeleton ? rowClasses.push("wb-skeleton") : 0;
2879
- // TODO: no need to hide!
2880
- // !(this.match || this.subMatchCount) ? rowClasses.push("wb-hide") : 0;
2881
- if (rowDiv) {
2882
- // Row markup already exists
2883
- nodeElem = rowDiv.querySelector("span.wb-node");
2884
- titleSpan = nodeElem.querySelector("span.wb-title");
2885
- expanderSpan = nodeElem.querySelector("i.wb-expander");
2886
- checkboxSpan = nodeElem.querySelector("i.wb-checkbox");
2887
- iconSpan = nodeElem.querySelector("i.wb-icon");
2888
- // TODO: we need this, when icons should be replacable
2889
- // iconSpan = this._createIcon(nodeElem, iconSpan);
2890
- // colElems = (<unknown>(
2891
- // rowDiv.querySelectorAll("span.wb-col")
2892
- // )) as HTMLElement[];
2893
- }
2894
- else {
2895
- rowDiv = document.createElement("div");
2896
- // rowDiv.classList.add("wb-row");
2897
- // Attach a node reference to the DOM Element:
2898
- rowDiv._wb_node = this;
2899
- nodeElem = document.createElement("span");
2900
- nodeElem.classList.add("wb-node", "wb-col");
2901
- rowDiv.appendChild(nodeElem);
2902
- let ofsTitlePx = 0;
2903
- if (checkbox) {
2904
- checkboxSpan = document.createElement("i");
2905
- nodeElem.appendChild(checkboxSpan);
2906
- ofsTitlePx += ICON_WIDTH;
2907
- }
2908
- for (let i = level - 1; i > 0; i--) {
2909
- elem = document.createElement("i");
2910
- elem.classList.add("wb-indent");
2911
- nodeElem.appendChild(elem);
2912
- ofsTitlePx += ICON_WIDTH;
2913
- }
2914
- if (level > treeOptions.minExpandLevel) {
2915
- expanderSpan = document.createElement("i");
2916
- nodeElem.appendChild(expanderSpan);
2917
- ofsTitlePx += ICON_WIDTH;
2918
- }
2919
- iconSpan = this._createIcon(nodeElem);
2920
- if (iconSpan) {
2921
- ofsTitlePx += ICON_WIDTH;
2922
- }
2923
- titleSpan = document.createElement("span");
2924
- titleSpan.classList.add("wb-title");
2925
- nodeElem.appendChild(titleSpan);
2926
- this._callEvent("enhanceTitle", { titleSpan: titleSpan });
2927
- // Store the width of leading icons with the node, so we can calculate
2928
- // the width of the embedded title span later
2929
- nodeElem._ofsTitlePx = ofsTitlePx;
2930
- if (tree.options.dnd.dragStart) {
2931
- nodeElem.draggable = true;
2932
- }
2933
- // Render columns
2934
- // colElems = [];
2935
- if (!this.colspan && columns.length > 1) {
2936
- let colIdx = 0;
2937
- for (let col of columns) {
2938
- colIdx++;
2939
- let colElem;
2940
- if (col.id === "*") {
2941
- colElem = nodeElem;
2942
- }
2943
- else {
2944
- colElem = document.createElement("span");
2945
- colElem.classList.add("wb-col");
2946
- // colElem.textContent = "" + col.id;
2947
- rowDiv.appendChild(colElem);
2948
- }
2949
- if (colIdx === activeColIdx) {
2950
- colElem.classList.add("wb-active");
2951
- }
2952
- // Add classes from `columns` definition to `<div.wb-col>` cells
2953
- col.classes ? colElem.classList.add(...col.classes.split(" ")) : 0;
2954
- colElem.style.left = col._ofsPx + "px";
2955
- colElem.style.width = col._widthPx + "px";
2956
- // colElems.push(colElem);
2957
- if (isNew && col.html) {
2958
- if (typeof col.html === "string") {
2959
- colElem.innerHTML = col.html;
2960
- }
2961
- }
2962
- }
2963
- }
2964
- }
2965
- // --- From here common code starts (either new or existing markup):
2966
- rowDiv.className = rowClasses.join(" "); // Reset prev. classes
3283
+ // Replace previous classes:
3284
+ rowDiv.className = rowClasses.join(" ");
2967
3285
  // Add classes from `node.extraClasses`
2968
3286
  rowDiv.classList.add(...this.extraClasses);
2969
3287
  // Add classes from `tree.types[node.type]`
2970
3288
  if (typeInfo && typeInfo.classes) {
2971
3289
  rowDiv.classList.add(...typeInfo.classes);
2972
3290
  }
2973
- // rowDiv.style.top = (this._rowIdx! * 1.1) + "em";
2974
- rowDiv.style.top = this._rowIdx * ROW_HEIGHT + "px";
2975
3291
  if (expanderSpan) {
2976
3292
  if (this.isExpandable(false)) {
2977
3293
  if (this.expanded) {
@@ -2999,50 +3315,42 @@ class WunderbaumNode {
2999
3315
  checkboxSpan.className = "wb-checkbox " + iconMap.checkUnchecked;
3000
3316
  }
3001
3317
  }
3002
- if (this.titleWithHighlight) {
3003
- titleSpan.innerHTML = this.titleWithHighlight;
3004
- }
3005
- else if (tree.options.escapeTitles) {
3006
- titleSpan.textContent = this.title;
3318
+ // Fix active cell in cell-nav mode
3319
+ if (!opts.isNew) {
3320
+ let i = 0;
3321
+ for (let colSpan of rowDiv.children) {
3322
+ colSpan.classList.toggle("wb-active", i++ === tree.activeColIdx);
3323
+ }
3007
3324
  }
3008
- else {
3009
- titleSpan.innerHTML = this.title;
3325
+ }
3326
+ /**
3327
+ * Create or update node's markup.
3328
+ *
3329
+ * `options.change` defaults to ChangeType.data, which updates the title,
3330
+ * icon, and status. It also triggers the `render` event, that lets the user
3331
+ * create or update the content of embeded cell elements.<br>
3332
+ *
3333
+ * If only the status or other class-only modifications have changed,
3334
+ * `options.change` should be set to ChangeType.status instead for best
3335
+ * efficiency.
3336
+ */
3337
+ render(options) {
3338
+ // this.log("render", options);
3339
+ const opts = Object.assign({ change: ChangeType.data }, options);
3340
+ if (!this._rowElem) {
3341
+ opts.change = "row";
3342
+ }
3343
+ switch (opts.change) {
3344
+ case "status":
3345
+ this._render_status(opts);
3346
+ break;
3347
+ case "data":
3348
+ this._render_data(opts);
3349
+ break;
3350
+ default:
3351
+ this._render_markup(opts);
3352
+ break;
3010
3353
  }
3011
- // Set the width of the title span, so overflow ellipsis work
3012
- if (!treeOptions.skeleton) {
3013
- if (this.colspan) {
3014
- let vpWidth = tree.element.clientWidth;
3015
- titleSpan.style.width =
3016
- vpWidth - nodeElem._ofsTitlePx - ROW_EXTRA_PAD + "px";
3017
- }
3018
- else {
3019
- titleSpan.style.width =
3020
- columns[0]._widthPx -
3021
- nodeElem._ofsTitlePx -
3022
- ROW_EXTRA_PAD +
3023
- "px";
3024
- }
3025
- }
3026
- this._rowElem = rowDiv;
3027
- if (this.statusNodeType) {
3028
- this._callEvent("renderStatusNode", {
3029
- isNew: isNew,
3030
- nodeElem: nodeElem,
3031
- });
3032
- }
3033
- else if (this.parent) {
3034
- // Skip root node
3035
- this._callEvent("render", {
3036
- isNew: isNew,
3037
- nodeElem: nodeElem,
3038
- typeInfo: typeInfo,
3039
- colInfosById: this._getRenderInfo(),
3040
- });
3041
- }
3042
- // Attach to DOM as late as possible
3043
- // if (!this._rowElem) {
3044
- tree.nodeListElement.appendChild(rowDiv);
3045
- // }
3046
3354
  }
3047
3355
  /**
3048
3356
  * Remove all children, collapse, and set the lazy-flag, so that the lazyLoad
@@ -3053,7 +3361,7 @@ class WunderbaumNode {
3053
3361
  this.expanded = false;
3054
3362
  this.lazy = true;
3055
3363
  this.children = null;
3056
- this.tree.updateViewport();
3364
+ this.tree.setModified(ChangeType.structure);
3057
3365
  }
3058
3366
  /** Convert node (or whole branch) into a plain object.
3059
3367
  *
@@ -3116,14 +3424,15 @@ class WunderbaumNode {
3116
3424
  *
3117
3425
  * Evaluation sequence:
3118
3426
  *
3119
- * If `tree.options.<name>` is a callback that returns something, use that.
3120
- * Else if `node.<name>` is defined, use that.
3121
- * Else if `tree.types[<node.type>]` is a value, use that.
3122
- * Else if `tree.options.<name>` is a value, use that.
3123
- * Else use `defaultValue`.
3427
+ * - If `tree.options.<name>` is a callback that returns something, use that.
3428
+ * - Else if `node.<name>` is defined, use that.
3429
+ * - Else if `tree.types[<node.type>]` is a value, use that.
3430
+ * - Else if `tree.options.<name>` is a value, use that.
3431
+ * - Else use `defaultValue`.
3124
3432
  *
3125
3433
  * @param name name of the option property (on node and tree)
3126
3434
  * @param defaultValue return this if nothing else matched
3435
+ * {@link Wunderbaum.getOption|Wunderbaum.getOption()}
3127
3436
  */
3128
3437
  getOption(name, defaultValue) {
3129
3438
  let tree = this.tree;
@@ -3158,15 +3467,21 @@ class WunderbaumNode {
3158
3467
  // Use value from value options dict, fallback do default
3159
3468
  return value !== null && value !== void 0 ? value : defaultValue;
3160
3469
  }
3470
+ /** Make sure that this node is visible in the viewport.
3471
+ * @see {@link Wunderbaum.scrollTo|Wunderbaum.scrollTo()}
3472
+ */
3161
3473
  async scrollIntoView(options) {
3162
3474
  return this.tree.scrollTo(this);
3163
3475
  }
3476
+ /**
3477
+ * Activate this node, deactivate previous, send events, activate column and scroll int viewport.
3478
+ */
3164
3479
  async setActive(flag = true, options) {
3165
3480
  const tree = this.tree;
3166
3481
  const prev = tree.activeNode;
3167
3482
  const retrigger = options === null || options === void 0 ? void 0 : options.retrigger;
3168
- const noEvent = options === null || options === void 0 ? void 0 : options.noEvent;
3169
- if (!noEvent) {
3483
+ const noEvents = options === null || options === void 0 ? void 0 : options.noEvents;
3484
+ if (!noEvents) {
3170
3485
  let orgEvent = options === null || options === void 0 ? void 0 : options.event;
3171
3486
  if (flag) {
3172
3487
  if (prev !== this || retrigger) {
@@ -3181,7 +3496,7 @@ class WunderbaumNode {
3181
3496
  orgEvent: orgEvent,
3182
3497
  }) === false) {
3183
3498
  tree.activeNode = null;
3184
- prev === null || prev === void 0 ? void 0 : prev.setDirty(ChangeType.status);
3499
+ prev === null || prev === void 0 ? void 0 : prev.setModified();
3185
3500
  return;
3186
3501
  }
3187
3502
  }
@@ -3192,8 +3507,8 @@ class WunderbaumNode {
3192
3507
  }
3193
3508
  if (prev !== this) {
3194
3509
  tree.activeNode = this;
3195
- prev === null || prev === void 0 ? void 0 : prev.setDirty(ChangeType.status);
3196
- this.setDirty(ChangeType.status);
3510
+ prev === null || prev === void 0 ? void 0 : prev.setModified(ChangeType.status);
3511
+ this.setModified(ChangeType.status);
3197
3512
  }
3198
3513
  if (options &&
3199
3514
  options.colIdx != null &&
@@ -3204,57 +3519,68 @@ class WunderbaumNode {
3204
3519
  // requestAnimationFrame(() => {
3205
3520
  // this.scrollIntoView();
3206
3521
  // })
3207
- this.scrollIntoView();
3208
- }
3209
- setDirty(type) {
3210
- if (this.tree._disableUpdate) {
3211
- return;
3212
- }
3213
- if (type === ChangeType.structure) {
3214
- this.tree.updateViewport();
3215
- }
3216
- else if (this._rowElem) {
3217
- // otherwise not in viewport, so no need to render
3218
- this.render();
3219
- }
3522
+ return this.scrollIntoView();
3220
3523
  }
3524
+ /**
3525
+ * Expand or collapse this node.
3526
+ */
3221
3527
  async setExpanded(flag = true, options) {
3222
3528
  // alert("" + this.getLevel() + ", "+ this.getOption("minExpandLevel");
3223
3529
  if (!flag &&
3224
3530
  this.isExpanded() &&
3225
3531
  this.getLevel() < this.getOption("minExpandLevel") &&
3226
3532
  !getOption(options, "force")) {
3227
- this.logDebug("Ignored collapse request.");
3533
+ this.logDebug("Ignored collapse request below expandLevel.");
3228
3534
  return;
3229
3535
  }
3230
3536
  if (flag && this.lazy && this.children == null) {
3231
3537
  await this.loadLazy();
3232
3538
  }
3233
3539
  this.expanded = flag;
3234
- this.setDirty(ChangeType.structure);
3235
- }
3236
- setIcon() {
3237
- throw new Error("Not yet implemented");
3238
- // this.setDirty(ChangeType.status);
3540
+ this.tree.setModified(ChangeType.structure);
3239
3541
  }
3542
+ /**
3543
+ * Set keyboard focus here.
3544
+ * @see {@link setActive}
3545
+ */
3240
3546
  setFocus(flag = true, options) {
3241
3547
  const prev = this.tree.focusNode;
3242
3548
  this.tree.focusNode = this;
3243
- prev === null || prev === void 0 ? void 0 : prev.setDirty(ChangeType.status);
3244
- this.setDirty(ChangeType.status);
3549
+ prev === null || prev === void 0 ? void 0 : prev.setModified();
3550
+ this.setModified();
3245
3551
  }
3552
+ /** Set a new icon path or class. */
3553
+ setIcon() {
3554
+ throw new Error("Not yet implemented");
3555
+ // this.setModified();
3556
+ }
3557
+ /** Change node's {@link key} and/or {@link refKey}. */
3558
+ setKey(key, refKey) {
3559
+ throw new Error("Not yet implemented");
3560
+ }
3561
+ /**
3562
+ * Schedule a render, typically called to update after a status or data change.
3563
+ *
3564
+ * `change` defaults to 'data', which handles modifcations of title, icon,
3565
+ * and column content. It can be reduced to 'ChangeType.status' if only
3566
+ * active/focus/selected state has changed.
3567
+ */
3568
+ setModified(change = ChangeType.data) {
3569
+ assert(change === ChangeType.status || change === ChangeType.data);
3570
+ this.tree.setModified(change, this);
3571
+ }
3572
+ /** Modify the check/uncheck state. */
3246
3573
  setSelected(flag = true, options) {
3247
3574
  const prev = this.selected;
3248
3575
  if (!!flag !== prev) {
3249
3576
  this._callEvent("select", { flag: flag });
3250
3577
  }
3251
3578
  this.selected = !!flag;
3252
- this.setDirty(ChangeType.status);
3579
+ this.setModified();
3253
3580
  }
3254
- /** Show node status (ok, loading, error, noData) using styles and a dummy child node.
3255
- */
3581
+ /** Display node status (ok, loading, error, noData) using styles and a dummy child node. */
3256
3582
  setStatus(status, message, details) {
3257
- let tree = this.tree;
3583
+ const tree = this.tree;
3258
3584
  let statusNode = null;
3259
3585
  const _clearStatusNode = () => {
3260
3586
  // Remove dedicated dummy node, if any
@@ -3325,12 +3651,13 @@ class WunderbaumNode {
3325
3651
  default:
3326
3652
  error("invalid node status " + status);
3327
3653
  }
3328
- tree.updateViewport();
3654
+ tree.setModified(ChangeType.structure);
3329
3655
  return statusNode;
3330
3656
  }
3657
+ /** Rename this node. */
3331
3658
  setTitle(title) {
3332
3659
  this.title = title;
3333
- this.setDirty(ChangeType.status);
3660
+ this.setModified();
3334
3661
  // this.triggerModify("rename"); // TODO
3335
3662
  }
3336
3663
  /**
@@ -3351,10 +3678,16 @@ class WunderbaumNode {
3351
3678
  * @param {object} [extra]
3352
3679
  */
3353
3680
  triggerModify(operation, extra) {
3681
+ if (!this.parent) {
3682
+ return;
3683
+ }
3354
3684
  this.parent.triggerModifyChild(operation, this, extra);
3355
3685
  }
3356
- /** Call fn(node) for all child nodes in hierarchical order (depth-first).<br>
3357
- * Stop iteration, if fn() returns false. Skip current branch, if fn() returns "skip".<br>
3686
+ /**
3687
+ * Call fn(node) for all child nodes in hierarchical order (depth-first).
3688
+ *
3689
+ * Stop iteration, if fn() returns false. Skip current branch, if fn()
3690
+ * returns "skip".<br>
3358
3691
  * Return false if iteration was stopped.
3359
3692
  *
3360
3693
  * @param {function} callback the callback function.
@@ -3398,7 +3731,8 @@ class WunderbaumNode {
3398
3731
  }
3399
3732
  return true;
3400
3733
  }
3401
- /** Call fn(node) for all sibling nodes.<br>
3734
+ /**
3735
+ * Call fn(node) for all sibling nodes.<br>
3402
3736
  * Stop iteration, if fn() returns false.<br>
3403
3737
  * Return false if iteration was stopped.
3404
3738
  *
@@ -3429,7 +3763,7 @@ WunderbaumNode.sequence = 0;
3429
3763
  /*!
3430
3764
  * Wunderbaum - ext-edit
3431
3765
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
3432
- * v0.0.1-0, Thu, 31 Mar 2022 15:13:20 GMT (https://github.com/mar10/wunderbaum)
3766
+ * v0.0.4, Tue, 03 May 2022 06:21:31 GMT (https://github.com/mar10/wunderbaum)
3433
3767
  */
3434
3768
  // const START_MARKER = "\uFFF7";
3435
3769
  class EditExtension extends WunderbaumExtension {
@@ -3547,7 +3881,7 @@ class EditExtension extends WunderbaumExtension {
3547
3881
  break;
3548
3882
  case "F2":
3549
3883
  if (trigger.indexOf("F2") >= 0) {
3550
- // tree.setCellMode(NavigationMode.cellEdit);
3884
+ // tree.setNavigationMode(NavigationMode.cellEdit);
3551
3885
  this.startEditTitle();
3552
3886
  return false;
3553
3887
  }
@@ -3588,6 +3922,7 @@ class EditExtension extends WunderbaumExtension {
3588
3922
  if (validity) {
3589
3923
  // Permanently apply input validations (CSS and tooltip)
3590
3924
  inputElem.addEventListener("keydown", (e) => {
3925
+ inputElem.setCustomValidity("");
3591
3926
  if (!inputElem.reportValidity()) ;
3592
3927
  });
3593
3928
  }
@@ -3628,6 +3963,11 @@ class EditExtension extends WunderbaumExtension {
3628
3963
  }
3629
3964
  node.logDebug(`stopEditTitle(${apply})`, opts, focusElem, newValue);
3630
3965
  if (apply && newValue !== null && newValue !== node.title) {
3966
+ const errMsg = focusElem.validationMessage;
3967
+ if (errMsg) {
3968
+ // input element's native validation failed
3969
+ throw new Error(`Input validation failed for "${newValue}": ${errMsg}.`);
3970
+ }
3631
3971
  const colElem = node.getColElem(0);
3632
3972
  this._applyChange("edit.apply", node, colElem, {
3633
3973
  oldValue: node.title,
@@ -3714,12 +4054,12 @@ class EditExtension extends WunderbaumExtension {
3714
4054
  * Copyright (c) 2021-2022, Martin Wendt (https://wwWendt.de).
3715
4055
  * Released under the MIT license.
3716
4056
  *
3717
- * @version v0.0.1-0
3718
- * @date Thu, 31 Mar 2022 15:13:20 GMT
4057
+ * @version v0.0.4
4058
+ * @date Tue, 03 May 2022 06:21:31 GMT
3719
4059
  */
3720
4060
  // const class_prefix = "wb-";
3721
4061
  // const node_props: string[] = ["title", "key", "refKey"];
3722
- const MAX_CHANGED_NODES = 10;
4062
+ // const MAX_CHANGED_NODES = 10;
3723
4063
  /**
3724
4064
  * A persistent plain object or array.
3725
4065
  *
@@ -3731,36 +4071,43 @@ class Wunderbaum {
3731
4071
  this.extensions = {};
3732
4072
  this.keyMap = new Map();
3733
4073
  this.refKeyMap = new Map();
3734
- this.viewNodes = new Set();
3735
- // protected rows: WunderbaumNode[] = [];
3736
- // protected _rowCount = 0;
4074
+ // protected viewNodes = new Set<WunderbaumNode>();
4075
+ this.treeRowCount = 0;
4076
+ this._disableUpdateCount = 0;
3737
4077
  // protected eventHandlers : Array<function> = [];
4078
+ /** Currently active node if any. */
3738
4079
  this.activeNode = null;
4080
+ /** Current node hat has keyboard focus if any. */
3739
4081
  this.focusNode = null;
3740
- this._disableUpdate = 0;
3741
- this._disableUpdateCount = 0;
3742
4082
  /** Shared properties, referenced by `node.type`. */
3743
4083
  this.types = {};
3744
4084
  /** List of column definitions. */
3745
4085
  this.columns = [];
3746
4086
  this._columnsById = {};
3747
4087
  // Modification Status
3748
- this.changedSince = 0;
3749
- this.changes = new Set();
3750
- this.changedNodes = new Set();
4088
+ // protected changedSince = 0;
4089
+ // protected changes = new Set<ChangeType>();
4090
+ // protected changedNodes = new Set<WunderbaumNode>();
4091
+ this.changeRedrawRequestPending = false;
4092
+ /** Expose some useful methods of the util.ts module as `tree._util`. */
4093
+ this._util = util;
3751
4094
  // --- FILTER ---
3752
4095
  this.filterMode = null;
3753
4096
  // --- KEYNAV ---
4097
+ /** @internal Use `setColumn()`/`getActiveColElem()`*/
3754
4098
  this.activeColIdx = 0;
4099
+ /** @internal */
3755
4100
  this.navMode = NavigationMode.row;
4101
+ /** @internal */
3756
4102
  this.lastQuicksearchTime = 0;
4103
+ /** @internal */
3757
4104
  this.lastQuicksearchTerm = "";
3758
4105
  // --- EDIT ---
3759
4106
  this.lastClickTime = 0;
3760
- // TODO: make accessible in compiled JS like this?
3761
- this._util = util;
3762
- /** Alias for `logDebug` */
3763
- this.log = this.logDebug; // Alias
4107
+ /** Alias for {@link Wunderbaum.logDebug}.
4108
+ * @alias Wunderbaum.logDebug
4109
+ */
4110
+ this.log = this.logDebug;
3764
4111
  let opts = (this.options = extend({
3765
4112
  id: null,
3766
4113
  source: null,
@@ -3771,7 +4118,7 @@ class Wunderbaum {
3771
4118
  rowHeightPx: ROW_HEIGHT,
3772
4119
  columns: null,
3773
4120
  types: null,
3774
- escapeTitles: true,
4121
+ // escapeTitles: true,
3775
4122
  showSpinner: false,
3776
4123
  checkbox: true,
3777
4124
  minExpandLevel: 0,
@@ -3828,6 +4175,7 @@ class Wunderbaum {
3828
4175
  this._registerExtension(new EditExtension(this));
3829
4176
  this._registerExtension(new FilterExtension(this));
3830
4177
  this._registerExtension(new DndExtension(this));
4178
+ this._registerExtension(new GridExtension(this));
3831
4179
  this._registerExtension(new LoggerExtension(this));
3832
4180
  // --- Evaluate options
3833
4181
  this.columns = opts.columns;
@@ -3851,9 +4199,7 @@ class Wunderbaum {
3851
4199
  opts.navigationMode === NavigationModeOption.startCell) {
3852
4200
  this.navMode = NavigationMode.cellNav;
3853
4201
  }
3854
- this._updateViewportThrottled = throttle(() => {
3855
- this._updateViewport();
3856
- }, opts.updateThrottleWait, { leading: true, trailing: true });
4202
+ this._updateViewportThrottled = addaptiveThrottle(this._updateViewport.bind(this), {});
3857
4203
  // --- Create Markup
3858
4204
  this.element = elemFromSelector(opts.element);
3859
4205
  assert(!!this.element, `Invalid 'element' option: ${opts.element}`);
@@ -3921,11 +4267,9 @@ class Wunderbaum {
3921
4267
  var _a;
3922
4268
  (_a = this.element.querySelector("progress.spinner")) === null || _a === void 0 ? void 0 : _a.remove();
3923
4269
  this.element.classList.remove("wb-initializing");
3924
- // this.updateViewport();
3925
4270
  });
3926
4271
  }
3927
4272
  else {
3928
- // this.updateViewport();
3929
4273
  readyDeferred.resolve();
3930
4274
  }
3931
4275
  // TODO: This is sometimes required, because this.element.clientWidth
@@ -3935,19 +4279,17 @@ class Wunderbaum {
3935
4279
  }, 50);
3936
4280
  // --- Bind listeners
3937
4281
  this.scrollContainer.addEventListener("scroll", (e) => {
3938
- this.updateViewport();
4282
+ this.setModified(ChangeType.vscroll);
3939
4283
  });
3940
- // window.addEventListener("resize", (e: Event) => {
3941
- // this.updateViewport();
3942
- // });
3943
4284
  this.resizeObserver = new ResizeObserver((entries) => {
3944
- this.updateViewport();
3945
- console.log("ResizeObserver: Size changed", entries);
4285
+ this.setModified(ChangeType.vscroll);
4286
+ // this.log("ResizeObserver: Size changed", entries);
3946
4287
  });
3947
4288
  this.resizeObserver.observe(this.element);
3948
4289
  onEvent(this.nodeListElement, "click", "div.wb-row", (e) => {
3949
4290
  const info = Wunderbaum.getEventInfo(e);
3950
4291
  const node = info.node;
4292
+ // this.log("click", info, e);
3951
4293
  if (this._callEvent("click", { event: e, node: node, info: info }) === false) {
3952
4294
  this.lastClickTime = Date.now();
3953
4295
  return false;
@@ -3975,8 +4317,6 @@ class Wunderbaum {
3975
4317
  node.setSelected(!node.isSelected());
3976
4318
  }
3977
4319
  }
3978
- // if(e.target.classList.)
3979
- // this.log("click", info);
3980
4320
  this.lastClickTime = Date.now();
3981
4321
  });
3982
4322
  onEvent(this.element, "keydown", (e) => {
@@ -3998,37 +4338,18 @@ class Wunderbaum {
3998
4338
  forceClose: true,
3999
4339
  });
4000
4340
  }
4001
- // if (flag && !this.activeNode ) {
4002
- // setTimeout(() => {
4003
- // if (!this.activeNode) {
4004
- // const firstNode = this.getFirstChild();
4005
- // if (firstNode && !firstNode?.isStatusNode()) {
4006
- // firstNode.logInfo("Activate on focus", e);
4007
- // firstNode.setActive(true, { event: e });
4008
- // }
4009
- // }
4010
- // }, 10);
4011
- // }
4012
4341
  });
4013
4342
  }
4014
- /** */
4015
- // _renderHeader(){
4016
- // const coldivs = "<span class='wb-col'></span>".repeat(this.columns.length);
4017
- // this.element.innerHTML = `
4018
- // <div class='wb-header'>
4019
- // <div class='wb-row'>
4020
- // ${coldivs}
4021
- // </div>
4022
- // </div>`;
4023
- // }
4024
- /** Return a Wunderbaum instance, from element, index, or event.
4343
+ /**
4344
+ * Return a Wunderbaum instance, from element, id, index, or event.
4025
4345
  *
4026
- * @example
4027
- * getTree(); // Get first Wunderbaum instance on page
4028
- * getTree(1); // Get second Wunderbaum instance on page
4029
- * getTree(event); // Get tree for this mouse- or keyboard event
4030
- * getTree("foo"); // Get tree for this `tree.options.id`
4346
+ * ```js
4347
+ * getTree(); // Get first Wunderbaum instance on page
4348
+ * getTree(1); // Get second Wunderbaum instance on page
4349
+ * getTree(event); // Get tree for this mouse- or keyboard event
4350
+ * getTree("foo"); // Get tree for this `tree.options.id`
4031
4351
  * getTree("#tree"); // Get tree for this matching element
4352
+ * ```
4032
4353
  */
4033
4354
  static getTree(el) {
4034
4355
  if (el instanceof Wunderbaum) {
@@ -4069,9 +4390,8 @@ class Wunderbaum {
4069
4390
  }
4070
4391
  return null;
4071
4392
  }
4072
- /** Return a WunderbaumNode instance from element, event.
4073
- *
4074
- * @param el
4393
+ /**
4394
+ * Return a WunderbaumNode instance from element or event.
4075
4395
  */
4076
4396
  static getNode(el) {
4077
4397
  if (!el) {
@@ -4093,7 +4413,7 @@ class Wunderbaum {
4093
4413
  }
4094
4414
  return null;
4095
4415
  }
4096
- /** */
4416
+ /** @internal */
4097
4417
  _registerExtension(extension) {
4098
4418
  this.extensionList.push(extension);
4099
4419
  this.extensions[extension.id] = extension;
@@ -4135,7 +4455,7 @@ class Wunderbaum {
4135
4455
  node.tree = null;
4136
4456
  node.parent = null;
4137
4457
  // node.title = "DISPOSED: " + node.title
4138
- this.viewNodes.delete(node);
4458
+ // this.viewNodes.delete(node);
4139
4459
  node.removeMarkup();
4140
4460
  }
4141
4461
  /** Call all hook methods of all registered extensions.*/
@@ -4153,7 +4473,9 @@ class Wunderbaum {
4153
4473
  }
4154
4474
  return res;
4155
4475
  }
4156
- /** Call tree method or extension method if defined.
4476
+ /**
4477
+ * Call tree method or extension method if defined.
4478
+ *
4157
4479
  * Example:
4158
4480
  * ```js
4159
4481
  * tree._callMethod("edit.startEdit", "arg1", "arg2")
@@ -4170,43 +4492,51 @@ class Wunderbaum {
4170
4492
  this.logError(`Calling undefined method '${name}()'.`);
4171
4493
  }
4172
4494
  }
4173
- /** Call event handler if defined in tree.options.
4495
+ /**
4496
+ * Call event handler if defined in tree or tree.EXTENSION options.
4497
+ *
4174
4498
  * Example:
4175
4499
  * ```js
4176
4500
  * tree._callEvent("edit.beforeEdit", {foo: 42})
4177
4501
  * ```
4178
4502
  */
4179
- _callEvent(name, extra) {
4180
- const [p, n] = name.split(".");
4503
+ _callEvent(type, extra) {
4504
+ const [p, n] = type.split(".");
4181
4505
  const opts = this.options;
4182
4506
  const func = n ? opts[p][n] : opts[p];
4183
4507
  if (func) {
4184
- return func.call(this, extend({ name: name, tree: this, util: this._util }, extra));
4508
+ return func.call(this, extend({ type: type, tree: this, util: this._util }, extra));
4185
4509
  // } else {
4186
- // this.logError(`Triggering undefined event '${name}'.`)
4510
+ // this.logError(`Triggering undefined event '${type}'.`)
4187
4511
  }
4188
4512
  }
4189
- /** Return the topmost visible node in the viewport */
4190
- _firstNodeInView(complete = true) {
4191
- let topIdx, node;
4192
- if (complete) {
4193
- topIdx = Math.ceil(this.scrollContainer.scrollTop / ROW_HEIGHT);
4194
- }
4195
- else {
4196
- topIdx = Math.floor(this.scrollContainer.scrollTop / ROW_HEIGHT);
4197
- }
4513
+ /** Return the node for given row index. */
4514
+ _getNodeByRowIdx(idx) {
4198
4515
  // TODO: start searching from active node (reverse)
4516
+ let node = null;
4199
4517
  this.visitRows((n) => {
4200
- if (n._rowIdx === topIdx) {
4518
+ if (n._rowIdx === idx) {
4201
4519
  node = n;
4202
4520
  return false;
4203
4521
  }
4204
4522
  });
4205
4523
  return node;
4206
4524
  }
4207
- /** Return the lowest visible node in the viewport */
4208
- _lastNodeInView(complete = true) {
4209
- let bottomIdx, node;
4525
+ /** Return the topmost visible node in the viewport. */
4526
+ getTopmostVpNode(complete = true) {
4527
+ let topIdx;
4528
+ const gracePy = 1; // ignore subpixel scrolling
4529
+ if (complete) {
4530
+ topIdx = Math.ceil((this.scrollContainer.scrollTop - gracePy) / ROW_HEIGHT);
4531
+ }
4532
+ else {
4533
+ topIdx = Math.floor(this.scrollContainer.scrollTop / ROW_HEIGHT);
4534
+ }
4535
+ return this._getNodeByRowIdx(topIdx);
4536
+ }
4537
+ /** Return the lowest visible node in the viewport. */
4538
+ getLowestVpNode(complete = true) {
4539
+ let bottomIdx;
4210
4540
  if (complete) {
4211
4541
  bottomIdx =
4212
4542
  Math.floor((this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
@@ -4217,16 +4547,10 @@ class Wunderbaum {
4217
4547
  Math.ceil((this.scrollContainer.scrollTop + this.scrollContainer.clientHeight) /
4218
4548
  ROW_HEIGHT) - 1;
4219
4549
  }
4220
- // TODO: start searching from active node
4221
- this.visitRows((n) => {
4222
- if (n._rowIdx === bottomIdx) {
4223
- node = n;
4224
- return false;
4225
- }
4226
- });
4227
- return node;
4550
+ bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
4551
+ return this._getNodeByRowIdx(bottomIdx);
4228
4552
  }
4229
- /** Return preceeding visible node in the viewport */
4553
+ /** Return preceeding visible node in the viewport. */
4230
4554
  _getPrevNodeInView(node, ofs = 1) {
4231
4555
  this.visitRows((n) => {
4232
4556
  node = n;
@@ -4236,7 +4560,7 @@ class Wunderbaum {
4236
4560
  }, { reverse: true, start: node || this.getActiveNode() });
4237
4561
  return node;
4238
4562
  }
4239
- /** Return following visible node in the viewport */
4563
+ /** Return following visible node in the viewport. */
4240
4564
  _getNextNodeInView(node, ofs = 1) {
4241
4565
  this.visitRows((n) => {
4242
4566
  node = n;
@@ -4246,10 +4570,15 @@ class Wunderbaum {
4246
4570
  }, { reverse: false, start: node || this.getActiveNode() });
4247
4571
  return node;
4248
4572
  }
4573
+ /**
4574
+ * Append (or insert) a list of toplevel nodes.
4575
+ *
4576
+ * @see {@link WunderbaumNode.addChildren}
4577
+ */
4249
4578
  addChildren(nodeData, options) {
4250
4579
  return this.root.addChildren(nodeData, options);
4251
4580
  }
4252
- /*
4581
+ /**
4253
4582
  * Apply a modification or navigation operation.
4254
4583
  *
4255
4584
  * Most of these commands simply map to a node or tree method.
@@ -4374,16 +4703,17 @@ class Wunderbaum {
4374
4703
  this.root.children = null;
4375
4704
  this.keyMap.clear();
4376
4705
  this.refKeyMap.clear();
4377
- this.viewNodes.clear();
4706
+ // this.viewNodes.clear();
4707
+ this.treeRowCount = 0;
4378
4708
  this.activeNode = null;
4379
4709
  this.focusNode = null;
4380
4710
  // this.types = {};
4381
4711
  // this. columns =[];
4382
4712
  // this._columnsById = {};
4383
4713
  // Modification Status
4384
- this.changedSince = 0;
4385
- this.changes.clear();
4386
- this.changedNodes.clear();
4714
+ // this.changedSince = 0;
4715
+ // this.changes.clear();
4716
+ // this.changedNodes.clear();
4387
4717
  // // --- FILTER ---
4388
4718
  // public filterMode: FilterModeType = null;
4389
4719
  // // --- KEYNAV ---
@@ -4391,7 +4721,7 @@ class Wunderbaum {
4391
4721
  // public cellNavMode = false;
4392
4722
  // public lastQuicksearchTime = 0;
4393
4723
  // public lastQuicksearchTerm = "";
4394
- this.updateViewport();
4724
+ this.setModified(ChangeType.structure);
4395
4725
  }
4396
4726
  /**
4397
4727
  * Clear nodes and markup and detach events and observers.
@@ -4411,10 +4741,11 @@ class Wunderbaum {
4411
4741
  /**
4412
4742
  * Return `tree.option.NAME` (also resolving if this is a callback).
4413
4743
  *
4414
- * See also [[WunderbaumNode.getOption()]] to consider `node.NAME` setting and
4415
- * `tree.types[node.type].NAME`.
4744
+ * See also {@link WunderbaumNode.getOption|WunderbaumNode.getOption()}
4745
+ * to consider `node.NAME` setting and `tree.types[node.type].NAME`.
4416
4746
  *
4417
- * @param name option name (use dot notation to access extension option, e.g. `filter.mode`)
4747
+ * @param name option name (use dot notation to access extension option, e.g.
4748
+ * `filter.mode`)
4418
4749
  */
4419
4750
  getOption(name, defaultValue) {
4420
4751
  let ext;
@@ -4458,18 +4789,14 @@ class Wunderbaum {
4458
4789
  }
4459
4790
  /** Run code, but defer `updateViewport()` until done. */
4460
4791
  runWithoutUpdate(func, hint = null) {
4461
- // const prev = this._disableUpdate;
4462
- // const start = Date.now();
4463
- // this._disableUpdate = Date.now();
4464
4792
  try {
4465
4793
  this.enableUpdate(false);
4466
- return func();
4794
+ const res = func();
4795
+ assert(!(res instanceof Promise));
4796
+ return res;
4467
4797
  }
4468
4798
  finally {
4469
4799
  this.enableUpdate(true);
4470
- // if (!prev && this._disableUpdate === start) {
4471
- // this._disableUpdate = 0;
4472
- // }
4473
4800
  }
4474
4801
  }
4475
4802
  /** Recursively expand all expandable nodes (triggers lazy load id needed). */
@@ -4487,11 +4814,12 @@ class Wunderbaum {
4487
4814
  /** Return the number of nodes in the data model.*/
4488
4815
  count(visible = false) {
4489
4816
  if (visible) {
4490
- return this.viewNodes.size;
4817
+ return this.treeRowCount;
4818
+ // return this.viewNodes.size;
4491
4819
  }
4492
4820
  return this.keyMap.size;
4493
4821
  }
4494
- /* Internal sanity check. */
4822
+ /** @internal sanity check. */
4495
4823
  _check() {
4496
4824
  let i = 0;
4497
4825
  this.visit((n) => {
@@ -4502,25 +4830,30 @@ class Wunderbaum {
4502
4830
  }
4503
4831
  // util.assert(this.keyMap.size === i);
4504
4832
  }
4505
- /**Find all nodes that matches condition.
4833
+ /**
4834
+ * Find all nodes that matches condition.
4506
4835
  *
4507
4836
  * @param match title string to search for, or a
4508
4837
  * callback function that returns `true` if a node is matched.
4509
- * @see [[WunderbaumNode.findAll]]
4838
+ *
4839
+ * @see {@link WunderbaumNode.findAll}
4510
4840
  */
4511
4841
  findAll(match) {
4512
4842
  return this.root.findAll(match);
4513
4843
  }
4514
- /**Find first node that matches condition.
4844
+ /**
4845
+ * Find first node that matches condition.
4515
4846
  *
4516
4847
  * @param match title string to search for, or a
4517
4848
  * callback function that returns `true` if a node is matched.
4518
- * @see [[WunderbaumNode.findFirst]]
4849
+ * @see {@link WunderbaumNode.findFirst}
4850
+ *
4519
4851
  */
4520
4852
  findFirst(match) {
4521
4853
  return this.root.findFirst(match);
4522
4854
  }
4523
- /** Find the next visible node that starts with `match`, starting at `startNode`
4855
+ /**
4856
+ * Find the next visible node that starts with `match`, starting at `startNode`
4524
4857
  * and wrap-around at the end.
4525
4858
  */
4526
4859
  findNextNode(match, startNode) {
@@ -4550,7 +4883,8 @@ class Wunderbaum {
4550
4883
  }
4551
4884
  return res;
4552
4885
  }
4553
- /** Find a node relative to another node.
4886
+ /**
4887
+ * Find a node relative to another node.
4554
4888
  *
4555
4889
  * @param node
4556
4890
  * @param where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'.
@@ -4560,7 +4894,7 @@ class Wunderbaum {
4560
4894
  */
4561
4895
  findRelatedNode(node, where, includeHidden = false) {
4562
4896
  let res = null;
4563
- let pageSize = Math.floor(this.scrollContainer.clientHeight / ROW_HEIGHT);
4897
+ const pageSize = Math.floor(this.scrollContainer.clientHeight / ROW_HEIGHT);
4564
4898
  switch (where) {
4565
4899
  case "parent":
4566
4900
  if (node.parent && node.parent.parent) {
@@ -4616,9 +4950,9 @@ class Wunderbaum {
4616
4950
  res = this._getNextNodeInView(node);
4617
4951
  break;
4618
4952
  case "pageDown":
4619
- let bottomNode = this._lastNodeInView();
4620
- // this.logDebug(where, this.focusNode, bottomNode);
4621
- if (this.focusNode !== bottomNode) {
4953
+ const bottomNode = this.getLowestVpNode();
4954
+ // this.logDebug(`${where}(${node}) -> ${bottomNode}`);
4955
+ if (node._rowIdx < bottomNode._rowIdx) {
4622
4956
  res = bottomNode;
4623
4957
  }
4624
4958
  else {
@@ -4626,12 +4960,13 @@ class Wunderbaum {
4626
4960
  }
4627
4961
  break;
4628
4962
  case "pageUp":
4629
- if (this.focusNode && this.focusNode._rowIdx === 0) {
4630
- res = this.focusNode;
4963
+ if (node._rowIdx === 0) {
4964
+ res = node;
4631
4965
  }
4632
4966
  else {
4633
- let topNode = this._firstNodeInView();
4634
- if (this.focusNode !== topNode) {
4967
+ const topNode = this.getTopmostVpNode();
4968
+ // this.logDebug(`${where}(${node}) -> ${topNode}`);
4969
+ if (node._rowIdx > topNode._rowIdx) {
4635
4970
  res = topNode;
4636
4971
  }
4637
4972
  else {
@@ -4645,7 +4980,7 @@ class Wunderbaum {
4645
4980
  return res;
4646
4981
  }
4647
4982
  /**
4648
- * Return the active cell of the currently active node or null.
4983
+ * Return the active cell (`span.wb-col`) of the currently active node or null.
4649
4984
  */
4650
4985
  getActiveColElem() {
4651
4986
  if (this.activeNode && this.activeColIdx >= 0) {
@@ -4678,7 +5013,8 @@ class Wunderbaum {
4678
5013
  * TYPE: 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined
4679
5014
  */
4680
5015
  static getEventInfo(event) {
4681
- let target = event.target, cl = target.classList, parentCol = target.closest(".wb-col"), node = Wunderbaum.getNode(target), res = {
5016
+ let target = event.target, cl = target.classList, parentCol = target.closest("span.wb-col"), node = Wunderbaum.getNode(target), tree = node ? node.tree : Wunderbaum.getTree(event), res = {
5017
+ tree: tree,
4682
5018
  node: node,
4683
5019
  region: TargetType.unknown,
4684
5020
  colDef: undefined,
@@ -4710,13 +5046,15 @@ class Wunderbaum {
4710
5046
  }
4711
5047
  else {
4712
5048
  // Somewhere near the title
4713
- console.warn("getEventInfo(): not found", event, res);
5049
+ if (event.type !== "mousemove" && !(event instanceof KeyboardEvent)) {
5050
+ console.warn("getEventInfo(): not found", event, res);
5051
+ }
4714
5052
  return res;
4715
5053
  }
4716
5054
  if (res.colIdx === -1) {
4717
5055
  res.colIdx = 0;
4718
5056
  }
4719
- res.colDef = node.tree.columns[res.colIdx];
5057
+ res.colDef = tree === null || tree === void 0 ? void 0 : tree.columns[res.colIdx];
4720
5058
  res.colDef != null ? (res.colId = res.colDef.id) : 0;
4721
5059
  // this.log("Event", event, res);
4722
5060
  return res;
@@ -4740,7 +5078,8 @@ class Wunderbaum {
4740
5078
  isEditing() {
4741
5079
  return this._callMethod("edit.isEditingTitle");
4742
5080
  }
4743
- /** Return true if any node is currently beeing loaded, i.e. a Ajax request is pending.
5081
+ /**
5082
+ * Return true if any node is currently beeing loaded, i.e. a Ajax request is pending.
4744
5083
  */
4745
5084
  isLoading() {
4746
5085
  var res = false;
@@ -4767,7 +5106,7 @@ class Wunderbaum {
4767
5106
  console.error.apply(console, args);
4768
5107
  }
4769
5108
  }
4770
- /* Log to console if opts.debugLevel >= 3 */
5109
+ /** Log to console if opts.debugLevel >= 3 */
4771
5110
  logInfo(...args) {
4772
5111
  if (this.options.debugLevel >= 3) {
4773
5112
  Array.prototype.unshift.call(args, this.toString());
@@ -4794,77 +5133,13 @@ class Wunderbaum {
4794
5133
  console.warn.apply(console, args);
4795
5134
  }
4796
5135
  }
4797
- /** */
4798
- render(opts) {
4799
- const label = this.logTime("render");
4800
- let idx = 0;
4801
- let top = 0;
4802
- const height = ROW_HEIGHT;
4803
- let modified = false;
4804
- let start = opts === null || opts === void 0 ? void 0 : opts.startIdx;
4805
- let end = opts === null || opts === void 0 ? void 0 : opts.endIdx;
4806
- const obsoleteViewNodes = this.viewNodes;
4807
- this.viewNodes = new Set();
4808
- let viewNodes = this.viewNodes;
4809
- // this.debug("render", opts);
4810
- assert(start != null && end != null);
4811
- // Make sure start is always even, so the alternating row colors don't
4812
- // change when scrolling:
4813
- if (start % 2) {
4814
- start--;
4815
- }
4816
- this.visitRows(function (node) {
4817
- const prevIdx = node._rowIdx;
4818
- viewNodes.add(node);
4819
- obsoleteViewNodes.delete(node);
4820
- if (prevIdx !== idx) {
4821
- node._rowIdx = idx;
4822
- modified = true;
4823
- }
4824
- if (idx < start || idx > end) {
4825
- node._callEvent("discard");
4826
- node.removeMarkup();
4827
- }
4828
- else {
4829
- // if (!node._rowElem || prevIdx != idx) {
4830
- node.render({ top: top });
4831
- }
4832
- idx++;
4833
- top += height;
4834
- });
4835
- for (const prevNode of obsoleteViewNodes) {
4836
- prevNode._callEvent("discard");
4837
- prevNode.removeMarkup();
4838
- }
4839
- // Resize tree container
4840
- this.nodeListElement.style.height = "" + top + "px";
4841
- // this.log("render()", this.nodeListElement.style.height);
4842
- this.logTimeEnd(label);
4843
- return modified;
4844
- }
4845
- /**Recalc and apply header columns from `this.columns`. */
4846
- renderHeader() {
4847
- if (!this.headerElement) {
4848
- return;
4849
- }
4850
- const headerRow = this.headerElement.querySelector(".wb-row");
4851
- assert(headerRow);
4852
- headerRow.innerHTML = "<span class='wb-col'></span>".repeat(this.columns.length);
4853
- for (let i = 0; i < this.columns.length; i++) {
4854
- let col = this.columns[i];
4855
- let colElem = headerRow.children[i];
4856
- colElem.style.left = col._ofsPx + "px";
4857
- colElem.style.width = col._widthPx + "px";
4858
- colElem.textContent = col.title || col.id;
4859
- }
4860
- }
4861
5136
  /**
5137
+ * Make sure that this node is scrolled into the viewport.
4862
5138
  *
4863
5139
  * @param {boolean | PlainObject} [effects=false] animation options.
4864
5140
  * @param {object} [options=null] {topNode: null, effects: ..., parent: ...}
4865
5141
  * this node will remain visible in
4866
5142
  * any case, even if `this` is outside the scroll pane.
4867
- * Make sure that a node is scrolled into the viewport.
4868
5143
  */
4869
5144
  scrollTo(opts) {
4870
5145
  const MARGIN = 1;
@@ -4885,47 +5160,34 @@ class Wunderbaum {
4885
5160
  // Node is above viewport
4886
5161
  newTop = nodeOfs + MARGIN;
4887
5162
  }
4888
- this.log("scrollTo(" + nodeOfs + "): " + curTop + " => " + newTop, height);
4889
5163
  if (newTop != null) {
5164
+ this.log("scrollTo(" + nodeOfs + "): " + curTop + " => " + newTop, height);
4890
5165
  this.scrollContainer.scrollTop = newTop;
4891
- this.updateViewport();
5166
+ this.setModified(ChangeType.vscroll);
4892
5167
  }
4893
5168
  }
4894
- /** */
4895
- setCellMode(mode) {
4896
- // util.assert(this.cellNavMode);
4897
- // util.assert(0 <= colIdx && colIdx < this.columns.length);
4898
- if (mode === this.navMode) {
4899
- return;
4900
- }
4901
- const prevMode = this.navMode;
4902
- const cellMode = mode !== NavigationMode.row;
4903
- this.navMode = mode;
4904
- if (cellMode && prevMode === NavigationMode.row) {
4905
- this.setColumn(0);
4906
- }
4907
- this.element.classList.toggle("wb-cell-mode", cellMode);
4908
- this.element.classList.toggle("wb-cell-edit-mode", mode === NavigationMode.cellEdit);
4909
- this.setModified(ChangeType.row, this.activeNode);
4910
- }
4911
- /** */
5169
+ /**
5170
+ * Set column #colIdx to 'active'.
5171
+ *
5172
+ * This higlights the column header and -cells by adding the `wb-active` class.
5173
+ * Available in cell-nav and cell-edit mode, not in row-mode.
5174
+ */
4912
5175
  setColumn(colIdx) {
5176
+ var _a;
4913
5177
  assert(this.navMode !== NavigationMode.row);
4914
5178
  assert(0 <= colIdx && colIdx < this.columns.length);
4915
5179
  this.activeColIdx = colIdx;
4916
- // node.setActive(true, { column: tree.activeColIdx + 1 });
4917
- this.setModified(ChangeType.row, this.activeNode);
4918
5180
  // Update `wb-active` class for all headers
4919
5181
  if (this.headerElement) {
4920
5182
  for (let rowDiv of this.headerElement.children) {
4921
- // for (let rowDiv of document.querySelector("div.wb-header").children) {
4922
5183
  let i = 0;
4923
5184
  for (let colDiv of rowDiv.children) {
4924
5185
  colDiv.classList.toggle("wb-active", i++ === colIdx);
4925
5186
  }
4926
5187
  }
4927
5188
  }
4928
- // Update `wb-active` class for all cell divs
5189
+ (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.setModified(ChangeType.status);
5190
+ // Update `wb-active` class for all cell spans
4929
5191
  for (let rowDiv of this.nodeListElement.children) {
4930
5192
  let i = 0;
4931
5193
  for (let colDiv of rowDiv.children) {
@@ -4933,7 +5195,7 @@ class Wunderbaum {
4933
5195
  }
4934
5196
  }
4935
5197
  }
4936
- /** */
5198
+ /** Set or remove keybaord focus to the tree container. */
4937
5199
  setFocus(flag = true) {
4938
5200
  if (flag) {
4939
5201
  this.element.focus();
@@ -4942,129 +5204,321 @@ class Wunderbaum {
4942
5204
  this.element.blur();
4943
5205
  }
4944
5206
  }
4945
- /** */
4946
5207
  setModified(change, node, options) {
4947
- if (!this.changedSince) {
4948
- this.changedSince = Date.now();
5208
+ if (this._disableUpdateCount) {
5209
+ // Assuming that we redraw all when enableUpdate() is re-enabled.
5210
+ // this.log(
5211
+ // `IGNORED setModified(${change}) node=${node} (disable level ${this._disableUpdateCount})`
5212
+ // );
5213
+ return;
4949
5214
  }
4950
- this.changes.add(change);
4951
- if (change === ChangeType.structure) {
4952
- this.changedNodes.clear();
5215
+ // this.log(`setModified(${change}) node=${node}`);
5216
+ if (!(node instanceof WunderbaumNode)) {
5217
+ options = node;
5218
+ }
5219
+ const immediate = !!getOption(options, "immediate");
5220
+ switch (change) {
5221
+ case ChangeType.any:
5222
+ case ChangeType.structure:
5223
+ case ChangeType.header:
5224
+ this.changeRedrawRequestPending = true;
5225
+ this.updateViewport(immediate);
5226
+ break;
5227
+ case ChangeType.vscroll:
5228
+ this.updateViewport(immediate);
5229
+ break;
5230
+ case ChangeType.row:
5231
+ case ChangeType.data:
5232
+ case ChangeType.status:
5233
+ // Single nodes are immedialtely updated if already inside the viewport
5234
+ // (otherwise we can ignore)
5235
+ if (node._rowElem) {
5236
+ node.render({ change: change });
5237
+ }
5238
+ break;
5239
+ default:
5240
+ error(`Invalid change type ${change}`);
4953
5241
  }
4954
- else if (node && !this.changes.has(ChangeType.structure)) {
4955
- if (this.changedNodes.size < MAX_CHANGED_NODES) {
4956
- this.changedNodes.add(node);
4957
- }
4958
- else {
4959
- this.changes.add(ChangeType.structure);
4960
- this.changedNodes.clear();
4961
- }
5242
+ }
5243
+ /** Set the tree's navigation mode. */
5244
+ setNavigationMode(mode) {
5245
+ var _a;
5246
+ // util.assert(this.cellNavMode);
5247
+ // util.assert(0 <= colIdx && colIdx < this.columns.length);
5248
+ if (mode === this.navMode) {
5249
+ return;
4962
5250
  }
4963
- // this.log("setModified(" + change + ")", node);
5251
+ const prevMode = this.navMode;
5252
+ const cellMode = mode !== NavigationMode.row;
5253
+ this.navMode = mode;
5254
+ if (cellMode && prevMode === NavigationMode.row) {
5255
+ this.setColumn(0);
5256
+ }
5257
+ this.element.classList.toggle("wb-cell-mode", cellMode);
5258
+ this.element.classList.toggle("wb-cell-edit-mode", mode === NavigationMode.cellEdit);
5259
+ // this.setModified(ChangeType.row, this.activeNode);
5260
+ (_a = this.activeNode) === null || _a === void 0 ? void 0 : _a.setModified(ChangeType.status);
4964
5261
  }
5262
+ /** Display tree status (ok, loading, error, noData) using styles and a dummy root node. */
4965
5263
  setStatus(status, message, details) {
4966
5264
  return this.root.setStatus(status, message, details);
4967
5265
  }
4968
5266
  /** Update column headers and width. */
4969
5267
  updateColumns(opts) {
4970
- let modified = false;
4971
- let minWidth = 4;
4972
- let vpWidth = this.element.clientWidth;
5268
+ opts = Object.assign({ calculateCols: true, updateRows: true }, opts);
5269
+ const minWidth = 4;
5270
+ const vpWidth = this.element.clientWidth;
4973
5271
  let totalWeight = 0;
4974
5272
  let fixedWidth = 0;
4975
- // Gather width requests
4976
- this._columnsById = {};
4977
- for (let col of this.columns) {
4978
- this._columnsById[col.id] = col;
4979
- let cw = col.width;
4980
- if (!cw || cw === "*") {
4981
- col._weight = 1.0;
4982
- totalWeight += 1.0;
4983
- }
4984
- else if (typeof cw === "number") {
4985
- col._weight = cw;
4986
- totalWeight += cw;
4987
- }
4988
- else if (typeof cw === "string" && cw.endsWith("px")) {
4989
- col._weight = 0;
4990
- let px = parseFloat(cw.slice(0, -2));
4991
- if (col._widthPx != px) {
4992
- modified = true;
4993
- col._widthPx = px;
5273
+ let modified = false;
5274
+ if (opts.calculateCols) {
5275
+ // Gather width requests
5276
+ this._columnsById = {};
5277
+ for (let col of this.columns) {
5278
+ this._columnsById[col.id] = col;
5279
+ let cw = col.width;
5280
+ if (!cw || cw === "*") {
5281
+ col._weight = 1.0;
5282
+ totalWeight += 1.0;
5283
+ }
5284
+ else if (typeof cw === "number") {
5285
+ col._weight = cw;
5286
+ totalWeight += cw;
5287
+ }
5288
+ else if (typeof cw === "string" && cw.endsWith("px")) {
5289
+ col._weight = 0;
5290
+ let px = parseFloat(cw.slice(0, -2));
5291
+ if (col._widthPx != px) {
5292
+ modified = true;
5293
+ col._widthPx = px;
5294
+ }
5295
+ fixedWidth += px;
5296
+ }
5297
+ else {
5298
+ error("Invalid column width: " + cw);
4994
5299
  }
4995
- fixedWidth += px;
4996
5300
  }
4997
- else {
4998
- error("Invalid column width: " + cw);
4999
- }
5000
- }
5001
- // Share remaining space between non-fixed columns
5002
- let restPx = Math.max(0, vpWidth - fixedWidth);
5003
- let ofsPx = 0;
5004
- for (let col of this.columns) {
5005
- if (col._weight) {
5006
- let px = Math.max(minWidth, (restPx * col._weight) / totalWeight);
5007
- if (col._widthPx != px) {
5008
- modified = true;
5009
- col._widthPx = px;
5301
+ // Share remaining space between non-fixed columns
5302
+ const restPx = Math.max(0, vpWidth - fixedWidth);
5303
+ let ofsPx = 0;
5304
+ for (let col of this.columns) {
5305
+ if (col._weight) {
5306
+ const px = Math.max(minWidth, (restPx * col._weight) / totalWeight);
5307
+ if (col._widthPx != px) {
5308
+ modified = true;
5309
+ col._widthPx = px;
5310
+ }
5010
5311
  }
5312
+ col._ofsPx = ofsPx;
5313
+ ofsPx += col._widthPx;
5011
5314
  }
5012
- col._ofsPx = ofsPx;
5013
- ofsPx += col._widthPx;
5014
5315
  }
5015
5316
  // Every column has now a calculated `_ofsPx` and `_widthPx`
5016
5317
  // this.logInfo("UC", this.columns, vpWidth, this.element.clientWidth, this.element);
5017
5318
  // console.trace();
5018
5319
  // util.error("BREAK");
5019
5320
  if (modified) {
5020
- this.renderHeader();
5021
- if (opts.render !== false) {
5022
- this.render();
5321
+ this._renderHeaderMarkup();
5322
+ if (opts.updateRows) {
5323
+ this._updateRows();
5023
5324
  }
5024
5325
  }
5025
5326
  }
5026
- /** Render all rows that are visible in the viewport. */
5327
+ /** Create/update header markup from `this.columns` definition.
5328
+ * @internal
5329
+ */
5330
+ _renderHeaderMarkup() {
5331
+ if (!this.headerElement) {
5332
+ return;
5333
+ }
5334
+ const headerRow = this.headerElement.querySelector(".wb-row");
5335
+ assert(headerRow);
5336
+ headerRow.innerHTML = "<span class='wb-col'></span>".repeat(this.columns.length);
5337
+ for (let i = 0; i < this.columns.length; i++) {
5338
+ const col = this.columns[i];
5339
+ const colElem = headerRow.children[i];
5340
+ colElem.style.left = col._ofsPx + "px";
5341
+ colElem.style.width = col._widthPx + "px";
5342
+ // colElem.textContent = col.title || col.id;
5343
+ const title = escapeHtml(col.title || col.id);
5344
+ colElem.innerHTML = `<span class="wb-col-title">${title}</span> <span class="wb-col-resizer"></span>`;
5345
+ // colElem.innerHTML = `${title} <span class="wb-col-resizer"></span>`;
5346
+ }
5347
+ }
5348
+ /** Render header and all rows that are visible in the viewport (async, throttled). */
5027
5349
  updateViewport(immediate = false) {
5028
5350
  // Call the `throttle` wrapper for `this._updateViewport()` which will
5029
5351
  // execute immediately on the leading edge of a sequence:
5030
- this._updateViewportThrottled();
5031
5352
  if (immediate) {
5032
- this._updateViewportThrottled.flush();
5353
+ this._updateViewport();
5354
+ }
5355
+ else {
5356
+ this._updateViewportThrottled();
5033
5357
  }
5034
5358
  }
5359
+ /**
5360
+ * This is the actual update method, which is wrapped inside a throttle method.
5361
+ * This protected method should not be called directly but via
5362
+ * `tree.updateViewport()` or `tree.setModified()`.
5363
+ * It calls `updateColumns()` and `_updateRows()`.
5364
+ * @internal
5365
+ */
5035
5366
  _updateViewport() {
5036
- if (this._disableUpdate) {
5367
+ if (this._disableUpdateCount) {
5368
+ this.log(`IGNORED _updateViewport() disable level: ${this._disableUpdateCount}`);
5037
5369
  return;
5038
5370
  }
5371
+ const newNodesOnly = !this.changeRedrawRequestPending;
5372
+ this.changeRedrawRequestPending = false;
5039
5373
  let height = this.scrollContainer.clientHeight;
5040
- // We cannot get the height for abolut positioned parent, so look at first col
5374
+ // We cannot get the height for absolute positioned parent, so look at first col
5041
5375
  // let headerHeight = this.headerElement.clientHeight
5042
5376
  // let headerHeight = this.headerElement.children[0].children[0].clientHeight;
5043
5377
  const headerHeight = this.options.headerHeightPx;
5044
- let wantHeight = this.element.clientHeight - headerHeight;
5045
- let ofs = this.scrollContainer.scrollTop;
5378
+ const wantHeight = this.element.clientHeight - headerHeight;
5046
5379
  if (Math.abs(height - wantHeight) > 1.0) {
5047
5380
  // this.log("resize", height, wantHeight);
5048
5381
  this.scrollContainer.style.height = wantHeight + "px";
5049
5382
  height = wantHeight;
5050
5383
  }
5051
- this.updateColumns({ render: false });
5052
- this.render({
5053
- startIdx: Math.max(0, ofs / ROW_HEIGHT - RENDER_MAX_PREFETCH),
5054
- endIdx: Math.max(0, (ofs + height) / ROW_HEIGHT + RENDER_MAX_PREFETCH),
5055
- });
5384
+ // console.profile(`_updateViewport()`)
5385
+ this.updateColumns({ updateRows: false });
5386
+ this._updateRows({ newNodesOnly: newNodesOnly });
5387
+ // console.profileEnd(`_updateViewport()`)
5056
5388
  this._callEvent("update");
5057
5389
  }
5058
- /** Call callback(node) for all nodes in hierarchical order (depth-first).
5390
+ /**
5391
+ * Assert that TR order matches the natural node order
5392
+ * @internal
5393
+ */
5394
+ _validateRows() {
5395
+ let trs = this.nodeListElement.childNodes;
5396
+ let i = 0;
5397
+ let prev = -1;
5398
+ let ok = true;
5399
+ trs.forEach((element) => {
5400
+ const tr = element;
5401
+ const top = Number.parseInt(tr.style.top);
5402
+ const n = tr._wb_node;
5403
+ // if (i < 4) {
5404
+ // console.info(
5405
+ // `TR#${i}, rowIdx=${n._rowIdx} , top=${top}px: '${n.title}'`
5406
+ // );
5407
+ // }
5408
+ if (prev >= 0 && top !== prev + ROW_HEIGHT) {
5409
+ n.logWarn(`TR order mismatch at index ${i}: top=${top}px != ${prev + ROW_HEIGHT}`);
5410
+ // throw new Error("fault");
5411
+ ok = false;
5412
+ }
5413
+ prev = top;
5414
+ i++;
5415
+ });
5416
+ return ok;
5417
+ }
5418
+ /*
5419
+ * - Traverse all *visible* of the whole tree, i.e. skip collapsed nodes.
5420
+ * - Store count of rows to `tree.treeRowCount`.
5421
+ * - Renumber `node._rowIdx` for all visible nodes.
5422
+ * - Calculate the index range that must be rendered to fill the viewport
5423
+ * (including upper and lower prefetch)
5424
+ * -
5425
+ */
5426
+ _updateRows(opts) {
5427
+ const label = this.logTime("_updateRows");
5428
+ // this.log("_updateRows", opts)
5429
+ opts = Object.assign({ newNodesOnly: false }, opts);
5430
+ const newNodesOnly = !!opts.newNodesOnly;
5431
+ const row_height = ROW_HEIGHT;
5432
+ const vp_height = this.scrollContainer.clientHeight;
5433
+ const prefetch = RENDER_MAX_PREFETCH;
5434
+ const ofs = this.scrollContainer.scrollTop;
5435
+ let startIdx = Math.max(0, ofs / row_height - prefetch);
5436
+ startIdx = Math.floor(startIdx);
5437
+ // Make sure start is always even, so the alternating row colors don't
5438
+ // change when scrolling:
5439
+ if (startIdx % 2) {
5440
+ startIdx--;
5441
+ }
5442
+ let endIdx = Math.max(0, (ofs + vp_height) / row_height + prefetch);
5443
+ endIdx = Math.ceil(endIdx);
5444
+ // const obsoleteViewNodes = this.viewNodes;
5445
+ // this.viewNodes = new Set();
5446
+ // const viewNodes = this.viewNodes;
5447
+ // this.debug("render", opts);
5448
+ const obsoleteNodes = new Set();
5449
+ this.nodeListElement.childNodes.forEach((elem) => {
5450
+ const tr = elem;
5451
+ obsoleteNodes.add(tr._wb_node);
5452
+ });
5453
+ let idx = 0;
5454
+ let top = 0;
5455
+ let modified = false;
5456
+ let prevElem = "first";
5457
+ this.visitRows(function (node) {
5458
+ // node.log("visit")
5459
+ const rowDiv = node._rowElem;
5460
+ // Renumber all expanded nodes
5461
+ if (node._rowIdx !== idx) {
5462
+ node._rowIdx = idx;
5463
+ modified = true;
5464
+ }
5465
+ if (idx < startIdx || idx > endIdx) {
5466
+ // row is outside viewport bounds
5467
+ if (rowDiv) {
5468
+ prevElem = rowDiv;
5469
+ }
5470
+ }
5471
+ else if (rowDiv && newNodesOnly) {
5472
+ obsoleteNodes.delete(node);
5473
+ // no need to update existing node markup
5474
+ rowDiv.style.top = idx * ROW_HEIGHT + "px";
5475
+ prevElem = rowDiv;
5476
+ }
5477
+ else {
5478
+ obsoleteNodes.delete(node);
5479
+ // Create new markup
5480
+ if (rowDiv) {
5481
+ rowDiv.style.top = idx * ROW_HEIGHT + "px";
5482
+ }
5483
+ node.render({ top: top, after: prevElem });
5484
+ // node.log("render", top, prevElem, "=>", node._rowElem);
5485
+ prevElem = node._rowElem;
5486
+ }
5487
+ idx++;
5488
+ top += row_height;
5489
+ });
5490
+ this.treeRowCount = idx;
5491
+ for (const n of obsoleteNodes) {
5492
+ n._callEvent("discard");
5493
+ n.removeMarkup();
5494
+ }
5495
+ // Resize tree container
5496
+ this.nodeListElement.style.height = `${top}px`;
5497
+ // this.log(
5498
+ // `render(scrollOfs:${ofs}, ${startIdx}..${endIdx})`,
5499
+ // this.nodeListElement.style.height
5500
+ // );
5501
+ this.logTimeEnd(label);
5502
+ this._validateRows();
5503
+ return modified;
5504
+ }
5505
+ /**
5506
+ * Call callback(node) for all nodes in hierarchical order (depth-first).
5059
5507
  *
5060
5508
  * @param {function} callback the callback function.
5061
- * Return false to stop iteration, return "skip" to skip this node and children only.
5509
+ * Return false to stop iteration, return "skip" to skip this node and
5510
+ * children only.
5062
5511
  * @returns {boolean} false, if the iterator was stopped.
5063
5512
  */
5064
5513
  visit(callback) {
5065
5514
  return this.root.visit(callback, false);
5066
5515
  }
5067
- /** Call fn(node) for all nodes in vertical order, top down (or bottom up).<br>
5516
+ /**
5517
+ * Call fn(node) for all nodes in vertical order, top down (or bottom up).
5518
+ *
5519
+ * Note that this considers expansion state, i.e. children of collapsed nodes
5520
+ * are skipped.
5521
+ *
5068
5522
  * Stop iteration, if fn() returns false.<br>
5069
5523
  * Return false if iteration was stopped.
5070
5524
  *
@@ -5144,7 +5598,8 @@ class Wunderbaum {
5144
5598
  }
5145
5599
  return true;
5146
5600
  }
5147
- /** Call fn(node) for all nodes in vertical order, bottom up.
5601
+ /**
5602
+ * Call fn(node) for all nodes in vertical order, bottom up.
5148
5603
  * @internal
5149
5604
  */
5150
5605
  _visitRowsUp(callback, opts) {
@@ -5188,19 +5643,36 @@ class Wunderbaum {
5188
5643
  }
5189
5644
  return true;
5190
5645
  }
5191
- /** . */
5646
+ /**
5647
+ * Reload the tree with a new source.
5648
+ *
5649
+ * Previous data is cleared.
5650
+ * Pass `options.columns` to define a header (may also be part of `source.columns`).
5651
+ */
5192
5652
  load(source, options = {}) {
5193
5653
  this.clear();
5194
5654
  const columns = options.columns || source.columns;
5195
5655
  if (columns) {
5196
5656
  this.columns = options.columns;
5197
- this.renderHeader();
5198
- // this.updateColumns({ render: false });
5657
+ // this._renderHeaderMarkup();
5658
+ this.updateColumns({ calculateCols: false });
5199
5659
  }
5200
5660
  return this.root.load(source);
5201
5661
  }
5202
5662
  /**
5663
+ * Disable render requests during operations that would trigger many updates.
5203
5664
  *
5665
+ * ```js
5666
+ * try {
5667
+ * tree.enableUpdate(false);
5668
+ * // ... (long running operation that would trigger many updates)
5669
+ * foo();
5670
+ * // ... NOTE: make sure that async operations have finished, e.g.
5671
+ * await foo();
5672
+ * } finally {
5673
+ * tree.enableUpdate(true);
5674
+ * }
5675
+ * ```
5204
5676
  */
5205
5677
  enableUpdate(flag) {
5206
5678
  /*
@@ -5208,20 +5680,22 @@ class Wunderbaum {
5208
5680
  1 >-------------------------------------<
5209
5681
  2 >--------------------<
5210
5682
  3 >--------------------------<
5211
-
5212
- 5
5213
-
5214
5683
  */
5215
- // this.logDebug( `enableUpdate(${flag}): count=${this._disableUpdateCount}...` );
5216
5684
  if (flag) {
5217
- assert(this._disableUpdateCount > 0);
5685
+ assert(this._disableUpdateCount > 0, "enableUpdate(true) was called too often");
5218
5686
  this._disableUpdateCount--;
5687
+ // this.logDebug(
5688
+ // `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...`
5689
+ // );
5219
5690
  if (this._disableUpdateCount === 0) {
5220
5691
  this.updateViewport();
5221
5692
  }
5222
5693
  }
5223
5694
  else {
5224
5695
  this._disableUpdateCount++;
5696
+ // this.logDebug(
5697
+ // `enableUpdate(${flag}): count -> ${this._disableUpdateCount}...`
5698
+ // );
5225
5699
  // this._disableUpdate = Date.now();
5226
5700
  }
5227
5701
  // return !flag; // return previous value
@@ -5254,8 +5728,10 @@ class Wunderbaum {
5254
5728
  return this.extensions.filter.updateFilter();
5255
5729
  }
5256
5730
  }
5257
- Wunderbaum.version = "v0.0.1-0"; // Set to semver by 'grunt release'
5258
5731
  Wunderbaum.sequence = 0;
5732
+ /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
5733
+ Wunderbaum.version = "v0.0.4"; // Set to semver by 'grunt release'
5734
+ /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
5259
5735
  Wunderbaum.util = util;
5260
5736
 
5261
5737
  export { Wunderbaum };