wunderbaum 0.10.1 → 0.11.1

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/wunderbaum.ts CHANGED
@@ -47,6 +47,7 @@ import {
47
47
  SetActiveOptions,
48
48
  SetColumnOptions,
49
49
  SetStatusOptions,
50
+ SortByPropertyOptions,
50
51
  SortCallback,
51
52
  SourceType,
52
53
  UpdateOptions,
@@ -60,7 +61,7 @@ import {
60
61
  makeNodeTitleStartMatcher,
61
62
  nodeTitleSorter,
62
63
  RENDER_MAX_PREFETCH,
63
- ROW_HEIGHT,
64
+ DEFAULT_ROW_HEIGHT,
64
65
  } from "./common";
65
66
  import { WunderbaumNode } from "./wb_node";
66
67
  import { Deferred } from "./deferred";
@@ -196,7 +197,7 @@ export class Wunderbaum {
196
197
  debugLevel: DEFAULT_DEBUGLEVEL, // 0:quiet, 1:errors, 2:warnings, 3:info, 4:verbose
197
198
  header: null, // Show/hide header (pass bool or string)
198
199
  // headerHeightPx: ROW_HEIGHT,
199
- rowHeightPx: ROW_HEIGHT,
200
+ rowHeightPx: DEFAULT_ROW_HEIGHT,
200
201
  iconMap: "bootstrap",
201
202
  columns: null,
202
203
  types: null,
@@ -285,7 +286,7 @@ export class Wunderbaum {
285
286
  delete opts.types;
286
287
 
287
288
  // --- Create Markup
288
- this.element = util.elemFromSelector(opts.element) as HTMLDivElement;
289
+ this.element = util.elemFromSelector<HTMLDivElement>(opts.element)!;
289
290
  util.assert(!!this.element, `Invalid 'element' option: ${opts.element}`);
290
291
 
291
292
  this.element.classList.add("wunderbaum");
@@ -293,14 +294,23 @@ export class Wunderbaum {
293
294
  this.element.tabIndex = 0;
294
295
  }
295
296
 
297
+ if (opts.rowHeightPx !== DEFAULT_ROW_HEIGHT) {
298
+ this.element.style.setProperty(
299
+ "--wb-row-outer-height",
300
+ opts.rowHeightPx + "px"
301
+ );
302
+ this.element.style.setProperty(
303
+ "--wb-row-inner-height",
304
+ opts.rowHeightPx - 2 + "px"
305
+ );
306
+ }
296
307
  // Attach tree instance to <div>
297
308
  (<any>this.element)._wb_tree = this;
298
309
 
299
310
  // Create header markup, or take it from the existing html
300
311
 
301
- this.headerElement = this.element.querySelector(
302
- "div.wb-header"
303
- ) as HTMLDivElement;
312
+ this.headerElement =
313
+ this.element.querySelector<HTMLDivElement>("div.wb-header")!;
304
314
 
305
315
  const wantHeader =
306
316
  opts.header == null ? this.columns.length > 1 : !!opts.header;
@@ -312,9 +322,8 @@ export class Wunderbaum {
312
322
  "`opts.columns` must not be set if markup already contains a header"
313
323
  );
314
324
  this.columns = [];
315
- const rowElement = this.headerElement.querySelector(
316
- "div.wb-row"
317
- ) as HTMLDivElement;
325
+ const rowElement =
326
+ this.headerElement.querySelector<HTMLDivElement>("div.wb-row")!;
318
327
  for (const colDiv of rowElement.querySelectorAll("div")) {
319
328
  this.columns.push({
320
329
  id: colDiv.dataset.id || `col_${this.columns.length}`,
@@ -337,9 +346,7 @@ export class Wunderbaum {
337
346
  </div>`;
338
347
 
339
348
  if (!wantHeader) {
340
- const he = this.element.querySelector(
341
- "div.wb-header"
342
- ) as HTMLDivElement;
349
+ const he = this.element.querySelector<HTMLDivElement>("div.wb-header")!;
343
350
  he.style.display = "none";
344
351
  }
345
352
  }
@@ -349,15 +356,15 @@ export class Wunderbaum {
349
356
  <div class="wb-list-container">
350
357
  <div class="wb-node-list"></div>
351
358
  </div>`;
352
- this.listContainerElement = this.element.querySelector(
359
+ this.listContainerElement = this.element.querySelector<HTMLDivElement>(
353
360
  "div.wb-list-container"
354
- ) as HTMLDivElement;
355
- this.nodeListElement = this.listContainerElement.querySelector(
356
- "div.wb-node-list"
357
- ) as HTMLDivElement;
358
- this.headerElement = this.element.querySelector(
359
- "div.wb-header"
360
- ) as HTMLDivElement;
361
+ )!;
362
+ this.nodeListElement =
363
+ this.listContainerElement.querySelector<HTMLDivElement>(
364
+ "div.wb-node-list"
365
+ )!;
366
+ this.headerElement =
367
+ this.element.querySelector<HTMLDivElement>("div.wb-header")!;
361
368
 
362
369
  this.element.classList.toggle("wb-grid", this.columns.length > 1);
363
370
 
@@ -418,6 +425,17 @@ export class Wunderbaum {
418
425
  });
419
426
  this.resizeObserver.observe(this.element);
420
427
 
428
+ util.onEvent(this.element, "click", ".wb-button,.wb-col-icon", (e) => {
429
+ const info = Wunderbaum.getEventInfo(e);
430
+ const command = (<HTMLElement>e.target)?.dataset?.command;
431
+
432
+ this._callEvent("buttonClick", {
433
+ event: e,
434
+ info: info,
435
+ command: command,
436
+ });
437
+ });
438
+
421
439
  util.onEvent(this.nodeListElement, "click", "div.wb-row", (e) => {
422
440
  const info = Wunderbaum.getEventInfo(e);
423
441
  const node = info.node;
@@ -751,6 +769,7 @@ export class Wunderbaum {
751
769
 
752
770
  /** Return the topmost visible node in the viewport. */
753
771
  getTopmostVpNode(complete = true) {
772
+ const rowHeight = this.options.rowHeightPx!;
754
773
  const gracePx = 1; // ignore subpixel scrolling
755
774
  const scrollParent = this.element;
756
775
  // const headerHeight = this.headerElement.clientHeight; // May be 0
@@ -758,15 +777,16 @@ export class Wunderbaum {
758
777
  let topIdx: number;
759
778
 
760
779
  if (complete) {
761
- topIdx = Math.ceil((scrollTop - gracePx) / ROW_HEIGHT);
780
+ topIdx = Math.ceil((scrollTop - gracePx) / rowHeight);
762
781
  } else {
763
- topIdx = Math.floor(scrollTop / ROW_HEIGHT);
782
+ topIdx = Math.floor(scrollTop / rowHeight);
764
783
  }
765
784
  return this._getNodeByRowIdx(topIdx)!;
766
785
  }
767
786
 
768
787
  /** Return the lowest visible node in the viewport. */
769
788
  getLowestVpNode(complete = true) {
789
+ const rowHeight = this.options.rowHeightPx!;
770
790
  const scrollParent = this.element;
771
791
  const headerHeight = this.headerElement.clientHeight; // May be 0
772
792
  const scrollTop = scrollParent.scrollTop;
@@ -774,9 +794,9 @@ export class Wunderbaum {
774
794
  let bottomIdx: number;
775
795
 
776
796
  if (complete) {
777
- bottomIdx = Math.floor((scrollTop + clientHeight) / ROW_HEIGHT) - 1;
797
+ bottomIdx = Math.floor((scrollTop + clientHeight) / rowHeight) - 1;
778
798
  } else {
779
- bottomIdx = Math.ceil((scrollTop + clientHeight) / ROW_HEIGHT) - 1;
799
+ bottomIdx = Math.ceil((scrollTop + clientHeight) / rowHeight) - 1;
780
800
  }
781
801
  bottomIdx = Math.min(bottomIdx, this.count(true) - 1);
782
802
  return this._getNodeByRowIdx(bottomIdx)!;
@@ -1078,7 +1098,7 @@ export class Wunderbaum {
1078
1098
 
1079
1099
  /** Run code, but defer rendering of viewport until done.
1080
1100
  *
1081
- * ```
1101
+ * ```js
1082
1102
  * tree.runWithDeferredUpdate(() => {
1083
1103
  * return someFuncThatWouldUpdateManyNodes();
1084
1104
  * });
@@ -1274,9 +1294,10 @@ export class Wunderbaum {
1274
1294
  * @param includeHidden Not yet implemented
1275
1295
  */
1276
1296
  findRelatedNode(node: WunderbaumNode, where: string, includeHidden = false) {
1297
+ const rowHeight = this.options.rowHeightPx!;
1277
1298
  let res = null;
1278
1299
  const pageSize = Math.floor(
1279
- this.listContainerElement.clientHeight / ROW_HEIGHT
1300
+ this.listContainerElement.clientHeight / rowHeight
1280
1301
  );
1281
1302
 
1282
1303
  switch (where) {
@@ -1602,7 +1623,7 @@ export class Wunderbaum {
1602
1623
  }
1603
1624
  }
1604
1625
 
1605
- /** Reset column widths to default. */
1626
+ /** Reset column widths to default. @since 0.10.0 */
1606
1627
  resetColumns() {
1607
1628
  this.columns.forEach((col) => {
1608
1629
  delete col.customWidthPx;
@@ -1610,6 +1631,11 @@ export class Wunderbaum {
1610
1631
  this.update(ChangeType.colStructure);
1611
1632
  }
1612
1633
 
1634
+ // /** Renumber nodes `_nativeIndex`. @see {@link WunderbaumNode.resetNativeChildOrder} */
1635
+ // resetNativeChildOrder(options?: ResetOrderOptions) {
1636
+ // this.root.resetNativeChildOrder(options);
1637
+ // }
1638
+
1613
1639
  /**
1614
1640
  * Make sure that this node is vertically scrolled into the viewport.
1615
1641
  *
@@ -1620,7 +1646,7 @@ export class Wunderbaum {
1620
1646
  const PADDING = 2; // leave some pixels between viewport bounds
1621
1647
 
1622
1648
  let node;
1623
- WunderbaumNode;
1649
+ // WunderbaumNode;
1624
1650
  let options: ScrollToOptions | undefined;
1625
1651
 
1626
1652
  if (nodeOrOpts instanceof WunderbaumNode) {
@@ -1631,14 +1657,15 @@ export class Wunderbaum {
1631
1657
  }
1632
1658
  util.assert(node && node._rowIdx != null, `Invalid node: ${node}`);
1633
1659
 
1660
+ const rowHeight = this.options.rowHeightPx!;
1634
1661
  const scrollParent = this.element;
1635
1662
  const headerHeight = this.headerElement.clientHeight; // May be 0
1636
1663
  const scrollTop = scrollParent.scrollTop;
1637
1664
  const vpHeight = scrollParent.clientHeight;
1638
- const rowTop = node._rowIdx! * ROW_HEIGHT + headerHeight;
1665
+ const rowTop = node._rowIdx! * rowHeight + headerHeight;
1639
1666
  const vpTop = headerHeight;
1640
1667
  const vpRowTop = rowTop - scrollTop;
1641
- const vpRowBottom = vpRowTop + ROW_HEIGHT;
1668
+ const vpRowBottom = vpRowTop + rowHeight;
1642
1669
  const topNode = options?.topNode;
1643
1670
 
1644
1671
  // this.log( `scrollTo(${node.title}), vpTop:${vpTop}px, scrollTop:${scrollTop}, vpHeight:${vpHeight}, rowTop:${rowTop}, vpRowTop:${vpRowTop}`, nodeOrOpts , options);
@@ -1651,7 +1678,7 @@ export class Wunderbaum {
1651
1678
  } else {
1652
1679
  // Node is below viewport
1653
1680
  // this.log("Below viewport");
1654
- newScrollTop = rowTop + ROW_HEIGHT - vpHeight + PADDING; // leave some pixels between viewport bounds
1681
+ newScrollTop = rowTop + rowHeight - vpHeight + PADDING; // leave some pixels between viewport bounds
1655
1682
  }
1656
1683
  } else {
1657
1684
  // Node is above viewport
@@ -1790,15 +1817,15 @@ export class Wunderbaum {
1790
1817
  * The render operation is async and debounced unless the `immediate` option
1791
1818
  * is set.
1792
1819
  *
1793
- * Use {@link WunderbaumNode.update()} if only a single node has changed,
1794
- * or {@link WunderbaumNode._render()}) to pass special options.
1820
+ * Use {@link WunderbaumNode.update} if only a single node has changed,
1821
+ * or {@link WunderbaumNode._render}) to pass special options.
1795
1822
  */
1796
1823
  update(change: ChangeType, options?: UpdateOptions): void;
1797
1824
 
1798
1825
  /**
1799
1826
  * Update a row to reflect a single node's modification.
1800
1827
  *
1801
- * @see {@link WunderbaumNode.update()}, {@link WunderbaumNode._render()}
1828
+ * @see {@link WunderbaumNode.update}, {@link WunderbaumNode._render}
1802
1829
  */
1803
1830
  update(
1804
1831
  change: ChangeType,
@@ -1988,6 +2015,15 @@ export class Wunderbaum {
1988
2015
  this.root.sortChildren(cmp, deep);
1989
2016
  }
1990
2017
 
2018
+ /**
2019
+ * Convenience method to implement column sorting.
2020
+ * @see {@link WunderbaumNode.sortByProperty}.
2021
+ * @since 0.11.0
2022
+ */
2023
+ sortByProperty(options: SortByPropertyOptions) {
2024
+ this.root.sortByProperty(options);
2025
+ }
2026
+
1991
2027
  /** Convert tree to an array of plain objects.
1992
2028
  *
1993
2029
  * @param callback is called for every node, in order to allow
@@ -2109,6 +2145,12 @@ export class Wunderbaum {
2109
2145
  return modified;
2110
2146
  }
2111
2147
 
2148
+ protected _insertIcon(icon: string, elem: HTMLElement) {
2149
+ const iconElem = document.createElement("i");
2150
+ iconElem.className = icon;
2151
+ elem.appendChild(iconElem);
2152
+ }
2153
+
2112
2154
  /** Create/update header markup from `this.columns` definition.
2113
2155
  * @internal
2114
2156
  */
@@ -2119,6 +2161,7 @@ export class Wunderbaum {
2119
2161
  if (!wantHeader) {
2120
2162
  return;
2121
2163
  }
2164
+ const iconMap = this.iconMap;
2122
2165
  const colCount = this.columns.length;
2123
2166
  const headerRow = this.headerElement.querySelector(".wb-row")!;
2124
2167
  util.assert(headerRow, "Expected a row in header element");
@@ -2139,22 +2182,55 @@ export class Wunderbaum {
2139
2182
  col.classes ? colElem.classList.add(...col.classes.split(" ")) : 0;
2140
2183
  }
2141
2184
 
2142
- const title = util.escapeHtml(col.title || col.id);
2185
+ // Add tooltip to column title
2143
2186
  let tooltip = "";
2144
2187
  if (col.tooltip) {
2145
2188
  tooltip = util.escapeTooltip(col.tooltip);
2146
2189
  tooltip = ` title="${tooltip}"`;
2147
2190
  }
2148
- let resizer = "";
2191
+ // Add column header icons
2192
+ let addMarkup = "";
2193
+ // NOTE: we use CSS float: right to align icons, so they must be added in
2194
+ // reverse order
2195
+ if (util.toBool(col.menu, this.options.columnsMenu, false)) {
2196
+ const iconClass = "wb-col-icon-menu " + iconMap.colMenu;
2197
+ const icon = `<i data-command=menu class="wb-col-icon ${iconClass}"></i>`;
2198
+ addMarkup += icon;
2199
+ }
2200
+ if (util.toBool(col.sortable, this.options.columnsSortable, false)) {
2201
+ let iconClass = "wb-col-icon-sort " + iconMap.colSortable;
2202
+ if (col.sortOrder) {
2203
+ iconClass += `wb-col-sort-${col.sortOrder}`;
2204
+ iconClass +=
2205
+ col.sortOrder === "asc" ? iconMap.colSortAsc : iconMap.colSortDesc;
2206
+ }
2207
+ const icon = `<i data-command=sort class="wb-col-icon ${iconClass}"></i>`;
2208
+ addMarkup += icon;
2209
+ }
2210
+ if (util.toBool(col.filterable, this.options.columnsFilterable, false)) {
2211
+ colElem.classList.toggle("wb-col-filter", !!col.filterActive);
2212
+ let iconClass = "wb-col-icon-filter " + iconMap.colFilter;
2213
+ if (col.filterActive) {
2214
+ iconClass += iconMap.colFilterActive;
2215
+ }
2216
+ const icon = `<i data-command=filter class="wb-col-icon ${iconClass}"></i>`;
2217
+ addMarkup += icon;
2218
+ }
2219
+ // Add resizer to all but the last column
2149
2220
  if (i < colCount - 1) {
2150
- if (util.toBool(col.resizable, this.options.resizableColumns, false)) {
2151
- resizer =
2221
+ if (util.toBool(col.resizable, this.options.columnsResizable, false)) {
2222
+ addMarkup +=
2152
2223
  '<span class="wb-col-resizer wb-col-resizer-active"></span>';
2153
2224
  } else {
2154
- resizer = '<span class="wb-col-resizer"></span>';
2225
+ addMarkup += '<span class="wb-col-resizer"></span>';
2155
2226
  }
2156
2227
  }
2157
- colElem.innerHTML = `<span class="wb-col-title"${tooltip}>${title}</span>${resizer}`;
2228
+
2229
+ // Create column header
2230
+ const title = util.escapeHtml(col.title || col.id);
2231
+ colElem.innerHTML = `<span class="wb-col-title"${tooltip}>${title}</span>${addMarkup}`;
2232
+
2233
+ // Highlight active column
2158
2234
  if (this.isCellNav()) {
2159
2235
  colElem.classList.toggle("wb-active", i === this.activeColIdx);
2160
2236
  }
@@ -2300,20 +2376,20 @@ export class Wunderbaum {
2300
2376
  options = Object.assign({ newNodesOnly: false }, options);
2301
2377
  const newNodesOnly = !!options.newNodesOnly;
2302
2378
 
2303
- const row_height = ROW_HEIGHT;
2304
- const vp_height = this.element.clientHeight;
2379
+ const rowHeight = this.options.rowHeightPx!;
2380
+ const vpHeight = this.element.clientHeight;
2305
2381
  const prefetch = RENDER_MAX_PREFETCH;
2306
2382
  // const grace_prefetch = RENDER_MAX_PREFETCH - RENDER_MIN_PREFETCH;
2307
2383
  const ofs = this.element.scrollTop;
2308
2384
 
2309
- let startIdx = Math.max(0, ofs / row_height - prefetch);
2385
+ let startIdx = Math.max(0, ofs / rowHeight - prefetch);
2310
2386
  startIdx = Math.floor(startIdx);
2311
2387
  // Make sure start is always even, so the alternating row colors don't
2312
2388
  // change when scrolling:
2313
2389
  if (startIdx % 2) {
2314
2390
  startIdx--;
2315
2391
  }
2316
- let endIdx = Math.max(0, (ofs + vp_height) / row_height + prefetch);
2392
+ let endIdx = Math.max(0, (ofs + vpHeight) / rowHeight + prefetch);
2317
2393
  endIdx = Math.ceil(endIdx);
2318
2394
 
2319
2395
  // this.debug("render", opts);
@@ -2346,20 +2422,20 @@ export class Wunderbaum {
2346
2422
  } else if (rowDiv && newNodesOnly) {
2347
2423
  obsoleteNodes.delete(node);
2348
2424
  // no need to update existing node markup
2349
- rowDiv.style.top = idx * ROW_HEIGHT + "px";
2425
+ rowDiv.style.top = idx * rowHeight + "px";
2350
2426
  prevElem = rowDiv;
2351
2427
  } else {
2352
2428
  obsoleteNodes.delete(node);
2353
2429
  // Create new markup
2354
2430
  if (rowDiv) {
2355
- rowDiv.style.top = idx * ROW_HEIGHT + "px";
2431
+ rowDiv.style.top = idx * rowHeight + "px";
2356
2432
  }
2357
2433
  node._render({ top: top, after: prevElem });
2358
2434
  // node.log("render", top, prevElem, "=>", node._rowElem);
2359
2435
  prevElem = node._rowElem!;
2360
2436
  }
2361
2437
  idx++;
2362
- top += row_height;
2438
+ top += rowHeight;
2363
2439
  });
2364
2440
  this.treeRowCount = idx;
2365
2441
  for (const n of obsoleteNodes) {
@@ -2633,6 +2709,7 @@ export class Wunderbaum {
2633
2709
  /**
2634
2710
  * Return the number of nodes that match the current filter.
2635
2711
  * @see {@link Wunderbaum.filterNodes}
2712
+ * @since 0.9.0
2636
2713
  */
2637
2714
  countMatches(): number {
2638
2715
  return (this.extensions.filter as FilterExtension).countMatches();