wunderbaum 0.0.6 → 0.0.8

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.
@@ -7,7 +7,7 @@
7
7
  /*!
8
8
  * Wunderbaum - util
9
9
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
10
- * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
10
+ * v0.0.8, Fri, 23 Sep 2022 20:47:29 GMT (https://github.com/mar10/wunderbaum)
11
11
  */
12
12
  /** @module util */
13
13
  /** Readable names for `MouseEvent.button` */
@@ -287,11 +287,17 @@
287
287
  case "week":
288
288
  case "datetime":
289
289
  case "datetime-local":
290
- input.valueAsDate = value;
290
+ input.valueAsDate = new Date(value);
291
+ // input.valueAsDate = value; // breaks in Edge?
291
292
  break;
292
293
  case "number":
293
294
  case "range":
294
- input.valueAsNumber = value;
295
+ if (value == null) {
296
+ input.value = value;
297
+ }
298
+ else {
299
+ input.valueAsNumber = value;
300
+ }
295
301
  break;
296
302
  case "radio":
297
303
  error("Not implemented");
@@ -308,7 +314,7 @@
308
314
  break;
309
315
  case "text":
310
316
  default:
311
- input.value = value || "";
317
+ input.value = value !== null && value !== void 0 ? value : "";
312
318
  }
313
319
  }
314
320
  else if (tag === "SELECT") {
@@ -466,7 +472,7 @@
466
472
  });
467
473
  });
468
474
  }
469
- /** Return a wrapped handler method, that provides `this._super`.
475
+ /** Return a wrapped handler method, that provides `this._super` and `this._superApply`.
470
476
  *
471
477
  * ```ts
472
478
  // Implement `opts.createNode` event to add the 'draggable' attribute
@@ -707,7 +713,7 @@
707
713
  /*!
708
714
  * Wunderbaum - types
709
715
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
710
- * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
716
+ * v0.0.8, Fri, 23 Sep 2022 20:47:29 GMT (https://github.com/mar10/wunderbaum)
711
717
  */
712
718
  /** Possible values for `setModified()`. */
713
719
  var ChangeType;
@@ -759,7 +765,7 @@
759
765
  /*!
760
766
  * Wunderbaum - wb_extension_base
761
767
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
762
- * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
768
+ * v0.0.8, Fri, 23 Sep 2022 20:47:29 GMT (https://github.com/mar10/wunderbaum)
763
769
  */
764
770
  class WunderbaumExtension {
765
771
  constructor(tree, id, defaults) {
@@ -1050,7 +1056,7 @@
1050
1056
  /*!
1051
1057
  * Wunderbaum - ext-filter
1052
1058
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1053
- * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
1059
+ * v0.0.8, Fri, 23 Sep 2022 20:47:29 GMT (https://github.com/mar10/wunderbaum)
1054
1060
  */
1055
1061
  const START_MARKER = "\uFFF7";
1056
1062
  const END_MARKER = "\uFFF8";
@@ -1352,107 +1358,10 @@
1352
1358
  return textPoses.join("");
1353
1359
  }
1354
1360
 
1355
- /*!
1356
- * Wunderbaum - common
1357
- * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1358
- * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
1359
- */
1360
- const DEFAULT_DEBUGLEVEL = 4; // Replaced by rollup script
1361
- const ROW_HEIGHT = 22;
1362
- // export const HEADER_HEIGHT = ROW_HEIGHT;
1363
- const ICON_WIDTH = 20;
1364
- const ROW_EXTRA_PAD = 7; // 2x $col-padding-x + 3px rounding errors
1365
- const RENDER_MAX_PREFETCH = 5;
1366
- const TEST_IMG = new RegExp(/\.|\//); // strings are considered image urls if they contain '.' or '/'
1367
- // export const RECURSIVE_REQUEST_ERROR = "$recursive_request";
1368
- // export const INVALID_REQUEST_TARGET_ERROR = "$request_target_invalid";
1369
- let iconMap = {
1370
- error: "bi bi-exclamation-triangle",
1371
- // loading: "bi bi-hourglass-split wb-busy",
1372
- loading: "bi bi-chevron-right wb-busy",
1373
- // loading: "bi bi-arrow-repeat wb-spin",
1374
- // loading: '<div class="spinner-border spinner-border-sm" role="status"> <span class="visually-hidden">Loading...</span> </div>',
1375
- // noData: "bi bi-search",
1376
- noData: "bi bi-question-circle",
1377
- expanderExpanded: "bi bi-chevron-down",
1378
- // expanderExpanded: "bi bi-dash-square",
1379
- expanderCollapsed: "bi bi-chevron-right",
1380
- // expanderCollapsed: "bi bi-plus-square",
1381
- expanderLazy: "bi bi-chevron-right wb-helper-lazy-expander",
1382
- // expanderLazy: "bi bi-chevron-bar-right",
1383
- checkChecked: "bi bi-check-square",
1384
- checkUnchecked: "bi bi-square",
1385
- checkUnknown: "bi dash-square-dotted",
1386
- radioChecked: "bi bi-circle-fill",
1387
- radioUnchecked: "bi bi-circle",
1388
- radioUnknown: "bi bi-circle-dotted",
1389
- folder: "bi bi-folder2",
1390
- folderOpen: "bi bi-folder2-open",
1391
- doc: "bi bi-file-earmark",
1392
- };
1393
- /** Dict keys that are evaluated by source loader (others are added to `tree.data` instead). */
1394
- const RESERVED_TREE_SOURCE_KEYS = new Set([
1395
- "children",
1396
- "columns",
1397
- "format",
1398
- "keyMap",
1399
- "positional",
1400
- "typeList",
1401
- "types",
1402
- "version", // reserved for future use
1403
- ]);
1404
- /** Key codes that trigger grid navigation, even when inside an input element. */
1405
- const INPUT_BREAKOUT_KEYS = new Set([
1406
- // "ArrowDown",
1407
- // "ArrowUp",
1408
- "Enter",
1409
- "Escape",
1410
- ]);
1411
- /** Map `KeyEvent.key` to navigation action. */
1412
- const KEY_TO_ACTION_DICT = {
1413
- " ": "toggleSelect",
1414
- "+": "expand",
1415
- Add: "expand",
1416
- ArrowDown: "down",
1417
- ArrowLeft: "left",
1418
- ArrowRight: "right",
1419
- ArrowUp: "up",
1420
- Backspace: "parent",
1421
- "/": "collapseAll",
1422
- Divide: "collapseAll",
1423
- End: "lastCol",
1424
- Home: "firstCol",
1425
- "Control+End": "last",
1426
- "Control+Home": "first",
1427
- "Meta+ArrowDown": "last",
1428
- "Meta+ArrowUp": "first",
1429
- "*": "expandAll",
1430
- Multiply: "expandAll",
1431
- PageDown: "pageDown",
1432
- PageUp: "pageUp",
1433
- "-": "collapse",
1434
- Subtract: "collapse",
1435
- };
1436
- /** Return a callback that returns true if the node title contains a substring (case-insensitive). */
1437
- function makeNodeTitleMatcher(s) {
1438
- s = escapeRegex(s.toLowerCase());
1439
- return function (node) {
1440
- return node.title.toLowerCase().indexOf(s) >= 0;
1441
- };
1442
- }
1443
- /** Return a callback that returns true if the node title starts with a string (case-insensitive). */
1444
- function makeNodeTitleStartMatcher(s) {
1445
- s = escapeRegex(s);
1446
- const reMatch = new RegExp("^" + s, "i");
1447
- return function (node) {
1448
- return reMatch.test(node.title);
1449
- };
1450
- }
1451
-
1452
1361
  /*!
1453
1362
  * Wunderbaum - ext-keynav
1454
1363
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1455
- * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
1364
+ * v0.0.8, Fri, 23 Sep 2022 20:47:29 GMT (https://github.com/mar10/wunderbaum)
1456
1365
  */
1457
1366
  const QUICKSEARCH_DELAY = 500;
1458
1367
  class KeynavExtension extends WunderbaumExtension {
@@ -1486,7 +1395,7 @@
1486
1395
  const event = data.event, tree = this.tree, opts = data.options, activate = !event.ctrlKey || opts.autoActivate, curInput = this._getEmbeddedInputElem(event.target), navModeOption = opts.navigationModeOption;
1487
1396
  // isCellEditMode = tree.navMode === NavigationMode.cellEdit;
1488
1397
  let focusNode, eventName = eventToString(event), node = data.node, handled = true;
1489
- tree.log(`onKeyEvent: ${eventName}, curInput`, curInput);
1398
+ // tree.log(`onKeyEvent: ${eventName}, curInput`, curInput);
1490
1399
  if (!tree.isEnabled()) {
1491
1400
  // tree.logDebug(`onKeyEvent ignored for disabled tree: ${eventName}`);
1492
1401
  return false;
@@ -1609,10 +1518,11 @@
1609
1518
  if (eventName === "Escape") {
1610
1519
  // Discard changes
1611
1520
  node.render();
1521
+ // } else if (!INPUT_BREAKOUT_KEYS.has(eventName)) {
1612
1522
  }
1613
- else if (!INPUT_BREAKOUT_KEYS.has(eventName)) {
1523
+ else if (eventName !== "Enter") {
1614
1524
  // Let current `<input>` handle it
1615
- node.logDebug(`Ignored ${eventName} inside input`);
1525
+ node.logDebug(`Ignored ${eventName} inside focused input`);
1616
1526
  return;
1617
1527
  }
1618
1528
  // const curInputType = curInput.type || curInput.tagName;
@@ -1683,24 +1593,24 @@
1683
1593
  if (isColspan && node.isExpanded()) {
1684
1594
  node.setExpanded(false);
1685
1595
  }
1686
- else if (tree.activeColIdx > 0) {
1596
+ else if (!isColspan && tree.activeColIdx > 0) {
1687
1597
  tree.setColumn(tree.activeColIdx - 1);
1688
- handled = true;
1689
1598
  }
1690
1599
  else if (navModeOption !== NavigationOptions.cell) {
1691
1600
  tree.setCellNav(false); // row-nav mode
1692
- handled = true;
1693
1601
  }
1602
+ handled = true;
1694
1603
  break;
1695
1604
  case "ArrowRight":
1696
1605
  tree.setFocus(); // Blur prev. input if any
1697
1606
  if (isColspan && !node.isExpanded()) {
1698
1607
  node.setExpanded();
1699
1608
  }
1700
- else if (tree.activeColIdx < tree.columns.length - 1) {
1609
+ else if (!isColspan &&
1610
+ tree.activeColIdx < tree.columns.length - 1) {
1701
1611
  tree.setColumn(tree.activeColIdx + 1);
1702
- handled = true;
1703
1612
  }
1613
+ handled = true;
1704
1614
  break;
1705
1615
  case "ArrowDown":
1706
1616
  case "ArrowUp":
@@ -1733,7 +1643,7 @@
1733
1643
  /*!
1734
1644
  * Wunderbaum - ext-logger
1735
1645
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1736
- * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
1646
+ * v0.0.8, Fri, 23 Sep 2022 20:47:29 GMT (https://github.com/mar10/wunderbaum)
1737
1647
  */
1738
1648
  class LoggerExtension extends WunderbaumExtension {
1739
1649
  constructor(tree) {
@@ -1756,7 +1666,7 @@
1756
1666
  if (ignoreEvents.has(name)) {
1757
1667
  return tree._superApply(arguments);
1758
1668
  }
1759
- let start = Date.now();
1669
+ const start = Date.now();
1760
1670
  const res = tree._superApply(arguments);
1761
1671
  console.debug(`${prefix}: callEvent('${name}') took ${Date.now() - start} ms.`, arguments[1]);
1762
1672
  return res;
@@ -1770,10 +1680,250 @@
1770
1680
  }
1771
1681
  }
1772
1682
 
1683
+ /*!
1684
+ * Wunderbaum - common
1685
+ * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1686
+ * v0.0.8, Fri, 23 Sep 2022 20:47:29 GMT (https://github.com/mar10/wunderbaum)
1687
+ */
1688
+ const DEFAULT_DEBUGLEVEL = 4; // Replaced by rollup script
1689
+ /**
1690
+ * Fixed height of a row in pixel. Must match the SCSS variable `$row-outer-height`.
1691
+ */
1692
+ const ROW_HEIGHT = 22;
1693
+ /**
1694
+ * Fixed width of node icons in pixel. Must match the SCSS variable `$icon-outer-width`.
1695
+ */
1696
+ const ICON_WIDTH = 20;
1697
+ /**
1698
+ * Adjust the width of the title span, so overflow ellipsis work.
1699
+ * (2 x `$col-padding-x` + 3px rounding errors).
1700
+ */
1701
+ const TITLE_SPAN_PAD_Y = 7;
1702
+ /** Render row markup for N nodes above and below the visible viewport. */
1703
+ const RENDER_MAX_PREFETCH = 5;
1704
+ /** Regular expression to detect if a string describes an image URL (in contrast
1705
+ * to a class name). Strings are considered image urls if they contain '.' or '/'.
1706
+ */
1707
+ const TEST_IMG = new RegExp(/\.|\//);
1708
+ // export const RECURSIVE_REQUEST_ERROR = "$recursive_request";
1709
+ // export const INVALID_REQUEST_TARGET_ERROR = "$request_target_invalid";
1710
+ /**
1711
+ * Default node icons.
1712
+ * Requires bootstrap icons https://icons.getbootstrap.com
1713
+ */
1714
+ const iconMap = {
1715
+ error: "bi bi-exclamation-triangle",
1716
+ // loading: "bi bi-hourglass-split wb-busy",
1717
+ loading: "bi bi-chevron-right wb-busy",
1718
+ // loading: "bi bi-arrow-repeat wb-spin",
1719
+ // loading: '<div class="spinner-border spinner-border-sm" role="status"> <span class="visually-hidden">Loading...</span> </div>',
1720
+ // noData: "bi bi-search",
1721
+ noData: "bi bi-question-circle",
1722
+ expanderExpanded: "bi bi-chevron-down",
1723
+ // expanderExpanded: "bi bi-dash-square",
1724
+ expanderCollapsed: "bi bi-chevron-right",
1725
+ // expanderCollapsed: "bi bi-plus-square",
1726
+ expanderLazy: "bi bi-chevron-right wb-helper-lazy-expander",
1727
+ // expanderLazy: "bi bi-chevron-bar-right",
1728
+ checkChecked: "bi bi-check-square",
1729
+ checkUnchecked: "bi bi-square",
1730
+ checkUnknown: "bi dash-square-dotted",
1731
+ radioChecked: "bi bi-circle-fill",
1732
+ radioUnchecked: "bi bi-circle",
1733
+ radioUnknown: "bi bi-circle-dotted",
1734
+ folder: "bi bi-folder2",
1735
+ folderOpen: "bi bi-folder2-open",
1736
+ folderLazy: "bi bi-folder-symlink",
1737
+ doc: "bi bi-file-earmark",
1738
+ };
1739
+ /** Dict keys that are evaluated by source loader (others are added to `tree.data` instead). */
1740
+ const RESERVED_TREE_SOURCE_KEYS = new Set([
1741
+ "_format",
1742
+ "_keyMap",
1743
+ "_positional",
1744
+ "_typeList",
1745
+ "_version",
1746
+ "children",
1747
+ "columns",
1748
+ "types",
1749
+ ]);
1750
+ // /** Key codes that trigger grid navigation, even when inside an input element. */
1751
+ // export const INPUT_BREAKOUT_KEYS: Set<string> = new Set([
1752
+ // // "ArrowDown",
1753
+ // // "ArrowUp",
1754
+ // "Enter",
1755
+ // "Escape",
1756
+ // ]);
1757
+ /** Map `KeyEvent.key` to navigation action. */
1758
+ const KEY_TO_ACTION_DICT = {
1759
+ " ": "toggleSelect",
1760
+ "+": "expand",
1761
+ Add: "expand",
1762
+ ArrowDown: "down",
1763
+ ArrowLeft: "left",
1764
+ ArrowRight: "right",
1765
+ ArrowUp: "up",
1766
+ Backspace: "parent",
1767
+ "/": "collapseAll",
1768
+ Divide: "collapseAll",
1769
+ End: "lastCol",
1770
+ Home: "firstCol",
1771
+ "Control+End": "last",
1772
+ "Control+Home": "first",
1773
+ "Meta+ArrowDown": "last",
1774
+ "Meta+ArrowUp": "first",
1775
+ "*": "expandAll",
1776
+ Multiply: "expandAll",
1777
+ PageDown: "pageDown",
1778
+ PageUp: "pageUp",
1779
+ "-": "collapse",
1780
+ Subtract: "collapse",
1781
+ };
1782
+ /** Return a callback that returns true if the node title matches the string
1783
+ * or regular expression.
1784
+ * @see {@link WunderbaumNode.findAll}
1785
+ */
1786
+ function makeNodeTitleMatcher(match) {
1787
+ if (match instanceof RegExp) {
1788
+ return function (node) {
1789
+ return match.test(node.title);
1790
+ };
1791
+ }
1792
+ assert(typeof match === "string");
1793
+ // s = escapeRegex(s.toLowerCase());
1794
+ return function (node) {
1795
+ return node.title === match;
1796
+ // console.log("match " + node, node.title.toLowerCase().indexOf(match))
1797
+ // return node.title.toLowerCase().indexOf(match) >= 0;
1798
+ };
1799
+ }
1800
+ /** Return a callback that returns true if the node title starts with a string (case-insensitive). */
1801
+ function makeNodeTitleStartMatcher(s) {
1802
+ s = escapeRegex(s);
1803
+ const reMatch = new RegExp("^" + s, "i");
1804
+ return function (node) {
1805
+ return reMatch.test(node.title);
1806
+ };
1807
+ }
1808
+ function unflattenSource(source) {
1809
+ var _a, _b, _c;
1810
+ const { _format, _keyMap, _positional, children } = source;
1811
+ if (_format !== "flat") {
1812
+ throw new Error(`Expected source._format: "flat", but got ${_format}`);
1813
+ }
1814
+ if (_positional && _positional.includes("children")) {
1815
+ throw new Error(`source._positional must not include "children": ${_positional}`);
1816
+ }
1817
+ // Inverse keyMap:
1818
+ let longToShort = {};
1819
+ if (_keyMap) {
1820
+ for (const [key, value] of Object.entries(_keyMap)) {
1821
+ longToShort[value] = key;
1822
+ }
1823
+ }
1824
+ const positionalShort = _positional.map((e) => longToShort[e]);
1825
+ const newChildren = [];
1826
+ const keyToNodeMap = {};
1827
+ const indexToNodeMap = {};
1828
+ const keyAttrName = (_a = longToShort["key"]) !== null && _a !== void 0 ? _a : "key";
1829
+ const childrenAttrName = (_b = longToShort["children"]) !== null && _b !== void 0 ? _b : "children";
1830
+ for (const [index, node] of children.entries()) {
1831
+ // Node entry format:
1832
+ // [PARENT_ID, [POSITIONAL_ARGS]]
1833
+ // or
1834
+ // [PARENT_ID, [POSITIONAL_ARGS], {KEY_VALUE_ARGS}]
1835
+ const [parentId, args, kwargs = {}] = node;
1836
+ // Free up some memory as we go
1837
+ node[1] = null;
1838
+ if (node[2] != null) {
1839
+ node[2] = null;
1840
+ }
1841
+ // console.log("flatten", parentId, args, kwargs)
1842
+ // We keep `kwargs` as our new node definition. Then we add all positional
1843
+ // values to this object:
1844
+ args.forEach((val, positionalIdx) => {
1845
+ kwargs[positionalShort[positionalIdx]] = val;
1846
+ });
1847
+ // Find the parent node. `null` means 'toplevel'. PARENT_ID may be the numeric
1848
+ // index of the source.children list. If PARENT_ID is a string, we search
1849
+ // a parent with node.key of this value.
1850
+ indexToNodeMap[index] = kwargs;
1851
+ const key = kwargs[keyAttrName];
1852
+ if (key != null) {
1853
+ keyToNodeMap[key] = kwargs;
1854
+ }
1855
+ let parentNode = null;
1856
+ if (parentId === null) ;
1857
+ else if (typeof parentId === "number") {
1858
+ parentNode = indexToNodeMap[parentId];
1859
+ if (parentNode === undefined) {
1860
+ throw new Error(`unflattenSource: Could not find parent node by index: ${parentId}.`);
1861
+ }
1862
+ }
1863
+ else {
1864
+ parentNode = keyToNodeMap[parentId];
1865
+ if (parentNode === undefined) {
1866
+ throw new Error(`unflattenSource: Could not find parent node by key: ${parentId}`);
1867
+ }
1868
+ }
1869
+ if (parentNode) {
1870
+ (_c = parentNode[childrenAttrName]) !== null && _c !== void 0 ? _c : (parentNode[childrenAttrName] = []);
1871
+ parentNode[childrenAttrName].push(kwargs);
1872
+ }
1873
+ else {
1874
+ newChildren.push(kwargs);
1875
+ }
1876
+ }
1877
+ delete source.children;
1878
+ source.children = newChildren;
1879
+ }
1880
+ function inflateSourceData(source) {
1881
+ const { _format, _keyMap, _typeList } = source;
1882
+ if (_format === "flat") {
1883
+ unflattenSource(source);
1884
+ }
1885
+ delete source._format;
1886
+ delete source._version;
1887
+ delete source._keyMap;
1888
+ delete source._typeList;
1889
+ delete source._positional;
1890
+ function _iter(childList) {
1891
+ for (let node of childList) {
1892
+ // Expand short alias names
1893
+ if (_keyMap) {
1894
+ // Iterate over a list of names, because we modify inside the loop:
1895
+ Object.getOwnPropertyNames(node).forEach((propName) => {
1896
+ var _a;
1897
+ const long = (_a = _keyMap[propName]) !== null && _a !== void 0 ? _a : propName;
1898
+ if (long !== propName) {
1899
+ node[long] = node[propName];
1900
+ delete node[propName];
1901
+ }
1902
+ });
1903
+ }
1904
+ // `node` now has long attribute names
1905
+ // Resolve node type indexes
1906
+ const type = node.type;
1907
+ if (_typeList && type != null && typeof type === "number") {
1908
+ const newType = _typeList[type];
1909
+ if (newType == null) {
1910
+ throw new Error(`Expected typeList[${type}] entry in [${_typeList}]`);
1911
+ }
1912
+ node.type = newType;
1913
+ }
1914
+ // Recursion
1915
+ if (node.children) {
1916
+ _iter(node.children);
1917
+ }
1918
+ }
1919
+ }
1920
+ _iter(source.children);
1921
+ }
1922
+
1773
1923
  /*!
1774
1924
  * Wunderbaum - ext-dnd
1775
1925
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
1776
- * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
1926
+ * v0.0.8, Fri, 23 Sep 2022 20:47:29 GMT (https://github.com/mar10/wunderbaum)
1777
1927
  */
1778
1928
  const nodeMimeType = "application/x-wunderbaum-node";
1779
1929
  class DndExtension extends WunderbaumExtension {
@@ -2041,7 +2191,7 @@
2041
2191
  /*!
2042
2192
  * Wunderbaum - drag_observer
2043
2193
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
2044
- * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
2194
+ * v0.0.8, Fri, 23 Sep 2022 20:47:29 GMT (https://github.com/mar10/wunderbaum)
2045
2195
  */
2046
2196
  /**
2047
2197
  * Convert mouse- and touch events to 'dragstart', 'drag', and 'dragstop'.
@@ -2175,7 +2325,7 @@
2175
2325
  /*!
2176
2326
  * Wunderbaum - ext-grid
2177
2327
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
2178
- * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
2328
+ * v0.0.8, Fri, 23 Sep 2022 20:47:29 GMT (https://github.com/mar10/wunderbaum)
2179
2329
  */
2180
2330
  class GridExtension extends WunderbaumExtension {
2181
2331
  constructor(tree) {
@@ -2212,7 +2362,7 @@
2212
2362
  /*!
2213
2363
  * Wunderbaum - deferred
2214
2364
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
2215
- * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
2365
+ * v0.0.8, Fri, 23 Sep 2022 20:47:29 GMT (https://github.com/mar10/wunderbaum)
2216
2366
  */
2217
2367
  /**
2218
2368
  * Implement a ES6 Promise, that exposes a resolve() and reject() method.
@@ -2265,7 +2415,7 @@
2265
2415
  /*!
2266
2416
  * Wunderbaum - wunderbaum_node
2267
2417
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
2268
- * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
2418
+ * v0.0.8, Fri, 23 Sep 2022 20:47:29 GMT (https://github.com/mar10/wunderbaum)
2269
2419
  */
2270
2420
  /** Top-level properties that can be passed with `data`. */
2271
2421
  const NODE_PROPS = new Set([
@@ -2406,57 +2556,63 @@
2406
2556
  /**
2407
2557
  * Append (or insert) a list of child nodes.
2408
2558
  *
2409
- * Tip: pass `{ before: 0 }` to prepend children
2410
- * @param {NodeData[]} nodeData array of child node definitions (also single child accepted)
2411
- * @param child node (or key or index of such).
2412
- * If omitted, the new children are appended.
2559
+ * Tip: pass `{ before: 0 }` to prepend new nodes as first children.
2560
+ *
2413
2561
  * @returns first child added
2414
2562
  */
2415
2563
  addChildren(nodeData, options) {
2416
- let insertBefore = options
2417
- ? options.before
2418
- : null,
2419
- // redraw = options ? options.redraw !== false : true,
2420
- nodeList = [];
2564
+ const tree = this.tree;
2565
+ let { before = null, applyMinExpanLevel = true, _level } = options !== null && options !== void 0 ? options : {};
2566
+ // let { before, loadLazy=true, _level } = options ?? {};
2567
+ // const isTopCall = _level == null;
2568
+ _level !== null && _level !== void 0 ? _level : (_level = this.getLevel());
2569
+ const nodeList = [];
2421
2570
  try {
2422
- this.tree.enableUpdate(false);
2571
+ tree.enableUpdate(false);
2423
2572
  if (isPlainObject(nodeData)) {
2424
2573
  nodeData = [nodeData];
2425
2574
  }
2575
+ const forceExpand = applyMinExpanLevel && _level < tree.options.minExpandLevel;
2426
2576
  for (let child of nodeData) {
2427
- let subChildren = child.children;
2577
+ const subChildren = child.children;
2428
2578
  delete child.children;
2429
- let n = new WunderbaumNode(this.tree, this, child);
2579
+ const n = new WunderbaumNode(tree, this, child);
2580
+ if (forceExpand && !n.isUnloaded()) {
2581
+ n.expanded = true;
2582
+ }
2430
2583
  nodeList.push(n);
2431
2584
  if (subChildren) {
2432
- n.addChildren(subChildren, { redraw: false });
2585
+ n.addChildren(subChildren, { _level: _level + 1 });
2433
2586
  }
2434
2587
  }
2435
2588
  if (!this.children) {
2436
2589
  this.children = nodeList;
2437
2590
  }
2438
- else if (insertBefore == null || this.children.length === 0) {
2591
+ else if (before == null || this.children.length === 0) {
2439
2592
  this.children = this.children.concat(nodeList);
2440
2593
  }
2441
2594
  else {
2442
- // Returns null if insertBefore is not a direct child:
2443
- insertBefore = this.findDirectChild(insertBefore);
2444
- let pos = this.children.indexOf(insertBefore);
2445
- assert(pos >= 0, "insertBefore must be an existing child");
2595
+ // Returns null if before is not a direct child:
2596
+ before = this.findDirectChild(before);
2597
+ let pos = this.children.indexOf(before);
2598
+ assert(pos >= 0, `options.before must be a direct child of ${this}`);
2446
2599
  // insert nodeList after children[pos]
2447
2600
  this.children.splice(pos, 0, ...nodeList);
2448
2601
  }
2449
2602
  // TODO:
2450
- // if (this.tree.options.selectMode === 3) {
2603
+ // if (tree.options.selectMode === 3) {
2451
2604
  // this.fixSelection3FromEndNodes();
2452
2605
  // }
2453
2606
  // this.triggerModifyChild("add", nodeList.length === 1 ? nodeList[0] : null);
2454
- this.tree.setModified(ChangeType.structure);
2455
- return nodeList[0];
2607
+ tree.setModified(ChangeType.structure);
2456
2608
  }
2457
2609
  finally {
2458
- this.tree.enableUpdate(true);
2610
+ tree.enableUpdate(true);
2459
2611
  }
2612
+ // if(isTopCall && loadLazy){
2613
+ // this.logWarn("addChildren(): loadLazy is not yet implemented.")
2614
+ // }
2615
+ return nodeList[0];
2460
2616
  }
2461
2617
  /**
2462
2618
  * Append or prepend a node, or append a child node.
@@ -2530,21 +2686,98 @@
2530
2686
  }
2531
2687
  }
2532
2688
  }
2533
- /** Call `setExpanded()` on al child nodes*/
2534
- async expandAll(flag = true) {
2535
- this.visit((node) => {
2536
- node.setExpanded(flag);
2537
- });
2689
+ /** Call `setExpanded()` on all descendant nodes. */
2690
+ async expandAll(flag = true, options) {
2691
+ const tree = this.tree;
2692
+ const minExpandLevel = this.tree.options.minExpandLevel;
2693
+ let { depth = 99, loadLazy, force } = options !== null && options !== void 0 ? options : {};
2694
+ const expand_opts = {
2695
+ scrollIntoView: false,
2696
+ force: force,
2697
+ loadLazy: loadLazy,
2698
+ };
2699
+ // this.logInfo(`expandAll(${flag})`);
2700
+ // Expand all direct children in parallel:
2701
+ async function _iter(n, level) {
2702
+ var _a;
2703
+ // n.logInfo(` _iter(${level})`);
2704
+ if (level === 0) {
2705
+ return;
2706
+ }
2707
+ // if (!flag && minExpandLevel && !force && n.getLevel() <= minExpandLevel) {
2708
+ // return; // Do not collapse until minExpandLevel
2709
+ // }
2710
+ const level_1 = level == null ? null : level - 1;
2711
+ const promises = [];
2712
+ (_a = n.children) === null || _a === void 0 ? void 0 : _a.forEach((cn) => {
2713
+ if (flag) {
2714
+ if (!cn.expanded && (cn.children || (loadLazy && cn.lazy))) {
2715
+ // Node is collapsed and may be expanded (i.e. has children or is lazy)
2716
+ // Expanding may be async, so we store the promise.
2717
+ // Also the recursion is delayed until expansion finished.
2718
+ const p = cn.setExpanded(true, expand_opts);
2719
+ promises.push(p);
2720
+ p.then(async () => {
2721
+ await _iter(cn, level_1);
2722
+ });
2723
+ }
2724
+ else {
2725
+ // We don't expand the node, but still visit descendants.
2726
+ // There we may find lazy nodes, so we
2727
+ promises.push(_iter(cn, level_1));
2728
+ }
2729
+ }
2730
+ else {
2731
+ // Collapsing is always synchronous, so no promises required
2732
+ if (!minExpandLevel || force || cn.getLevel() > minExpandLevel) {
2733
+ // Do not collapse until minExpandLevel
2734
+ cn.setExpanded(false, expand_opts);
2735
+ }
2736
+ _iter(cn, level_1); // recursion, even if cn was already collapsed
2737
+ }
2738
+ });
2739
+ return new Promise((resolve) => {
2740
+ Promise.all(promises).then(() => {
2741
+ resolve(true);
2742
+ });
2743
+ });
2744
+ }
2745
+ const tag = tree.logTime(`${this}.expandAll(${flag})`);
2746
+ try {
2747
+ tree.enableUpdate(false);
2748
+ await _iter(this, depth);
2749
+ }
2750
+ finally {
2751
+ tree.enableUpdate(true);
2752
+ tree.logTimeEnd(tag);
2753
+ }
2538
2754
  }
2539
- /**Find all nodes that match condition (excluding self).
2755
+ /**
2756
+ * Find all descendant nodes that match condition (excluding self).
2540
2757
  *
2541
- * @param {string | function(node)} match title string to search for, or a
2542
- * callback function that returns `true` if a node is matched.
2758
+ * If `match` is a string, search for exact node title.
2759
+ * If `match` is a RegExp expression, apply it to node.title, using
2760
+ * [RegExp.test()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test).
2761
+ * If `match` is a callback, match all nodes for that the callback(node) returns true.
2762
+ *
2763
+ * Returns an empty array if no nodes were found.
2764
+ *
2765
+ * Examples:
2766
+ * ```js
2767
+ * // Match all node titles that match exactly 'Joe':
2768
+ * nodeList = node.findAll("Joe")
2769
+ * // Match all node titles that start with 'Joe' case sensitive:
2770
+ * nodeList = node.findAll(/^Joe/)
2771
+ * // Match all node titles that contain 'oe', case insensitive:
2772
+ * nodeList = node.findAll(/oe/i)
2773
+ * // Match all nodes with `data.price` >= 99:
2774
+ * nodeList = node.findAll((n) => {
2775
+ * return n.data.price >= 99;
2776
+ * })
2777
+ * ```
2543
2778
  */
2544
2779
  findAll(match) {
2545
- const matcher = isFunction(match)
2546
- ? match
2547
- : makeNodeTitleMatcher(match);
2780
+ const matcher = typeof match === "function" ? match : makeNodeTitleMatcher(match);
2548
2781
  const res = [];
2549
2782
  this.visit((n) => {
2550
2783
  if (matcher(n)) {
@@ -2574,15 +2807,13 @@
2574
2807
  }
2575
2808
  return null;
2576
2809
  }
2577
- /**Find first node that matches condition (excluding self).
2810
+ /**
2811
+ * Find first descendant node that matches condition (excluding self) or null.
2578
2812
  *
2579
- * @param match title string to search for, or a
2580
- * callback function that returns `true` if a node is matched.
2813
+ * @see {@link WunderbaumNode.findAll} for examples.
2581
2814
  */
2582
2815
  findFirst(match) {
2583
- const matcher = isFunction(match)
2584
- ? match
2585
- : makeNodeTitleMatcher(match);
2816
+ const matcher = typeof match === "function" ? match : makeNodeTitleMatcher(match);
2586
2817
  let res = null;
2587
2818
  this.visit((n) => {
2588
2819
  if (matcher(n)) {
@@ -2746,7 +2977,8 @@
2746
2977
  * an expand operation is currently possible.
2747
2978
  */
2748
2979
  isExpandable(andCollapsed = false) {
2749
- return !!this.children && (!this.expanded || !andCollapsed);
2980
+ // return !!this.children && (!this.expanded || !andCollapsed);
2981
+ return !!(this.children || this.lazy) && (!this.expanded || !andCollapsed);
2750
2982
  }
2751
2983
  /** Return true if this node is currently in edit-title mode. */
2752
2984
  isEditing() {
@@ -2847,14 +3079,20 @@
2847
3079
  // this.debug("isVisible: VISIBLE");
2848
3080
  return true;
2849
3081
  }
2850
- _loadSourceObject(source) {
3082
+ _loadSourceObject(source, level) {
3083
+ var _a;
2851
3084
  const tree = this.tree;
3085
+ level !== null && level !== void 0 ? level : (level = this.getLevel());
2852
3086
  // Let caller modify the parsed JSON response:
2853
3087
  this._callEvent("receive", { response: source });
2854
3088
  if (isArray(source)) {
2855
3089
  source = { children: source };
2856
3090
  }
2857
3091
  assert(isPlainObject(source));
3092
+ const format = (_a = source.format) !== null && _a !== void 0 ? _a : "nested";
3093
+ assert(format === "nested" || format === "flat");
3094
+ // Pre-rocess for 'nested' or 'flat' format
3095
+ inflateSourceData(source);
2858
3096
  assert(source.children, "If `source` is an object, it must have a `children` property");
2859
3097
  if (source.types) {
2860
3098
  tree.logInfo("Redefine types", source.columns);
@@ -2868,7 +3106,6 @@
2868
3106
  tree.updateColumns({ calculateCols: false });
2869
3107
  }
2870
3108
  this.addChildren(source.children);
2871
- delete source.columns;
2872
3109
  // Add extra data to `tree.data`
2873
3110
  for (const [key, value] of Object.entries(source)) {
2874
3111
  if (!RESERVED_TREE_SOURCE_KEYS.has(key)) {
@@ -3015,17 +3252,16 @@
3015
3252
  }
3016
3253
  /** Expand all parents and optionally scroll into visible area as neccessary.
3017
3254
  * Promise is resolved, when lazy loading and animations are done.
3018
- * @param {object} [opts] passed to `setExpanded()`.
3255
+ * @param {object} [options] passed to `setExpanded()`.
3019
3256
  * Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true}
3020
3257
  */
3021
- async makeVisible(opts) {
3022
- let i, dfd = new Deferred(), deferreds = [], parents = this.getParentList(false, false), len = parents.length,
3023
- // effects = !(opts && opts.noAnimation === true),
3024
- scroll = !(opts && opts.scrollIntoView === false);
3258
+ async makeVisible(options) {
3259
+ let i, dfd = new Deferred(), deferreds = [], parents = this.getParentList(false, false), len = parents.length, noAnimation = getOption(options, "noAnimation", false), scroll = getOption(options, "scrollIntoView", true);
3260
+ // scroll = !(options && options.scrollIntoView === false);
3025
3261
  // Expand bottom-up, so only the top node is animated
3026
3262
  for (i = len - 1; i >= 0; i--) {
3027
3263
  // self.debug("pushexpand" + parents[i]);
3028
- const seOpts = { noAnimation: opts === null || opts === void 0 ? void 0 : opts.noAnimation };
3264
+ const seOpts = { noAnimation: noAnimation };
3029
3265
  deferreds.push(parents[i].setExpanded(true, seOpts));
3030
3266
  }
3031
3267
  Promise.all(deferreds).then(() => {
@@ -3267,6 +3503,9 @@
3267
3503
  else if (this.children) {
3268
3504
  icon = iconMap.folder;
3269
3505
  }
3506
+ else if (this.lazy) {
3507
+ icon = iconMap.folderLazy;
3508
+ }
3270
3509
  else {
3271
3510
  icon = iconMap.doc;
3272
3511
  }
@@ -3430,13 +3669,13 @@
3430
3669
  if (isColspan) {
3431
3670
  let vpWidth = tree.element.clientWidth;
3432
3671
  titleSpan.style.width =
3433
- vpWidth - nodeElem._ofsTitlePx - ROW_EXTRA_PAD + "px";
3672
+ vpWidth - nodeElem._ofsTitlePx - TITLE_SPAN_PAD_Y + "px";
3434
3673
  }
3435
3674
  else {
3436
3675
  titleSpan.style.width =
3437
3676
  columns[0]._widthPx -
3438
3677
  nodeElem._ofsTitlePx -
3439
- ROW_EXTRA_PAD +
3678
+ TITLE_SPAN_PAD_Y +
3440
3679
  "px";
3441
3680
  }
3442
3681
  }
@@ -3749,22 +3988,32 @@
3749
3988
  * Expand or collapse this node.
3750
3989
  */
3751
3990
  async setExpanded(flag = true, options) {
3991
+ const { force, scrollIntoView, immediate } = options !== null && options !== void 0 ? options : {};
3752
3992
  if (!flag &&
3753
3993
  this.isExpanded() &&
3754
3994
  this.getLevel() <= this.tree.getOption("minExpandLevel") &&
3755
- !getOption(options, "force")) {
3995
+ !force) {
3756
3996
  this.logDebug("Ignored collapse request below expandLevel.");
3757
3997
  return;
3758
3998
  }
3759
3999
  if (!flag === !this.expanded) {
3760
4000
  return; // Nothing to do
3761
4001
  }
4002
+ // this.log("setExpanded()");
3762
4003
  if (flag && this.lazy && this.children == null) {
3763
4004
  await this.loadLazy();
3764
4005
  }
3765
4006
  this.expanded = flag;
3766
- const updateOpts = { immediate: !!getOption(options, "immediate") };
4007
+ const updateOpts = { immediate: immediate };
4008
+ // const updateOpts = { immediate: !!util.getOption(options, "immediate") };
3767
4009
  this.tree.setModified(ChangeType.structure, updateOpts);
4010
+ if (flag && scrollIntoView !== false) {
4011
+ const lastChild = this.getLastChild();
4012
+ if (lastChild) {
4013
+ this.tree.updatePendingModifications();
4014
+ lastChild.scrollIntoView({ topNode: this });
4015
+ }
4016
+ }
3768
4017
  }
3769
4018
  /**
3770
4019
  * Set keyboard focus here.
@@ -3993,7 +4242,7 @@
3993
4242
  /*!
3994
4243
  * Wunderbaum - ext-edit
3995
4244
  * Copyright (c) 2021-2022, Martin Wendt. Released under the MIT license.
3996
- * v0.0.6, Sat, 10 Sep 2022 19:29:21 GMT (https://github.com/mar10/wunderbaum)
4245
+ * v0.0.8, Fri, 23 Sep 2022 20:47:29 GMT (https://github.com/mar10/wunderbaum)
3997
4246
  */
3998
4247
  // const START_MARKER = "\uFFF7";
3999
4248
  class EditExtension extends WunderbaumExtension {
@@ -4139,8 +4388,9 @@
4139
4388
  node.logInfo("beforeEdit canceled operation.");
4140
4389
  return;
4141
4390
  }
4142
- // `beforeEdit(e)` may return an input HTML string. Otherwise use a default:
4143
- if (!inputHtml) {
4391
+ // `beforeEdit(e)` may return an input HTML string. Otherwise use a default.
4392
+ // (we also treat a `true` return value as 'use default'):
4393
+ if (inputHtml === true || !inputHtml) {
4144
4394
  const title = escapeHtml(node.title);
4145
4395
  inputHtml = `<input type=text class="wb-input-edit" value="${title}" required autocorrect=off>`;
4146
4396
  }
@@ -4285,8 +4535,8 @@
4285
4535
  * https://github.com/mar10/wunderbaum
4286
4536
  *
4287
4537
  * Released under the MIT license.
4288
- * @version v0.0.6
4289
- * @date Sat, 10 Sep 2022 19:29:21 GMT
4538
+ * @version v0.0.8
4539
+ * @date Fri, 23 Sep 2022 20:47:29 GMT
4290
4540
  */
4291
4541
  class WbSystemRoot extends WunderbaumNode {
4292
4542
  constructor(tree) {
@@ -4857,22 +5107,22 @@
4857
5107
  * - 'down', 'first', 'last', 'left', 'parent', 'right', 'up': navigate
4858
5108
  *
4859
5109
  */
4860
- applyCommand(cmd, nodeOrOpts, opts) {
5110
+ applyCommand(cmd, nodeOrOpts, options) {
4861
5111
  let // clipboard,
4862
5112
  node, refNode;
4863
- // opts = $.extend(
5113
+ // options = $.extend(
4864
5114
  // { setActive: true, clipboard: CLIPBOARD },
4865
- // opts_
5115
+ // options_
4866
5116
  // );
4867
5117
  if (nodeOrOpts instanceof WunderbaumNode) {
4868
5118
  node = nodeOrOpts;
4869
5119
  }
4870
5120
  else {
4871
5121
  node = this.getActiveNode();
4872
- assert(opts === undefined);
4873
- opts = nodeOrOpts;
5122
+ assert(options === undefined);
5123
+ options = nodeOrOpts;
4874
5124
  }
4875
- // clipboard = opts.clipboard;
5125
+ // clipboard = options.clipboard;
4876
5126
  switch (cmd) {
4877
5127
  // Sorting and indentation:
4878
5128
  case "moveUp":
@@ -5078,16 +5328,8 @@
5078
5328
  }
5079
5329
  }
5080
5330
  /** Recursively expand all expandable nodes (triggers lazy load id needed). */
5081
- async expandAll(flag = true) {
5082
- const tag = this.logTime("expandAll(" + flag + ")");
5083
- try {
5084
- this.enableUpdate(false);
5085
- await this.root.expandAll(flag);
5086
- }
5087
- finally {
5088
- this.enableUpdate(true);
5089
- this.logTimeEnd(tag);
5090
- }
5331
+ async expandAll(flag = true, options) {
5332
+ await this.root.expandAll(flag, options);
5091
5333
  }
5092
5334
  /** Recursively select all nodes. */
5093
5335
  selectAll(flag = true) {
@@ -5121,10 +5363,7 @@
5121
5363
  // util.assert(this.keyMap.size === i);
5122
5364
  }
5123
5365
  /**
5124
- * Find all nodes that matches condition.
5125
- *
5126
- * @param match title string to search for, or a
5127
- * callback function that returns `true` if a node is matched.
5366
+ * Find all nodes that match condition.
5128
5367
  *
5129
5368
  * @see {@link WunderbaumNode.findAll}
5130
5369
  */
@@ -5134,10 +5373,7 @@
5134
5373
  /**
5135
5374
  * Find first node that matches condition.
5136
5375
  *
5137
- * @param match title string to search for, or a
5138
- * callback function that returns `true` if a node is matched.
5139
5376
  * @see {@link WunderbaumNode.findFirst}
5140
- *
5141
5377
  */
5142
5378
  findFirst(match) {
5143
5379
  return this.root.findFirst(match);
@@ -5151,7 +5387,7 @@
5151
5387
  *
5152
5388
  */
5153
5389
  findKey(key) {
5154
- return this.keyMap.get(key);
5390
+ return this.keyMap.get(key) || null;
5155
5391
  }
5156
5392
  /**
5157
5393
  * Find the next visible node that starts with `match`, starting at `startNode`
@@ -5440,17 +5676,20 @@
5440
5676
  }
5441
5677
  /**
5442
5678
  * Make sure that this node is vertically scrolled into the viewport.
5679
+ *
5680
+ * Nodes that are above the visible area become the top row, nodes that are
5681
+ * below the viewport become the bottom row.
5443
5682
  */
5444
5683
  scrollTo(nodeOrOpts) {
5445
5684
  const PADDING = 2; // leave some pixels between viewport bounds
5446
5685
  let node;
5447
- let opts;
5686
+ let options;
5448
5687
  if (nodeOrOpts instanceof WunderbaumNode) {
5449
5688
  node = nodeOrOpts;
5450
5689
  }
5451
5690
  else {
5452
- opts = nodeOrOpts;
5453
- node = opts.node;
5691
+ options = nodeOrOpts;
5692
+ node = options.node;
5454
5693
  }
5455
5694
  assert(node && node._rowIdx != null);
5456
5695
  const scrollParent = this.element;
@@ -5461,6 +5700,7 @@
5461
5700
  const vpTop = headerHeight;
5462
5701
  const vpRowTop = rowTop - scrollTop;
5463
5702
  const vpRowBottom = vpRowTop + ROW_HEIGHT;
5703
+ const topNode = options === null || options === void 0 ? void 0 : options.topNode;
5464
5704
  // this.log( `scrollTo(${node.title}), vpTop:${vpTop}px, scrollTop:${scrollTop}, vpHeight:${vpHeight}, rowTop:${rowTop}, vpRowTop:${vpRowTop}`, nodeOrOpts );
5465
5705
  let newScrollTop = null;
5466
5706
  if (vpRowTop >= vpTop) {
@@ -5468,17 +5708,21 @@
5468
5708
  else {
5469
5709
  // Node is below viewport
5470
5710
  // this.log("Below viewport");
5471
- newScrollTop = rowTop + ROW_HEIGHT - vpHeight + PADDING; // leave some pixels between vieeport bounds
5711
+ newScrollTop = rowTop + ROW_HEIGHT - vpHeight + PADDING; // leave some pixels between viewport bounds
5472
5712
  }
5473
5713
  }
5474
5714
  else {
5475
5715
  // Node is above viewport
5476
5716
  // this.log("Above viewport");
5477
- newScrollTop = rowTop - vpTop - PADDING; // leave some pixels between vieeport bounds
5717
+ newScrollTop = rowTop - vpTop - PADDING; // leave some pixels between viewport bounds
5478
5718
  }
5479
5719
  if (newScrollTop != null) {
5480
5720
  this.log(`scrollTo(${rowTop}): ${scrollTop} => ${newScrollTop}`);
5481
5721
  scrollParent.scrollTop = newScrollTop;
5722
+ if (topNode) {
5723
+ // Make sure the topNode is always visible
5724
+ this.scrollTo(topNode);
5725
+ }
5482
5726
  // this.setModified(ChangeType.vscroll);
5483
5727
  }
5484
5728
  }
@@ -5507,10 +5751,9 @@
5507
5751
  newLeft = colRight - vpWidth;
5508
5752
  }
5509
5753
  // util.assert(node._rowIdx != null);
5510
- // const curLeft = this.scrollContainer.scrollLeft;
5511
- this.log(`scrollToHorz(${this.activeColIdx}): ${colLeft}..${colRight}, fixedOfs=${fixedWidth}, vpWidth=${vpWidth}, curLeft=${scrollLeft} -> ${newLeft}`);
5512
- // const nodeOfs = node._rowIdx * ROW_HEIGHT;
5513
- // let newLeft;
5754
+ // this.log(
5755
+ // `scrollToHorz(${this.activeColIdx}): ${colLeft}..${colRight}, fixedOfs=${fixedWidth}, vpWidth=${vpWidth}, curLeft=${scrollLeft} -> ${newLeft}`
5756
+ // );
5514
5757
  this.element.scrollLeft = newLeft;
5515
5758
  // this.setModified(ChangeType.vscroll);
5516
5759
  // }
@@ -5701,11 +5944,13 @@
5701
5944
  }
5702
5945
  }
5703
5946
  /** Update column headers and width. */
5704
- updateColumns(opts) {
5705
- opts = Object.assign({ calculateCols: true, updateRows: true }, opts);
5947
+ updateColumns(options) {
5948
+ options = Object.assign({ calculateCols: true, updateRows: true }, options);
5706
5949
  const defaultMinWidth = 4;
5707
5950
  const vpWidth = this.element.clientWidth;
5708
5951
  const isGrid = this.isGrid();
5952
+ // Shorten last column width to avoid h-scrollbar
5953
+ const FIX_ADJUST_LAST_COL = 2;
5709
5954
  let totalWidth = 0;
5710
5955
  let totalWeight = 0;
5711
5956
  let fixedWidth = 0;
@@ -5714,7 +5959,7 @@
5714
5959
  if (!isGrid && this.isCellNav()) {
5715
5960
  this.setCellNav(false);
5716
5961
  }
5717
- if (opts.calculateCols) {
5962
+ if (options.calculateCols) {
5718
5963
  // Gather width definitions
5719
5964
  this._columnsById = {};
5720
5965
  for (let col of this.columns) {
@@ -5766,7 +6011,8 @@
5766
6011
  col._ofsPx = ofsPx;
5767
6012
  ofsPx += col._widthPx;
5768
6013
  }
5769
- totalWidth = ofsPx;
6014
+ this.columns[this.columns.length - 1]._widthPx -= FIX_ADJUST_LAST_COL;
6015
+ totalWidth = ofsPx - FIX_ADJUST_LAST_COL;
5770
6016
  }
5771
6017
  // if (this.options.fixedCol) {
5772
6018
  // 'position: fixed' requires that the content has the correct size
@@ -5780,7 +6026,7 @@
5780
6026
  // util.error("BREAK");
5781
6027
  if (modified) {
5782
6028
  this._renderHeaderMarkup();
5783
- if (opts.updateRows) {
6029
+ if (options.updateRows) {
5784
6030
  this._updateRows();
5785
6031
  }
5786
6032
  }
@@ -5843,6 +6089,8 @@
5843
6089
  */
5844
6090
  _updateViewportImmediately() {
5845
6091
  var _a;
6092
+ // Shorten container height to avoid v-scrollbar
6093
+ const FIX_ADJUST_HEIGHT = 1;
5846
6094
  if (this._disableUpdateCount) {
5847
6095
  this.log(`IGNORED _updateViewportImmediately() disable level: ${this._disableUpdateCount}`);
5848
6096
  return;
@@ -5856,7 +6104,7 @@
5856
6104
  // let headerHeight = this.headerElement.children[0].children[0].clientHeight;
5857
6105
  // const headerHeight = this.options.headerHeightPx;
5858
6106
  const headerHeight = this.headerElement.clientHeight; // May be 0
5859
- const wantHeight = this.element.clientHeight - headerHeight;
6107
+ const wantHeight = this.element.clientHeight - headerHeight - FIX_ADJUST_HEIGHT;
5860
6108
  if (Math.abs(height - wantHeight) > 1.0) {
5861
6109
  // this.log("resize", height, wantHeight);
5862
6110
  this.scrollContainerElement.style.height = wantHeight + "px";
@@ -5913,14 +6161,15 @@
5913
6161
  * (including upper and lower prefetch)
5914
6162
  * -
5915
6163
  */
5916
- _updateRows(opts) {
6164
+ _updateRows(options) {
5917
6165
  // const label = this.logTime("_updateRows");
5918
6166
  // this.log("_updateRows", opts)
5919
- opts = Object.assign({ newNodesOnly: false }, opts);
5920
- const newNodesOnly = !!opts.newNodesOnly;
6167
+ options = Object.assign({ newNodesOnly: false }, options);
6168
+ const newNodesOnly = !!options.newNodesOnly;
5921
6169
  const row_height = ROW_HEIGHT;
5922
6170
  const vp_height = this.element.clientHeight;
5923
6171
  const prefetch = RENDER_MAX_PREFETCH;
6172
+ // const grace_prefetch = RENDER_MAX_PREFETCH - RENDER_MIN_PREFETCH;
5924
6173
  const ofs = this.element.scrollTop;
5925
6174
  let startIdx = Math.max(0, ofs / row_height - prefetch);
5926
6175
  startIdx = Math.floor(startIdx);
@@ -6019,16 +6268,16 @@
6019
6268
  * {start: First tree node, reverse: false, includeSelf: true, includeHidden: false, wrap: false}
6020
6269
  * @returns {boolean} false if iteration was canceled
6021
6270
  */
6022
- visitRows(callback, opts) {
6271
+ visitRows(callback, options) {
6023
6272
  if (!this.root.hasChildren()) {
6024
6273
  return false;
6025
6274
  }
6026
- if (opts && opts.reverse) {
6027
- delete opts.reverse;
6028
- return this._visitRowsUp(callback, opts);
6275
+ if (options && options.reverse) {
6276
+ delete options.reverse;
6277
+ return this._visitRowsUp(callback, options);
6029
6278
  }
6030
- opts = opts || {};
6031
- let i, nextIdx, parent, res, siblings, stopNode, siblingOfs = 0, skipFirstNode = opts.includeSelf === false, includeHidden = !!opts.includeHidden, checkFilter = !includeHidden && this.filterMode === "hide", node = opts.start || this.root.children[0];
6279
+ options = options || {};
6280
+ let i, nextIdx, parent, res, siblings, stopNode, siblingOfs = 0, skipFirstNode = options.includeSelf === false, includeHidden = !!options.includeHidden, checkFilter = !includeHidden && this.filterMode === "hide", node = options.start || this.root.children[0];
6032
6281
  parent = node.parent;
6033
6282
  while (parent) {
6034
6283
  // visit siblings
@@ -6077,11 +6326,11 @@
6077
6326
  node = parent;
6078
6327
  parent = parent.parent;
6079
6328
  siblingOfs = 1; //
6080
- if (!parent && opts.wrap) {
6329
+ if (!parent && options.wrap) {
6081
6330
  this.logDebug("visitRows(): wrap around");
6082
- assert(opts.start, "`wrap` option requires `start`");
6083
- stopNode = opts.start;
6084
- opts.wrap = false;
6331
+ assert(options.start, "`wrap` option requires `start`");
6332
+ stopNode = options.start;
6333
+ options.wrap = false;
6085
6334
  parent = this.root;
6086
6335
  siblingOfs = 0;
6087
6336
  }
@@ -6216,7 +6465,7 @@
6216
6465
  }
6217
6466
  Wunderbaum.sequence = 0;
6218
6467
  /** Wunderbaum release version number "MAJOR.MINOR.PATCH". */
6219
- Wunderbaum.version = "v0.0.6"; // Set to semver by 'grunt release'
6468
+ Wunderbaum.version = "v0.0.8"; // Set to semver by 'grunt release'
6220
6469
  /** Expose some useful methods of the util.ts module as `Wunderbaum.util`. */
6221
6470
  Wunderbaum.util = util;
6222
6471