wunderbaum 0.12.1 → 0.14.0

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.
package/src/wb_node.ts CHANGED
@@ -13,7 +13,6 @@ import {
13
13
  ApplyCommandType,
14
14
  ChangeType,
15
15
  CheckboxOption,
16
- ColumnDefinition,
17
16
  ColumnEventInfoMap,
18
17
  ExpandAllOptions,
19
18
  IconOption,
@@ -21,6 +20,7 @@ import {
21
20
  MakeVisibleOptions,
22
21
  MatcherCallback,
23
22
  NavigateOptions,
23
+ NavigationType,
24
24
  NodeAnyCallback,
25
25
  NodeStatusType,
26
26
  NodeStringCallback,
@@ -36,7 +36,7 @@ import {
36
36
  SetStatusOptions,
37
37
  SortByPropertyOptions,
38
38
  SortCallback,
39
- SortOrderType,
39
+ SortOptions,
40
40
  SourceType,
41
41
  TooltipOption,
42
42
  TristateType,
@@ -46,11 +46,13 @@ import {
46
46
  import {
47
47
  decompressSourceData,
48
48
  ICON_WIDTH,
49
- KEY_TO_ACTION_DICT,
49
+ KEY_TO_NAVIGATION_MAP,
50
50
  makeNodeTitleMatcher,
51
+ NODE_TYPE_FOLDER,
51
52
  nodeTitleSorter,
52
53
  RESERVED_TREE_SOURCE_KEYS,
53
- TEST_IMG,
54
+ TEST_FILE_PATH,
55
+ TEST_HTML,
54
56
  TITLE_SPAN_PAD_Y,
55
57
  } from "./common";
56
58
  import { Deferred } from "./deferred";
@@ -171,7 +173,11 @@ export class WunderbaumNode {
171
173
  _partsel = false;
172
174
  _partload = false;
173
175
  // --- FILTER ---
174
- public match?: boolean; // Added and removed by filter code
176
+ /**
177
+ * > 0 if matched (-1 to keep system nodes visible);
178
+ * Added and removed by filter code.
179
+ */
180
+ public match?: number;
175
181
  public subMatchCount?: number = 0;
176
182
  // public subMatchBadge?: HTMLElement;
177
183
  /** @internal */
@@ -181,13 +187,13 @@ export class WunderbaumNode {
181
187
  _rowIdx: number | undefined = 0;
182
188
  _rowElem: HTMLDivElement | undefined = undefined;
183
189
 
184
- constructor(tree: Wunderbaum, parent: WunderbaumNode, data: any) {
190
+ constructor(tree: Wunderbaum, parent: WunderbaumNode, data: WbNodeData) {
185
191
  util.assert(!parent || parent.tree === tree, `Invalid parent: ${parent}`);
186
192
  util.assert(!data.children, "'children' not allowed here");
187
193
 
188
194
  this.tree = tree;
189
195
  this.parent = parent;
190
- this.key = "" + (data.key ?? ++WunderbaumNode.sequence);
196
+ this.key = tree._calculateKey(data, parent);
191
197
  this.title = "" + (data.title ?? "<" + this.key + ">");
192
198
  this.expanded = !!data.expanded;
193
199
  this.lazy = !!data.lazy;
@@ -327,12 +333,19 @@ export class WunderbaumNode {
327
333
  nodeData = [<WbNodeData>nodeData];
328
334
  }
329
335
  const forceExpand =
330
- applyMinExpanLevel && _level < tree.options.minExpandLevel!;
336
+ applyMinExpanLevel && _level < tree.options.minExpandLevel;
331
337
  for (const child of <WbNodeData[]>nodeData) {
332
338
  const subChildren = child.children;
339
+ // Remove children property from source data because it should not be
340
+ // passed to the constructor of WunderbaumNode:
333
341
  delete child.children;
334
342
 
335
343
  const n = new WunderbaumNode(tree, this, child);
344
+
345
+ // Set `children` property again, so it can be used in `reload()`
346
+ if (subChildren != null) {
347
+ child.children = subChildren;
348
+ }
336
349
  if (forceExpand && !n.isUnloaded()) {
337
350
  n.expanded = true;
338
351
  }
@@ -656,7 +669,7 @@ export class WunderbaumNode {
656
669
  *
657
670
  * @see {@link Wunderbaum.findRelatedNode|tree.findRelatedNode()}
658
671
  */
659
- findRelatedNode(where: string, includeHidden = false) {
672
+ findRelatedNode(where: NavigationType, includeHidden = false) {
660
673
  return this.tree.findRelatedNode(this, where, includeHidden);
661
674
  }
662
675
 
@@ -799,7 +812,7 @@ export class WunderbaumNode {
799
812
  }
800
813
  return l;
801
814
  }
802
- /** Return a string representing the hierachical node path, e.g. "a/b/c".
815
+ /** Return a string representing the hierarchical node path, e.g. "a/b/c".
803
816
  * @param includeSelf
804
817
  * @param part property name or callback
805
818
  * @param separator
@@ -809,10 +822,6 @@ export class WunderbaumNode {
809
822
  part: keyof WunderbaumNode | NodeAnyCallback = "title",
810
823
  separator: string = "/"
811
824
  ) {
812
- // includeSelf = includeSelf !== false;
813
- // part = part || "title";
814
- // separator = separator || "/";
815
-
816
825
  let val;
817
826
  const path: string[] = [];
818
827
  const isFunc = typeof part === "function";
@@ -829,7 +838,7 @@ export class WunderbaumNode {
829
838
  return path.join(separator);
830
839
  }
831
840
 
832
- /** Return the preceeding node (under the same parent) or null. */
841
+ /** Return the preceding node (under the same parent) or null. */
833
842
  getPrevSibling(): WunderbaumNode | null {
834
843
  const ac = this.parent.children!;
835
844
  const idx = ac.indexOf(this);
@@ -861,7 +870,7 @@ export class WunderbaumNode {
861
870
  return this.classes ? this.classes.has(className) : false;
862
871
  }
863
872
 
864
- /** Return true if node ist the currently focused node. @since 0.9.0 */
873
+ /** Return true if node is the currently focused node. @since 0.9.0 */
865
874
  hasFocus(): boolean {
866
875
  return this.tree.focusNode === this;
867
876
  }
@@ -922,7 +931,7 @@ export class WunderbaumNode {
922
931
  * an expand operation is currently possible.
923
932
  */
924
933
  isExpandable(andCollapsed = false): boolean {
925
- // `false` is never expandable (unoffical)
934
+ // `false` is never expandable (unofficial)
926
935
  if ((andCollapsed && this.expanded) || <any>this.children === false) {
927
936
  return false;
928
937
  }
@@ -987,7 +996,7 @@ export class WunderbaumNode {
987
996
  return other && other.parent === this;
988
997
  }
989
998
 
990
- /** (experimental) Return true if this node is partially loaded. */
999
+ /** Return true if this node is partially loaded. @experimental */
991
1000
  isPartload(): boolean {
992
1001
  return !!this._partload;
993
1002
  }
@@ -997,12 +1006,12 @@ export class WunderbaumNode {
997
1006
  return !this.selected && !!this._partsel;
998
1007
  }
999
1008
 
1000
- /** Return true if this node has DOM representaion, i.e. is displayed in the viewport. */
1009
+ /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */
1001
1010
  isRadio(): boolean {
1002
1011
  return !!this.parent.radiogroup || this.getOption("checkbox") === "radio";
1003
1012
  }
1004
1013
 
1005
- /** Return true if this node has DOM representaion, i.e. is displayed in the viewport. */
1014
+ /** Return true if this node has DOM representation, i.e. is displayed in the viewport. */
1006
1015
  isRendered(): boolean {
1007
1016
  return !!this._rowElem;
1008
1017
  }
@@ -1509,12 +1518,12 @@ export class WunderbaumNode {
1509
1518
  * e.g. `ArrowLeft` = 'left'.
1510
1519
  * @param options
1511
1520
  */
1512
- async navigate(where: string, options?: NavigateOptions) {
1521
+ async navigate(where: NavigationType | string, options?: NavigateOptions) {
1513
1522
  // Allow to pass 'ArrowLeft' instead of 'left'
1514
- where = KEY_TO_ACTION_DICT[where] || where;
1523
+ const navType = (KEY_TO_NAVIGATION_MAP[where] ?? where) as NavigationType;
1515
1524
 
1516
1525
  // Otherwise activate or focus the related node
1517
- const node = this.findRelatedNode(where);
1526
+ const node = this.findRelatedNode(navType);
1518
1527
  if (!node) {
1519
1528
  this.logWarn(`Could not find related node '${where}'.`);
1520
1529
  return Promise.resolve(this);
@@ -1618,91 +1627,19 @@ export class WunderbaumNode {
1618
1627
  }
1619
1628
 
1620
1629
  protected _createIcon(
1621
- iconMap: any,
1622
1630
  parentElem: HTMLElement,
1623
1631
  replaceChild: HTMLElement | null,
1624
1632
  showLoading: boolean
1625
1633
  ): HTMLElement | null {
1626
- let iconSpan;
1627
- let icon = this.getOption("icon");
1628
- if (this._errorInfo) {
1629
- icon = iconMap.error;
1630
- } else if (this._isLoading && showLoading) {
1631
- // Status nodes, or nodes without expander (< minExpandLevel) should
1632
- // display the 'loading' status with the i.wb-icon span
1633
- icon = iconMap.loading;
1634
- }
1635
- if (icon === false) {
1636
- return null; // explicitly disabled: don't try default icons
1637
- }
1638
- if (typeof icon === "string") {
1639
- // Callback returned an icon definition
1640
- // icon = icon.trim()
1641
- } else if (this.statusNodeType) {
1642
- icon = (<any>iconMap)[this.statusNodeType];
1643
- } else if (this.expanded) {
1644
- icon = iconMap.folderOpen;
1645
- } else if (this.children) {
1646
- icon = iconMap.folder;
1647
- } else if (this.lazy) {
1648
- icon = iconMap.folderLazy;
1649
- } else {
1650
- icon = iconMap.doc;
1651
- }
1652
-
1653
- // this.log("_createIcon: " + icon);
1654
- if (!icon) {
1655
- iconSpan = document.createElement("i");
1656
- iconSpan.className = "wb-icon";
1657
- } else if (icon.indexOf("<") >= 0) {
1658
- // HTML
1659
- iconSpan = util.elemFromHtml(icon);
1660
- } else if (TEST_IMG.test(icon)) {
1661
- // Image URL
1662
- iconSpan = util.elemFromHtml(
1663
- `<i class="wb-icon" style="background-image: url('${icon}');">`
1664
- );
1665
- } else {
1666
- // Class name
1667
- iconSpan = document.createElement("i");
1668
- iconSpan.className = "wb-icon " + icon;
1669
- }
1670
- if (replaceChild) {
1671
- parentElem.replaceChild(iconSpan, replaceChild);
1672
- } else {
1673
- parentElem.appendChild(iconSpan);
1674
- }
1675
-
1676
- // Event handler `tree.iconBadge` can return a badge text or HTMLSpanElement
1677
-
1678
- const cbRes = this._callEvent("iconBadge", { iconSpan: iconSpan });
1679
- let badge = null;
1680
- if (cbRes != null && cbRes !== false) {
1681
- let classes = "";
1682
- let tooltip = "";
1683
- if (util.isPlainObject(cbRes)) {
1684
- badge = "" + cbRes.badge;
1685
- classes = cbRes.badgeClass ? " " + cbRes.badgeClass : "";
1686
- tooltip = cbRes.badgeTooltip ? ` title="${cbRes.badgeTooltip}"` : "";
1687
- } else if (typeof cbRes === "number") {
1688
- badge = "" + cbRes;
1634
+ const iconElem = this.tree._createNodeIcon(this, showLoading, true);
1635
+ if (iconElem) {
1636
+ if (replaceChild) {
1637
+ parentElem.replaceChild(iconElem, replaceChild);
1689
1638
  } else {
1690
- badge = cbRes; // string or HTMLSpanElement
1691
- }
1692
- if (typeof badge === "string") {
1693
- badge = util.elemFromHtml(
1694
- `<span class="wb-badge${classes}"${tooltip}>${util.escapeHtml(
1695
- badge
1696
- )}</span>`
1697
- );
1698
- }
1699
- if (badge) {
1700
- iconSpan.append(<HTMLSpanElement>badge);
1639
+ parentElem.appendChild(iconElem);
1701
1640
  }
1702
1641
  }
1703
-
1704
- // this.log("_createIcon: ", iconSpan);
1705
- return iconSpan;
1642
+ return iconElem;
1706
1643
  }
1707
1644
 
1708
1645
  /**
@@ -1712,7 +1649,7 @@ export class WunderbaumNode {
1712
1649
  protected _render_markup(opts: RenderOptions) {
1713
1650
  const tree = this.tree;
1714
1651
  const treeOptions = tree.options;
1715
- const rowHeight = treeOptions.rowHeightPx!;
1652
+ const rowHeight = treeOptions.rowHeightPx;
1716
1653
  const checkbox = this.getOption("checkbox");
1717
1654
  const columns = tree.columns;
1718
1655
  const level = this.getLevel();
@@ -1773,12 +1710,7 @@ export class WunderbaumNode {
1773
1710
 
1774
1711
  // Render the icon (show a 'loading' icon if we do not have an expander that
1775
1712
  // we would prefer).
1776
- const iconSpan = this._createIcon(
1777
- tree.iconMap,
1778
- nodeElem,
1779
- null,
1780
- !expanderSpan
1781
- );
1713
+ const iconSpan = this._createIcon(nodeElem, null, !expanderSpan);
1782
1714
  if (iconSpan) {
1783
1715
  ofsTitlePx += ICON_WIDTH;
1784
1716
  }
@@ -1938,11 +1870,11 @@ export class WunderbaumNode {
1938
1870
  const rowDiv = this._rowElem!;
1939
1871
 
1940
1872
  // Row markup already exists
1941
- const nodeElem = rowDiv.querySelector("span.wb-node") as HTMLSpanElement;
1942
- const expanderSpan = nodeElem.querySelector(
1873
+ const nodeSpan = rowDiv.querySelector("span.wb-node") as HTMLSpanElement;
1874
+ const expanderElem = nodeSpan.querySelector(
1943
1875
  "i.wb-expander"
1944
1876
  ) as HTMLLIElement;
1945
- const checkboxSpan = nodeElem.querySelector(
1877
+ const checkboxElem = nodeSpan.querySelector(
1946
1878
  "i.wb-checkbox"
1947
1879
  ) as HTMLLIElement;
1948
1880
 
@@ -1975,7 +1907,7 @@ export class WunderbaumNode {
1975
1907
  rowDiv.classList.add(...typeInfo.classes);
1976
1908
  }
1977
1909
 
1978
- if (expanderSpan) {
1910
+ if (expanderElem) {
1979
1911
  let image = null;
1980
1912
  if (this._isLoading) {
1981
1913
  image = iconMap.loading;
@@ -1990,14 +1922,17 @@ export class WunderbaumNode {
1990
1922
  }
1991
1923
 
1992
1924
  if (image == null) {
1993
- expanderSpan.classList.add("wb-indent");
1994
- } else if (TEST_IMG.test(image)) {
1995
- expanderSpan.style.backgroundImage = `url('${image}')`;
1925
+ expanderElem.className = "wb-expander";
1926
+ expanderElem.classList.add("wb-indent");
1927
+ } else if (TEST_HTML.test(image)) {
1928
+ expanderElem.replaceWith(util.elemFromHtml(image));
1929
+ } else if (TEST_FILE_PATH.test(image)) {
1930
+ expanderElem.style.backgroundImage = `url('${image}')`;
1996
1931
  } else {
1997
- expanderSpan.className = "wb-expander " + image;
1932
+ expanderElem.className = "wb-expander " + image;
1998
1933
  }
1999
1934
  }
2000
- if (checkboxSpan) {
1935
+ if (checkboxElem) {
2001
1936
  let cbclass = "wb-checkbox ";
2002
1937
  if (this.isRadio()) {
2003
1938
  cbclass += "wb-radio ";
@@ -2017,7 +1952,7 @@ export class WunderbaumNode {
2017
1952
  cbclass += iconMap.checkUnchecked;
2018
1953
  }
2019
1954
  }
2020
- checkboxSpan.className = cbclass;
1955
+ checkboxElem.className = cbclass;
2021
1956
  }
2022
1957
  // Fix active cell in cell-nav mode
2023
1958
  if (!opts.isNew) {
@@ -2027,9 +1962,9 @@ export class WunderbaumNode {
2027
1962
  colSpan.classList.remove("wb-error", "wb-invalid");
2028
1963
  }
2029
1964
  // Update icon (if not opts.isNew, which would rebuild markup anyway)
2030
- const iconSpan = nodeElem.querySelector("i.wb-icon") as HTMLElement;
1965
+ const iconSpan = nodeSpan.querySelector("i.wb-icon") as HTMLElement;
2031
1966
  if (iconSpan) {
2032
- this._createIcon(tree.iconMap, nodeElem, iconSpan, !expanderSpan);
1967
+ this._createIcon(nodeSpan, iconSpan, !expanderElem);
2033
1968
  }
2034
1969
  }
2035
1970
  // Adjust column width
@@ -2289,6 +2224,7 @@ export class WunderbaumNode {
2289
2224
  async setExpanded(flag: boolean = true, options?: SetExpandedOptions) {
2290
2225
  const { force, scrollIntoView, immediate, resetLazy } = options ?? {};
2291
2226
  const sendEvents = !options?.noEvents; // Default: send events
2227
+
2292
2228
  if (
2293
2229
  !flag &&
2294
2230
  this.isExpanded() &&
@@ -2355,6 +2291,32 @@ export class WunderbaumNode {
2355
2291
  setKey(key: string | null, refKey: string | null) {
2356
2292
  throw new Error("Not yet implemented");
2357
2293
  }
2294
+ // /**
2295
+ // * Calculate a *stable*, unique key for this node from its refKey (or title).
2296
+ // * We also add information from the parent, because a refKey may occur multiple
2297
+ // * times in a tree.
2298
+ // */
2299
+ // calcUniqueKey() {
2300
+ // // Assuming that the parent's key was calculated the same way, we implicitly
2301
+ // // involve the whole refKey-path:
2302
+ // const s = this.key + (this.refKey || this.title);
2303
+ // // 32-bit has a high probability of collisions, so we pump up to 64-bit
2304
+ // // https://security.stackexchange.com/q/209882/207588
2305
+ // const h1 = util.murmurHash3(s, true);
2306
+ // return "id_" + h1 + util.murmurHash3(h1 + s, true);
2307
+ // // const l = [];
2308
+ // // // eslint-disable-next-line @typescript-eslint/no-this-alias
2309
+ // // let node: WunderbaumNode = this;
2310
+ // // while (node.parent) {
2311
+ // // l.unshift(node.refKey || node.key);
2312
+ // // node = node.parent;
2313
+ // // }
2314
+ // // const path = l.join("/");
2315
+ // // 32-bit has a high probability of collisions, so we pump up to 64-bit
2316
+ // // https://security.stackexchange.com/q/209882/207588
2317
+ // // const h1 = util.murmurHash3(path, true);
2318
+ // // return "id_" + h1 + util.murmurHash3(h1 + path, true);
2319
+ // }
2358
2320
 
2359
2321
  /**
2360
2322
  * Trigger a repaint, typically after a status or data change.
@@ -2392,6 +2354,24 @@ export class WunderbaumNode {
2392
2354
  return nodeList;
2393
2355
  }
2394
2356
 
2357
+ /**
2358
+ * Return an array of refKey values.
2359
+ *
2360
+ * RefKeys are unique identifiers for a node data, and are used to identify
2361
+ * clones.
2362
+ * If more than one node has the same refKey, it is only returned once.
2363
+ * @param selected if true, only return refKeys of selected nodes.
2364
+ */
2365
+ getRefKeys(selected = false): string[] {
2366
+ const refKeys = new Set<string>();
2367
+ this.visit((node) => {
2368
+ if (node.refKey != null && (!selected || node.selected)) {
2369
+ refKeys.add(node.refKey);
2370
+ }
2371
+ });
2372
+ return Array.from(refKeys);
2373
+ }
2374
+
2395
2375
  /** Toggle the check/uncheck state. */
2396
2376
  toggleSelected(options?: SetSelectedOptions): TristateType {
2397
2377
  let flag = this.isSelected();
@@ -2593,9 +2573,11 @@ export class WunderbaumNode {
2593
2573
  this.selected = flag;
2594
2574
  if (selectMode === "hier") {
2595
2575
  this.fixSelection3AfterClick();
2596
- } else if (selectMode === "single") {
2576
+ } else if (selectMode === "single" && flag) {
2597
2577
  tree.visit((n) => {
2598
- n.selected = false;
2578
+ if (n !== this) {
2579
+ n.selected = false;
2580
+ }
2599
2581
  });
2600
2582
  }
2601
2583
  }
@@ -2641,7 +2623,7 @@ export class WunderbaumNode {
2641
2623
  );
2642
2624
 
2643
2625
  statusNode = this.addNode(data, "prependChild");
2644
- statusNode.match = true;
2626
+ statusNode.match = -1; // Mark as 'match' to avoid hiding
2645
2627
  tree.update(ChangeType.structure);
2646
2628
 
2647
2629
  return statusNode;
@@ -2718,35 +2700,19 @@ export class WunderbaumNode {
2718
2700
  this.update();
2719
2701
  }
2720
2702
 
2721
- _sortChildren(cmp: SortCallback, deep: boolean): void {
2722
- const cl = this.children;
2723
-
2724
- if (!cl) {
2725
- return;
2726
- }
2727
- cl.sort(cmp);
2728
- if (deep) {
2729
- for (let i = 0, l = cl.length; i < l; i++) {
2730
- if (cl[i].children) {
2731
- cl[i]._sortChildren(cmp, deep);
2732
- }
2733
- }
2734
- }
2735
- }
2736
-
2737
2703
  /**
2738
2704
  * Sort child list by title or custom criteria.
2739
2705
  * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1
2740
2706
  * (defaults to sorting by title).
2741
2707
  * @param {boolean} deep pass true to sort all descendant nodes recursively
2708
+ * @deprecated use {@link sort}
2742
2709
  */
2743
2710
  sortChildren(
2744
2711
  cmp: SortCallback | null = nodeTitleSorter,
2745
2712
  deep: boolean = false
2746
2713
  ): void {
2747
- this._sortChildren(cmp || nodeTitleSorter, deep);
2748
- this.tree.update(ChangeType.structure);
2749
- // this.triggerModify("sort"); // TODO
2714
+ this.tree.logDeprecate("node.sortChildren()", { since: "0.14.0" });
2715
+ return this.sort({ cmp: cmp ? cmp : undefined, deep: deep });
2750
2716
  }
2751
2717
 
2752
2718
  /**
@@ -2771,82 +2737,159 @@ export class WunderbaumNode {
2771
2737
  /**
2772
2738
  * Convenience method to implement column sorting.
2773
2739
  * @since 0.11.0
2740
+ * @deprecated use {@link sort}
2774
2741
  */
2775
2742
  sortByProperty(options: SortByPropertyOptions) {
2776
- const {
2777
- caseInsensitive = true,
2743
+ this.tree.logDeprecate("node.sortByProperty()", { since: "0.14.0" });
2744
+ return this.sort(options);
2745
+ }
2746
+
2747
+ /**
2748
+ * Implement column sorting.
2749
+ * @since 0.14.0
2750
+ */
2751
+ sort(options: SortOptions) {
2752
+ const tree = this.tree;
2753
+ let {
2754
+ propName = undefined,
2778
2755
  deep = true,
2779
- nativeOrderPropName = "_nativeIndex",
2756
+ key = undefined,
2757
+ order = undefined,
2758
+ caseInsensitive = true,
2759
+ cmp = undefined,
2760
+ // Support click on column sort header:
2780
2761
  updateColInfo = false,
2762
+ nativeOrderPropName = "_nativeIndex",
2763
+ colId = undefined,
2781
2764
  } = options;
2782
2765
 
2783
- let order: SortOrderType;
2784
- let colDef: ColumnDefinition | null;
2766
+ propName ??= colId;
2767
+ if (propName === "*") {
2768
+ propName = "title";
2769
+ }
2770
+
2771
+ const isFolder =
2772
+ tree.options.sortFoldersFirst === true
2773
+ ? (node: WunderbaumNode) =>
2774
+ node.hasChildren() !== false || node.type === NODE_TYPE_FOLDER
2775
+ : tree.options.sortFoldersFirst;
2785
2776
 
2786
2777
  if (updateColInfo) {
2787
- colDef = this.tree["_columnsById"][options.colId!];
2778
+ const colDef = this.tree["_columnsById"][options.colId!];
2788
2779
  util.assert(colDef, `Invalid colId specified: ${options.colId}`);
2789
- order =
2790
- options.order ??
2791
- util.rotate(colDef!.sortOrder, ["asc", "desc", undefined]);
2780
+ order ??= util.rotate(colDef.sortOrder, ["asc", "desc", undefined]);
2792
2781
 
2793
2782
  for (const col of this.tree.columns) {
2794
2783
  col.sortOrder = col === colDef ? order : undefined;
2795
2784
  }
2796
-
2785
+ if (order === undefined) {
2786
+ propName = nativeOrderPropName;
2787
+ order = "asc";
2788
+ }
2797
2789
  this.tree.update(ChangeType.colStructure);
2798
2790
  } else {
2799
- order = options.order ?? "asc";
2791
+ propName ??= "title";
2792
+ order ??= "asc";
2800
2793
  }
2801
2794
 
2802
- let propName = options.propName ?? (options.colId || "");
2803
- if (propName === "*") {
2804
- propName = "title";
2805
- }
2806
- if (order == null) {
2807
- propName = nativeOrderPropName;
2808
- order = "asc";
2809
- }
2810
- this.logDebug(`sortByProperty(), propName=${propName}, ${order}`, options);
2811
- util.assert(propName, "No property name specified");
2795
+ this.logDebug(`sort(), propName=${propName}, ${order}`, options);
2796
+ util.assert(propName || cmp || key, "No `propName` or `key` specified");
2812
2797
 
2813
- const cmp = (a: WunderbaumNode, b: WunderbaumNode) => {
2814
- let av, bv;
2815
- if (NODE_DICT_PROPS.has(<string>propName)) {
2816
- av = a[propName as keyof WunderbaumNode];
2817
- bv = b[propName as keyof WunderbaumNode];
2818
- } else {
2819
- av = a.data[propName];
2820
- bv = b.data[propName];
2821
- }
2822
- if (av == null && bv == null) {
2823
- return 0;
2824
- }
2825
- if (av == null) {
2826
- av = typeof bv === "string" ? "" : 0;
2827
- } else if (typeof av === "boolean") {
2828
- av = av ? 1 : 0;
2829
- }
2830
- if (bv == null) {
2831
- bv = typeof av === "string" ? "" : 0;
2832
- } else if (typeof bv === "boolean") {
2833
- bv = bv ? 1 : 0;
2798
+ // Define a key callback from the parameters we have
2799
+ if (key == null && cmp == null) {
2800
+ key = (node) => {
2801
+ let val;
2802
+ if (NODE_DICT_PROPS.has(<string>propName)) {
2803
+ val = node[propName as keyof WunderbaumNode];
2804
+ } else {
2805
+ val = node.data[propName!];
2806
+ }
2807
+ if (caseInsensitive && typeof val === "string") {
2808
+ val = val.toLowerCase();
2809
+ }
2810
+ return val;
2811
+ };
2812
+ }
2813
+ // Define a compare callback that uses the key callback
2814
+ if (cmp) {
2815
+ util.assert(!key, "`key` and `cmp` are mutually exclusive");
2816
+ tree.logDeprecate("SortOptions.cmp", {
2817
+ since: "0.14.0",
2818
+ hint: "use the `key` callback instead",
2819
+ });
2820
+ } else {
2821
+ if (options.propName || options.caseInsensitive) {
2822
+ tree.logWarn("sort(): ignoring propName, caseInsensitive");
2834
2823
  }
2835
- if (caseInsensitive) {
2836
- if (typeof av === "string") {
2837
- av = av.toLowerCase();
2824
+
2825
+ cmp = (a, b) => {
2826
+ if (isFolder) {
2827
+ const isFolderA = isFolder(a);
2828
+ if (isFolderA !== isFolder(b)) {
2829
+ return isFolderA ? -1 : 1;
2830
+ }
2831
+ }
2832
+ let x = key!(a);
2833
+ let y = key!(b);
2834
+ // Assure we have reasonable comparisons with null values:
2835
+ if (x == null) {
2836
+ x = typeof y === "string" ? "" : 0;
2837
+ } else if (typeof x === "boolean") {
2838
+ x = x ? 1 : 0;
2839
+ }
2840
+ if (y == null) {
2841
+ y = typeof x === "string" ? "" : 0;
2842
+ } else if (typeof y === "boolean") {
2843
+ y = y ? 1 : 0;
2838
2844
  }
2839
- if (typeof bv === "string") {
2840
- bv = bv.toLowerCase();
2845
+
2846
+ if (order === "desc") {
2847
+ return x === y ? 0 : x > y ? -1 : 1;
2841
2848
  }
2849
+ return x === y ? 0 : x > y ? 1 : -1;
2850
+ };
2851
+ }
2852
+
2853
+ function _sortChildren(cl: WunderbaumNode[]): void {
2854
+ if (!cl) {
2855
+ return;
2842
2856
  }
2843
- if (order === "desc") {
2844
- return av === bv ? 0 : av > bv ? -1 : 1;
2857
+ cl.sort(cmp);
2858
+ if (deep) {
2859
+ for (let i = 0, l = cl.length; i < l; i++) {
2860
+ if (cl[i].children) {
2861
+ _sortChildren(cl[i].children!);
2862
+ }
2863
+ }
2845
2864
  }
2846
- return av === bv ? 0 : av > bv ? 1 : -1;
2847
- };
2865
+ }
2866
+ if (this.children) {
2867
+ _sortChildren(this.children);
2868
+ }
2869
+ this.tree.update(ChangeType.structure);
2870
+ // this.triggerModify("sort"); // TODO
2871
+ }
2848
2872
 
2849
- return this.sortChildren(cmp, deep);
2873
+ /**
2874
+ * Re-apply current sorting if any (use after lazy load).
2875
+ * Example:
2876
+ * ```js
2877
+ * load: function (e) {
2878
+ * // Whe loading a lazy branch, apply current sort order if any
2879
+ * e.node.resort();
2880
+ * },
2881
+ * ```
2882
+ * @since 0.14.0
2883
+ */
2884
+ resort(options: SortOptions = {}): void {
2885
+ for (const colDef of this.tree.columns) {
2886
+ if (colDef.sortOrder) {
2887
+ options.colId = colDef.id;
2888
+ options.order = colDef.sortOrder;
2889
+ this.sort(options);
2890
+ break;
2891
+ }
2892
+ }
2850
2893
  }
2851
2894
 
2852
2895
  /**
@@ -2893,7 +2936,8 @@ export class WunderbaumNode {
2893
2936
  * @param {function} callback the callback function.
2894
2937
  * Return false to stop iteration, return "skip" to skip this node and
2895
2938
  * its children only.
2896
- * @see {@link IterableIterator<WunderbaumNode>}, {@link Wunderbaum.visit}.
2939
+ * @see `wb_node.WunderbaumNode.IterableIterator<WunderbaumNode>`
2940
+ * @see {@link Wunderbaum.visit}.
2897
2941
  */
2898
2942
  visit(
2899
2943
  callback: NodeVisitCallback,