wunderbaum 0.2.0 → 0.3.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.
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * Wunderbaum - wb_extension_base
3
- * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
3
+ * Copyright (c) 2021-2023, Martin Wendt. Released under the MIT license.
4
4
  * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
5
5
  */
6
6
 
package/src/wb_node.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * Wunderbaum - wunderbaum_node
3
- * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
3
+ * Copyright (c) 2021-2023, Martin Wendt. Released under the MIT license.
4
4
  * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
5
5
  */
6
6
 
@@ -10,7 +10,7 @@ import * as util from "./util";
10
10
  import { Wunderbaum } from "./wunderbaum";
11
11
  import {
12
12
  AddChildrenOptions,
13
- AddNodeType,
13
+ InsertNodeType,
14
14
  ApplyCommandOptions,
15
15
  ApplyCommandType,
16
16
  ChangeType,
@@ -30,6 +30,7 @@ import {
30
30
  SetExpandedOptions,
31
31
  SetSelectedOptions,
32
32
  SetStatusOptions,
33
+ SortCallback,
33
34
  } from "./types";
34
35
  import {
35
36
  iconMap,
@@ -41,6 +42,7 @@ import {
41
42
  ROW_HEIGHT,
42
43
  TEST_IMG,
43
44
  inflateSourceData,
45
+ nodeTitleSorter,
44
46
  } from "./common";
45
47
  import { Deferred } from "./deferred";
46
48
  import { WbNodeData } from "./wb_options";
@@ -332,9 +334,12 @@ export class WunderbaumNode {
332
334
  * @param [mode=child] 'before', 'after', 'firstChild', or 'child' ('over' is a synonym for 'child')
333
335
  * @returns new node
334
336
  */
335
- addNode(nodeData: WbNodeData, mode = "child"): WunderbaumNode {
336
- if (mode === "over") {
337
- mode = "child"; // compatible with drop region
337
+ addNode(
338
+ nodeData: WbNodeData,
339
+ mode: InsertNodeType = "appendChild"
340
+ ): WunderbaumNode {
341
+ if (<string>mode === "over") {
342
+ mode = "appendChild"; // compatible with drop region
338
343
  }
339
344
  switch (mode) {
340
345
  case "after":
@@ -343,11 +348,11 @@ export class WunderbaumNode {
343
348
  });
344
349
  case "before":
345
350
  return this.parent.addChildren(nodeData, { before: this });
346
- case "firstChild":
351
+ case "prependChild":
347
352
  // Insert before the first child if any
348
353
  // let insertBefore = this.children ? this.children[0] : undefined;
349
354
  return this.addChildren(nodeData, { before: 0 });
350
- case "child":
355
+ case "appendChild":
351
356
  return this.addChildren(nodeData);
352
357
  }
353
358
  util.assert(false, "Invalid mode: " + mode);
@@ -784,8 +789,17 @@ export class WunderbaumNode {
784
789
  * an expand operation is currently possible.
785
790
  */
786
791
  isExpandable(andCollapsed = false): boolean {
787
- // return !!this.children && (!this.expanded || !andCollapsed);
788
- return !!(this.children || this.lazy) && (!this.expanded || !andCollapsed);
792
+ // `false` is never expandable (unoffical)
793
+ if ((andCollapsed && this.expanded) || <any>this.children === false) {
794
+ return false;
795
+ }
796
+ if (this.children == null) {
797
+ return this.lazy; // null or undefined can trigger lazy load
798
+ }
799
+ if (this.children.length === 0) {
800
+ return !!this.tree.options.emptyChildListExpandable;
801
+ }
802
+ return true;
789
803
  }
790
804
 
791
805
  /** Return true if this node is currently in edit-title mode. */
@@ -953,7 +967,7 @@ export class WunderbaumNode {
953
967
  tree.logInfo("Redefine columns", source.columns);
954
968
  tree.columns = source.columns;
955
969
  delete source.columns;
956
- tree.updateColumns({ calculateCols: false });
970
+ tree.setModified(ChangeType.colStructure);
957
971
  }
958
972
 
959
973
  this.addChildren(source.children);
@@ -969,12 +983,50 @@ export class WunderbaumNode {
969
983
  this._callEvent("load");
970
984
  }
971
985
 
986
+ async _fetchWithOptions(source: any) {
987
+ // Either a URL string or an object with a `.url` property.
988
+ let url: string, params, body, options, rest;
989
+ let fetchOpts: any = {};
990
+ if (typeof source === "string") {
991
+ // source is a plain URL string: assume GET request
992
+ url = source;
993
+ fetchOpts.method = "GET";
994
+ } else if (util.isPlainObject(source)) {
995
+ // source is a plain object with `.url` property.
996
+ ({ url, params, body, options, ...rest } = source);
997
+ util.assert(typeof url === "string", `expected source.url as string`);
998
+ if (util.isPlainObject(options)) {
999
+ fetchOpts = options;
1000
+ }
1001
+ if (util.isPlainObject(body)) {
1002
+ // we also accept 'body' as object...
1003
+ util.assert(
1004
+ !fetchOpts.body,
1005
+ "options.body should be passed as source.body"
1006
+ );
1007
+ fetchOpts.body = JSON.stringify(fetchOpts.body);
1008
+ fetchOpts.method ??= "POST"; // set default
1009
+ }
1010
+ if (util.isPlainObject(params)) {
1011
+ url += "?" + new URLSearchParams(params);
1012
+ fetchOpts.method ??= "GET"; // set default
1013
+ }
1014
+ } else {
1015
+ url = ""; // keep linter happy
1016
+ util.error(`Unsupported source format: ${source}`);
1017
+ }
1018
+ this.setStatus(NodeStatusType.loading);
1019
+ const response = await fetch(url, fetchOpts);
1020
+ if (!response.ok) {
1021
+ util.error(`GET ${url} returned ${response.status}, ${response}`);
1022
+ }
1023
+ return await response.json();
1024
+ }
972
1025
  /** Download data from the cloud, then call `.update()`. */
973
1026
  async load(source: any) {
974
1027
  const tree = this.tree;
975
1028
  const requestId = Date.now();
976
1029
  const prevParent = this.parent;
977
- const url = typeof source === "string" ? source : source.url;
978
1030
  const start = Date.now();
979
1031
  let elap = 0,
980
1032
  elapLoad = 0,
@@ -992,16 +1044,16 @@ export class WunderbaumNode {
992
1044
  // const timerLabel = tree.logTime(this + ".load()");
993
1045
 
994
1046
  try {
1047
+ let url: string = typeof source === "string" ? source : source.url;
995
1048
  if (!url) {
1049
+ // An array or a plain object (that does NOT contain a `.url` property)
1050
+ // will be treated as native Wunderbaum data
996
1051
  this._loadSourceObject(source);
997
1052
  elapProcess = Date.now() - start;
998
1053
  } else {
999
- this.setStatus(NodeStatusType.loading);
1000
- const response = await fetch(url, { method: "GET" });
1001
- if (!response.ok) {
1002
- util.error(`GET ${url} returned ${response.status}, ${response}`);
1003
- }
1004
- const data = await response.json();
1054
+ // Either a URL string or an object with a `.url` property.
1055
+ const data = await this._fetchWithOptions(source);
1056
+
1005
1057
  elapLoad = Date.now() - start;
1006
1058
 
1007
1059
  if (this._requestId && this._requestId > requestId) {
@@ -1169,9 +1221,12 @@ export class WunderbaumNode {
1169
1221
  /** Move this node to targetNode. */
1170
1222
  moveTo(
1171
1223
  targetNode: WunderbaumNode,
1172
- mode: AddNodeType = "appendChild",
1224
+ mode: InsertNodeType = "appendChild",
1173
1225
  map?: NodeAnyCallback
1174
1226
  ) {
1227
+ if (<string>mode === "over") {
1228
+ mode = "appendChild"; // compatible with drop region
1229
+ }
1175
1230
  if (mode === "prependChild") {
1176
1231
  if (targetNode.children && targetNode.children.length) {
1177
1232
  mode = "before";
@@ -1229,7 +1284,7 @@ export class WunderbaumNode {
1229
1284
  targetParent.children!.splice(pos + 1, 0, this);
1230
1285
  break;
1231
1286
  default:
1232
- util.error("Invalid mode " + mode);
1287
+ util.error(`Invalid mode '${mode}'.`);
1233
1288
  }
1234
1289
  } else {
1235
1290
  targetParent.children = [this];
@@ -1255,8 +1310,12 @@ export class WunderbaumNode {
1255
1310
  n.tree = targetNode.tree;
1256
1311
  }, true);
1257
1312
  }
1258
-
1259
- tree.setModified(ChangeType.structure);
1313
+ // Make sure we update async, because discarding the markup would prevent
1314
+ // DragAndDrop to generate a dragend event on the source node
1315
+ setTimeout(() => {
1316
+ // Even indentation may have changed:
1317
+ tree.setModified(ChangeType.any);
1318
+ }, 0);
1260
1319
  // TODO: fix selection state
1261
1320
  // TODO: fix active state
1262
1321
  }
@@ -1395,7 +1454,7 @@ export class WunderbaumNode {
1395
1454
  icon = iconMap.loading;
1396
1455
  }
1397
1456
  if (icon === false) {
1398
- return null;
1457
+ return null; // explicitly disabled: don't try default icons
1399
1458
  }
1400
1459
  if (typeof icon === "string") {
1401
1460
  // Callback returned an icon definition
@@ -1413,7 +1472,10 @@ export class WunderbaumNode {
1413
1472
  }
1414
1473
 
1415
1474
  // this.log("_createIcon: " + icon);
1416
- if (icon.indexOf("<") >= 0) {
1475
+ if (!icon) {
1476
+ iconSpan = document.createElement("i");
1477
+ iconSpan.className = "wb-icon";
1478
+ } else if (icon.indexOf("<") >= 0) {
1417
1479
  // HTML
1418
1480
  iconSpan = util.elemFromHtml(icon);
1419
1481
  } else if (TEST_IMG.test(icon)) {
@@ -1765,9 +1827,12 @@ export class WunderbaumNode {
1765
1827
  case "data":
1766
1828
  this._render_data(opts);
1767
1829
  break;
1768
- default:
1830
+ case "row":
1831
+ // _rowElem is not yet created (asserted in _render_markup)
1769
1832
  this._render_markup(opts);
1770
1833
  break;
1834
+ default:
1835
+ util.error(`Invalid change type '${opts.change}'.`);
1771
1836
  }
1772
1837
  }
1773
1838
 
@@ -2005,9 +2070,9 @@ export class WunderbaumNode {
2005
2070
  }
2006
2071
 
2007
2072
  /** Set a new icon path or class. */
2008
- setIcon() {
2009
- throw new Error("Not yet implemented");
2010
- // this.setModified();
2073
+ setIcon(icon: string) {
2074
+ this.icon = icon;
2075
+ this.setModified();
2011
2076
  }
2012
2077
 
2013
2078
  /** Change node's {@link key} and/or {@link refKey}. */
@@ -2016,11 +2081,15 @@ export class WunderbaumNode {
2016
2081
  }
2017
2082
 
2018
2083
  /**
2019
- * Schedule a render, typically called to update after a status or data change.
2084
+ * Trigger a repaint, typically after a status or data change.
2020
2085
  *
2021
2086
  * `change` defaults to 'data', which handles modifcations of title, icon,
2022
2087
  * and column content. It can be reduced to 'ChangeType.status' if only
2023
2088
  * active/focus/selected state has changed.
2089
+ *
2090
+ * This method will eventually call {@link WunderbaumNode.render()} with
2091
+ * default options, but may be more consistent with the tree's
2092
+ * {@link Wunderbaum.setModified()} API.
2024
2093
  */
2025
2094
  setModified(change: ChangeType = ChangeType.data) {
2026
2095
  util.assert(change === ChangeType.status || change === ChangeType.data);
@@ -2067,7 +2136,7 @@ export class WunderbaumNode {
2067
2136
  util.assert(data.statusNodeType);
2068
2137
  util.assert(!firstChild || !firstChild.isStatusNode());
2069
2138
 
2070
- statusNode = this.addNode(data, "firstChild");
2139
+ statusNode = this.addNode(data, "prependChild");
2071
2140
  statusNode.match = true;
2072
2141
  tree.setModified(ChangeType.structure);
2073
2142
 
@@ -2139,6 +2208,37 @@ export class WunderbaumNode {
2139
2208
  // this.triggerModify("rename"); // TODO
2140
2209
  }
2141
2210
 
2211
+ _sortChildren(cmp: SortCallback, deep: boolean): void {
2212
+ const cl = this.children;
2213
+
2214
+ if (!cl) {
2215
+ return;
2216
+ }
2217
+ cl.sort(cmp);
2218
+ if (deep) {
2219
+ for (let i = 0, l = cl.length; i < l; i++) {
2220
+ if (cl[i].children) {
2221
+ cl[i]._sortChildren(cmp, deep);
2222
+ }
2223
+ }
2224
+ }
2225
+ }
2226
+
2227
+ /**
2228
+ * Sort child list by title or custom criteria.
2229
+ * @param {function} cmp custom compare function(a, b) that returns -1, 0, or 1
2230
+ * (defaults to sorting by title).
2231
+ * @param {boolean} deep pass true to sort all descendant nodes recursively
2232
+ */
2233
+ sortChildren(
2234
+ cmp: SortCallback | null = nodeTitleSorter,
2235
+ deep: boolean = false
2236
+ ): void {
2237
+ this._sortChildren(cmp || nodeTitleSorter, deep);
2238
+ this.tree.setModified(ChangeType.structure);
2239
+ // this.triggerModify("sort"); // TODO
2240
+ }
2241
+
2142
2242
  /**
2143
2243
  * Trigger `modifyChild` event on a parent to signal that a child was modified.
2144
2244
  * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ...
@@ -2148,8 +2248,8 @@ export class WunderbaumNode {
2148
2248
  child: WunderbaumNode | null,
2149
2249
  extra?: any
2150
2250
  ) {
2251
+ this.logDebug(`modifyChild(${operation})`, extra, child);
2151
2252
  if (!this.tree.options.modifyChild) return;
2152
-
2153
2253
  if (child && child.parent !== this) {
2154
2254
  util.error("child " + child + " is not a child of " + this);
2155
2255
  }
package/src/wb_options.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * Wunderbaum - utils
3
- * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
3
+ * Copyright (c) 2021-2023, Martin Wendt. Released under the MIT license.
4
4
  * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
5
5
  */
6
6
 
@@ -44,7 +44,7 @@ export interface WbNodeData {
44
44
  * ```js
45
45
  * const tree = new mar10.Wunderbaum({
46
46
  * id: "demo",
47
- * element: document.querySelector("#demo-tree"),
47
+ * element: document.getElementById("demo-tree"),
48
48
  * source: "url/of/data/request",
49
49
  * ...
50
50
  * });
@@ -107,7 +107,7 @@ export interface WunderbaumOptions {
107
107
  * real data.
108
108
  * Default: false.
109
109
  */
110
- skeleton?: false;
110
+ skeleton?: boolean;
111
111
  /**
112
112
  * Translation map for some system messages.
113
113
  */
@@ -123,6 +123,12 @@ export interface WunderbaumOptions {
123
123
  * Default: 0
124
124
  */
125
125
  minExpandLevel?: number;
126
+ /**
127
+ * If true, allow to expand parent nodes, even if `node.children` conatains
128
+ * an empty array (`[]`). This is the the behavior of macOS Finder, for example.
129
+ * Default: false
130
+ */
131
+ emptyChildListExpandable?: boolean;
126
132
  // escapeTitles: boolean;
127
133
  // /**
128
134
  // * Height of the header row div.
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * Wunderbaum style sheet (generated from wunderbaum.scss)
3
- * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
3
+ * Copyright (c) 2021-2023, Martin Wendt. Released under the MIT license.
4
4
  * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
5
5
  */
6
6
 
@@ -52,11 +52,14 @@ $icon-padding-x: ($icon-outer-width - $icon-width) / 2;
52
52
  $header-height: $row-outer-height;
53
53
 
54
54
  // PyCharm:
55
- $level-rainbow: rgba(255, 255, 232, 1), rgba(240, 255, 240, 1),
56
- rgba(255, 240, 255, 1), rgba(234, 253, 253, 1);
55
+ // $level-rainbow: rgba(255, 255, 232, 1), rgba(240, 255, 240, 1),
56
+ // rgba(255, 240, 255, 1), rgba(234, 253, 253, 1);
57
57
  // VS-Code_
58
58
  // $level-rainbow: rgba(255, 255, 64, 0.07), rgba(127, 255, 127, 0.07),
59
59
  // rgba(255, 127, 255, 0.07), rgba(79, 236, 236, 0.07);
60
+ // Slightly stronger*
61
+ $level-rainbow: rgb(255, 255, 201), rgb(218, 255, 218), rgb(255, 217, 254),
62
+ rgb(204, 250, 250);
60
63
 
61
64
  div.wunderbaum {
62
65
  box-sizing: border-box;