wunderbaum 0.7.0 → 0.8.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.
@@ -4,7 +4,7 @@
4
4
  * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
5
5
  */
6
6
 
7
- import { NavModeEnum } from "./types";
7
+ import { KeynavOptionsType, NavModeEnum } from "./types";
8
8
  import { eventToString } from "./util";
9
9
  import { Wunderbaum } from "./wunderbaum";
10
10
  import { WunderbaumNode } from "./wb_node";
@@ -12,7 +12,7 @@ import { WunderbaumExtension } from "./wb_extension_base";
12
12
 
13
13
  const QUICKSEARCH_DELAY = 500;
14
14
 
15
- export class KeynavExtension extends WunderbaumExtension<any> {
15
+ export class KeynavExtension extends WunderbaumExtension<KeynavOptionsType> {
16
16
  constructor(tree: Wunderbaum) {
17
17
  super(tree, "keynav", {});
18
18
  }
@@ -32,6 +32,14 @@ export class KeynavExtension extends WunderbaumExtension<any> {
32
32
  return input;
33
33
  }
34
34
 
35
+ // /* Return the current cell's embedded input that has keyboard focus. */
36
+ // protected _getFocusedInputElem(): HTMLInputElement | null {
37
+ // const ace = this.tree
38
+ // .getActiveColElem()
39
+ // ?.querySelector<HTMLInputElement>("input:focus,select:focus");
40
+ // return ace || null;
41
+ // }
42
+
35
43
  /* Return true if the current cell's embedded input has keyboard focus. */
36
44
  protected _isCurInputFocused(): boolean {
37
45
  const ace = this.tree
@@ -48,7 +56,6 @@ export class KeynavExtension extends WunderbaumExtension<any> {
48
56
  const curInput = this._getEmbeddedInputElem(event.target);
49
57
  const inputHasFocus = curInput && this._isCurInputFocused();
50
58
  const navModeOption = opts.navigationModeOption as NavModeEnum;
51
- // isCellEditMode = tree.navMode === NavigationMode.cellEdit;
52
59
 
53
60
  let focusNode,
54
61
  eventName = eventToString(event),
@@ -215,23 +222,28 @@ export class KeynavExtension extends WunderbaumExtension<any> {
215
222
  // // Standard navigation (cell mode)
216
223
  // if (isCellEditMode && INPUT_BREAKOUT_KEYS.has(eventName)) {
217
224
  // }
218
- const ENTER_EDITS = false;
219
- const curInput = this._getEmbeddedInputElem(null);
225
+ // const curInput = this._getEmbeddedInputElem(null);
220
226
  const curInputType = curInput ? curInput.type || curInput.tagName : "";
221
- const inputHasFocus = curInput && this._isCurInputFocused();
227
+ // const inputHasFocus = curInput && this._isCurInputFocused();
222
228
  const inputCanFocus = curInput && curInputType !== "checkbox";
223
229
 
224
230
  if (inputHasFocus) {
225
231
  if (eventName === "Escape") {
226
- // Discard changes
232
+ node.logDebug(`Reset focused input on Escape`);
233
+ // Discard changes and reset input validation state
234
+ curInput.setCustomValidity("");
227
235
  node._render();
228
236
  // Keep cell-nav mode
229
- node.logDebug(`Reset focused input`);
230
237
  tree.setFocus();
231
238
  tree.setColumn(tree.activeColIdx);
232
239
  return;
233
240
  // } else if (!INPUT_BREAKOUT_KEYS.has(eventName)) {
234
241
  } else if (eventName !== "Enter") {
242
+ if (curInput && curInput.checkValidity && !curInput.checkValidity()) {
243
+ // Invalid input: ignore all keys except Enter and Escape
244
+ node.logDebug(`Ignored ${eventName} inside invalid input`);
245
+ return false;
246
+ }
235
247
  // Let current `<input>` handle it
236
248
  node.logDebug(`Ignored ${eventName} inside focused input`);
237
249
  return;
@@ -245,9 +257,10 @@ export class KeynavExtension extends WunderbaumExtension<any> {
245
257
  } else if (curInput) {
246
258
  // On a cell that has an embedded, unfocused <input>
247
259
  if (eventName.length === 1 && inputCanFocus) {
260
+ // Typing a single char
248
261
  curInput.focus();
249
262
  curInput.value = "";
250
- node.logDebug(`Focus imput: ${eventName}`);
263
+ node.logDebug(`Focus input: ${eventName}`);
251
264
  return false;
252
265
  }
253
266
  }
@@ -257,8 +270,6 @@ export class KeynavExtension extends WunderbaumExtension<any> {
257
270
  } else if (eventName === "Shift+Tab") {
258
271
  eventName = tree.activeColIdx > 0 ? "ArrowLeft" : "";
259
272
  handled = true;
260
- } else if (ENTER_EDITS && eventName === "Enter") {
261
- eventName = "F2";
262
273
  }
263
274
 
264
275
  switch (eventName) {
@@ -4,11 +4,12 @@
4
4
  * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
5
5
  */
6
6
 
7
+ import { LoggerOptionsType } from "./types";
7
8
  import { overrideMethod } from "./util";
8
9
  import { WunderbaumExtension } from "./wb_extension_base";
9
10
  import { Wunderbaum } from "./wunderbaum";
10
11
 
11
- export class LoggerExtension extends WunderbaumExtension<any> {
12
+ export class LoggerExtension extends WunderbaumExtension<LoggerOptionsType> {
12
13
  readonly prefix: string;
13
14
  protected ignoreEvents = new Set<string>([
14
15
  "iconBadge",
package/src/wb_node.ts CHANGED
@@ -420,6 +420,11 @@ export class WunderbaumNode {
420
420
  }
421
421
  }
422
422
 
423
+ /** Start editing this node's title. */
424
+ startEditTitle(): void {
425
+ this.tree._callMethod("edit.startEditTitle", this);
426
+ }
427
+
423
428
  /** Call `setExpanded()` on all descendant nodes. */
424
429
  async expandAll(flag: boolean = true, options?: ExpandAllOptions) {
425
430
  const tree = this.tree;
@@ -648,6 +653,22 @@ export class WunderbaumNode {
648
653
  const colElems = this._rowElem?.querySelectorAll("span.wb-col");
649
654
  return colElems ? (colElems[colIdx] as HTMLSpanElement) : null;
650
655
  }
656
+ /**
657
+ * Return all nodes with the same refKey.
658
+ *
659
+ * @param includeSelf Include this node itself.
660
+ * @see {@link Wunderbaum.findByRefKey}
661
+ */
662
+ getCloneList(includeSelf = false): WunderbaumNode[] {
663
+ if (!this.refKey) {
664
+ return [];
665
+ }
666
+ const clones = this.tree.findByRefKey(this.refKey);
667
+ if (includeSelf) {
668
+ return clones;
669
+ }
670
+ return [...clones].filter((n) => n !== this);
671
+ }
651
672
 
652
673
  /** Return the first child node or null.
653
674
  * @returns {WunderbaumNode | null}
@@ -771,19 +792,24 @@ export class WunderbaumNode {
771
792
  }
772
793
 
773
794
  /** Return true if this node is a direct or indirect parent of `other`.
774
- * (See also [[isParentOf]].)
795
+ * @see {@link WunderbaumNode.isParentOf}
775
796
  */
776
797
  isAncestorOf(other: WunderbaumNode) {
777
798
  return other && other.isDescendantOf(this);
778
799
  }
779
800
 
780
801
  /** Return true if this node is a **direct** subnode of `other`.
781
- * (See also [[isDescendantOf]].)
802
+ * @see {@link WunderbaumNode.isDescendantOf}
782
803
  */
783
804
  isChildOf(other: WunderbaumNode) {
784
805
  return other && this.parent === other;
785
806
  }
786
807
 
808
+ /** Return true if this node's refKey is used by at least one other node.
809
+ */
810
+ isClone() {
811
+ return !!this.refKey && this.tree.findByRefKey(this.refKey).length > 1;
812
+ }
787
813
  /** Return true if this node's title spans all columns, i.e. the node has no
788
814
  * grid cells.
789
815
  */
@@ -792,7 +818,7 @@ export class WunderbaumNode {
792
818
  }
793
819
 
794
820
  /** Return true if this node is a direct or indirect subnode of `other`.
795
- * (See also [[isChildOf]].)
821
+ * @see {@link WunderbaumNode.isChildOf}
796
822
  */
797
823
  isDescendantOf(other: WunderbaumNode) {
798
824
  if (!other || other.tree !== this.tree) {
@@ -829,8 +855,11 @@ export class WunderbaumNode {
829
855
  return true;
830
856
  }
831
857
 
832
- /** Return true if this node is currently in edit-title mode. */
833
- isEditing(): boolean {
858
+ /** Return true if _this_ node is currently in edit-title mode.
859
+ *
860
+ * See {@link Wunderbaum.startEditTitle} to check if any node is currently edited.
861
+ */
862
+ isEditingTitle(): boolean {
834
863
  return this.tree._callMethod("edit.isEditingTitle", this);
835
864
  }
836
865
 
@@ -872,7 +901,7 @@ export class WunderbaumNode {
872
901
  }
873
902
 
874
903
  /** Return true if this node is a **direct** parent of `other`.
875
- * (See also [[isAncestorOf]].)
904
+ * @see {@link WunderbaumNode.isAncestorOf}
876
905
  */
877
906
  isParentOf(other: WunderbaumNode) {
878
907
  return other && other.parent === this;
@@ -899,7 +928,7 @@ export class WunderbaumNode {
899
928
  }
900
929
 
901
930
  /** Return true if this node is the (invisible) system root node.
902
- * (See also [[isTopLevel()]].)
931
+ * @see {@link WunderbaumNode.isTopLevel}
903
932
  */
904
933
  isRootNode(): boolean {
905
934
  return this.tree.root === this;
@@ -1152,14 +1181,22 @@ export class WunderbaumNode {
1152
1181
  }
1153
1182
  }
1154
1183
 
1155
- /**Load content of a lazy node. */
1156
- async loadLazy(forceReload = false) {
1184
+ /**
1185
+ * Load content of a lazy node.
1186
+ * If the node is already loaded, nothing happens.
1187
+ * @param [forceReload=false] If true, reload even if already loaded.
1188
+ */
1189
+ async loadLazy(forceReload: boolean = false) {
1157
1190
  const wasExpanded = this.expanded;
1158
1191
 
1159
1192
  util.assert(this.lazy, "load() requires a lazy node");
1160
- // _assert( forceReload || this.isUndefined(), "Pass forceReload=true to re-load a lazy node" );
1193
+
1161
1194
  if (!forceReload && !this.isUnloaded()) {
1162
- return;
1195
+ return; // Already loaded: nothing to do
1196
+ }
1197
+ if (this.isLoading()) {
1198
+ this.logWarn("loadLazy() called while already loading: ignored.");
1199
+ return; // Already loading: prevent duplicate requests
1163
1200
  }
1164
1201
  if (this.isLoaded()) {
1165
1202
  this.resetLazy(); // Also collapses if currently expanded
@@ -1180,7 +1217,7 @@ export class WunderbaumNode {
1180
1217
 
1181
1218
  await this.load(source);
1182
1219
 
1183
- this.setStatus(NodeStatusType.ok);
1220
+ this.setStatus(NodeStatusType.ok); // Also resets `this._isLoading`
1184
1221
 
1185
1222
  if (wasExpanded) {
1186
1223
  this.expanded = true;
@@ -1191,38 +1228,46 @@ export class WunderbaumNode {
1191
1228
  } catch (e) {
1192
1229
  this.logError("Error during loadLazy()", e);
1193
1230
  this._callEvent("error", { error: e });
1231
+ // Also resets `this._isLoading`:
1194
1232
  this.setStatus(NodeStatusType.error, { message: "" + e });
1195
1233
  }
1196
1234
  return;
1197
1235
  }
1198
1236
 
1199
- /** Alias for `logDebug` */
1237
+ /** Write to `console.log` with node name as prefix if opts.debugLevel >= 4.
1238
+ * @see {@link WunderbaumNode.logDebug}
1239
+ */
1200
1240
  log(...args: any[]) {
1201
- this.logDebug(...args);
1241
+ if (this.tree.options.debugLevel! >= 4) {
1242
+ console.log(this.toString(), ...args); // eslint-disable-line no-console
1243
+ }
1202
1244
  }
1203
1245
 
1204
- /* Log to console if opts.debugLevel >= 4 */
1246
+ /** Write to `console.debug` with node name as prefix if opts.debugLevel >= 4
1247
+ * and browser console level includes debug/verbose messages.
1248
+ * @see {@link WunderbaumNode.log}
1249
+ */
1205
1250
  logDebug(...args: any[]) {
1206
1251
  if (this.tree.options.debugLevel! >= 4) {
1207
- console.log(this.toString(), ...args); // eslint-disable-line no-console
1252
+ console.debug(this.toString(), ...args); // eslint-disable-line no-console
1208
1253
  }
1209
1254
  }
1210
1255
 
1211
- /* Log error to console. */
1256
+ /** Write to `console.error` with node name as prefix if opts.debugLevel >= 1. */
1212
1257
  logError(...args: any[]) {
1213
1258
  if (this.tree.options.debugLevel! >= 1) {
1214
1259
  console.error(this.toString(), ...args); // eslint-disable-line no-console
1215
1260
  }
1216
1261
  }
1217
1262
 
1218
- /* Log to console if opts.debugLevel >= 3 */
1263
+ /** Write to `console.info` with node name as prefix if opts.debugLevel >= 3. */
1219
1264
  logInfo(...args: any[]) {
1220
1265
  if (this.tree.options.debugLevel! >= 3) {
1221
1266
  console.info(this.toString(), ...args); // eslint-disable-line no-console
1222
1267
  }
1223
1268
  }
1224
1269
 
1225
- /* Log warning to console if opts.debugLevel >= 2 */
1270
+ /** Write to `console.warn` with node name as prefix if opts.debugLevel >= 2. */
1226
1271
  logWarn(...args: any[]) {
1227
1272
  if (this.tree.options.debugLevel! >= 2) {
1228
1273
  console.warn(this.toString(), ...args); // eslint-disable-line no-console
@@ -1425,11 +1470,11 @@ export class WunderbaumNode {
1425
1470
  if (!this.children) {
1426
1471
  return;
1427
1472
  }
1428
- if (tree.activeNode && tree.activeNode.isDescendantOf(this)) {
1473
+ if (tree.activeNode?.isDescendantOf(this)) {
1429
1474
  tree.activeNode.setActive(false); // TODO: don't fire events
1430
1475
  }
1431
- if (tree.focusNode && tree.focusNode.isDescendantOf(this)) {
1432
- tree.focusNode = null;
1476
+ if (tree.focusNode?.isDescendantOf(this)) {
1477
+ tree._setFocusNode(null);
1433
1478
  }
1434
1479
  // TODO: persist must take care to clear select and expand cookies
1435
1480
  // Unlink children to support GC
@@ -1613,7 +1658,7 @@ export class WunderbaumNode {
1613
1658
  // Attach a node reference to the DOM Element:
1614
1659
  (<any>rowDiv)._wb_node = this;
1615
1660
 
1616
- const nodeElem: HTMLElement = document.createElement("span");
1661
+ const nodeElem: HTMLSpanElement = document.createElement("span");
1617
1662
  nodeElem.classList.add("wb-node", "wb-col");
1618
1663
  rowDiv.appendChild(nodeElem);
1619
1664
 
@@ -1891,6 +1936,7 @@ export class WunderbaumNode {
1891
1936
  let i = 0;
1892
1937
  for (const colSpan of rowDiv.children) {
1893
1938
  colSpan.classList.toggle("wb-active", i++ === tree.activeColIdx);
1939
+ colSpan.classList.remove("wb-error", "wb-invalid");
1894
1940
  }
1895
1941
  // Update icon (if not opts.isNew, which would rebuild markup anyway)
1896
1942
  const iconSpan = nodeElem.querySelector("i.wb-icon") as HTMLElement;
@@ -2081,16 +2127,22 @@ export class WunderbaumNode {
2081
2127
  }
2082
2128
 
2083
2129
  /**
2084
- * Activate this node, deactivate previous, send events, activate column and scroll int viewport.
2130
+ * Activate this node, deactivate previous, send events, activate column and
2131
+ * scroll into viewport.
2085
2132
  */
2086
2133
  async setActive(flag: boolean = true, options?: SetActiveOptions) {
2087
2134
  const tree = this.tree;
2088
- const prev = tree.activeNode;
2135
+ const prev = tree.getActiveNode();
2089
2136
  const retrigger = options?.retrigger; // Default: false
2090
2137
  const focusTree = options?.focusTree; // Default: false
2091
- const focusNode = options?.focusNode !== false; // Default: true
2138
+ // const focusNode = options?.focusNode !== false; // Default: true
2092
2139
  const noEvents = options?.noEvents; // Default: false
2093
- const orgEvent = options?.event; // Default: false
2140
+ const orgEvent = options?.event; // Default: null
2141
+ const colIdx = options?.colIdx; // Default: null
2142
+ const edit = options?.edit; // Default: false
2143
+
2144
+ util.assert(!colIdx || tree.isCellNav(), "colIdx requires cellNav");
2145
+ util.assert(!edit || colIdx != null, "edit requires colIdx");
2094
2146
 
2095
2147
  if (!noEvents) {
2096
2148
  if (flag) {
@@ -2107,7 +2159,7 @@ export class WunderbaumNode {
2107
2159
  ) {
2108
2160
  return;
2109
2161
  }
2110
- tree.activeNode = null;
2162
+ tree._setActiveNode(null);
2111
2163
  prev?.update(ChangeType.status);
2112
2164
  }
2113
2165
  } else if (prev === this || retrigger) {
@@ -2117,29 +2169,30 @@ export class WunderbaumNode {
2117
2169
 
2118
2170
  if (prev !== this) {
2119
2171
  if (flag) {
2120
- tree.activeNode = this;
2121
- if (focusNode || focusTree) {
2122
- tree.focusNode = this;
2123
- }
2124
- if (focusTree) {
2125
- tree.setFocus();
2126
- }
2172
+ tree._setActiveNode(this);
2127
2173
  }
2128
2174
  prev?.update(ChangeType.status);
2129
2175
  this.update(ChangeType.status);
2130
2176
  }
2131
- if (
2132
- options &&
2133
- options.colIdx != null &&
2134
- options.colIdx !== tree.activeColIdx &&
2135
- tree.isCellNav()
2136
- ) {
2137
- tree.setColumn(options.colIdx);
2138
- }
2139
- if (flag && !noEvents) {
2140
- this._callEvent("activate", { prevNode: prev, event: orgEvent });
2141
- }
2142
- return this.makeVisible();
2177
+ return this.makeVisible().then(() => {
2178
+ if (flag) {
2179
+ if (focusTree || edit) {
2180
+ tree.setFocus();
2181
+ tree._setFocusNode(this);
2182
+ tree.focusNode!.setFocus();
2183
+ }
2184
+ // if (focusNode || edit) {
2185
+ // tree.focusNode = this;
2186
+ // tree.focusNode.setFocus();
2187
+ // }
2188
+ if (colIdx != null && tree.isCellNav()) {
2189
+ tree.setColumn(colIdx, { edit: edit });
2190
+ }
2191
+ if (!noEvents) {
2192
+ this._callEvent("activate", { prevNode: prev, event: orgEvent });
2193
+ }
2194
+ }
2195
+ });
2143
2196
  }
2144
2197
 
2145
2198
  /**
@@ -2147,18 +2200,26 @@ export class WunderbaumNode {
2147
2200
  */
2148
2201
  async setExpanded(flag: boolean = true, options?: SetExpandedOptions) {
2149
2202
  const { force, scrollIntoView, immediate } = options ?? {};
2203
+ const sendEvents = !options?.noEvents; // Default: send events
2150
2204
  if (
2151
2205
  !flag &&
2152
2206
  this.isExpanded() &&
2153
2207
  this.getLevel() <= this.tree.getOption("minExpandLevel") &&
2154
2208
  !force
2155
2209
  ) {
2156
- this.logDebug("Ignored collapse request below expandLevel.");
2210
+ this.logDebug("Ignored collapse request below minExpandLevel.");
2157
2211
  return;
2158
2212
  }
2159
2213
  if (!flag === !this.expanded) {
2160
2214
  return; // Nothing to do
2161
2215
  }
2216
+ if (
2217
+ sendEvents &&
2218
+ this._callEvent("beforeExpand", { flag: flag }) === false
2219
+ ) {
2220
+ return;
2221
+ }
2222
+
2162
2223
  // this.log("setExpanded()");
2163
2224
  if (flag && this.getOption("autoCollapse")) {
2164
2225
  this.collapseSiblings(options);
@@ -2177,6 +2238,9 @@ export class WunderbaumNode {
2177
2238
  lastChild.scrollIntoView({ topNode: this });
2178
2239
  }
2179
2240
  }
2241
+ if (sendEvents) {
2242
+ this._callEvent("expand", { flag: flag });
2243
+ }
2180
2244
  }
2181
2245
 
2182
2246
  /**
@@ -2184,9 +2248,9 @@ export class WunderbaumNode {
2184
2248
  * @see {@link setActive}
2185
2249
  */
2186
2250
  setFocus(flag: boolean = true) {
2187
- util.assert(!!flag, "blur is not yet implemented");
2251
+ util.assert(!!flag, "Blur is not yet implemented");
2188
2252
  const prev = this.tree.focusNode;
2189
- this.tree.focusNode = this;
2253
+ this.tree._setFocusNode(this);
2190
2254
  prev?.update();
2191
2255
  this.update();
2192
2256
  }
package/src/wb_options.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import {
8
+ WbCancelableEventResultType,
8
9
  ColumnDefinitionList,
9
10
  DndOptionsType,
10
11
  DynamicBoolOption,
@@ -13,6 +14,9 @@ import {
13
14
  DynamicIconOption,
14
15
  EditOptionsType,
15
16
  FilterOptionsType,
17
+ GridOptionsType,
18
+ KeynavOptionsType,
19
+ LoggerOptionsType,
16
20
  NavModeEnum,
17
21
  NodeTypeDefinitionMap,
18
22
  SelectModeType,
@@ -21,6 +25,7 @@ import {
21
25
  WbClickEventType,
22
26
  WbDeactivateEventType,
23
27
  WbErrorEventType,
28
+ WbExpandEventType,
24
29
  WbIconBadgeCallback,
25
30
  WbInitEventType,
26
31
  WbKeydownEventType,
@@ -28,11 +33,13 @@ import {
28
33
  WbNodeEventType,
29
34
  WbReceiveEventType,
30
35
  WbRenderEventType,
36
+ WbSelectEventType,
31
37
  WbTreeEventType,
38
+ WbIconBadgeEventResultType,
32
39
  } from "./types";
33
40
 
34
41
  /**
35
- * Available options for [[Wunderbaum]].
42
+ * Available options for {@link wunderbaum.Wunderbaum}.
36
43
  *
37
44
  * Options are passed to the constructor as plain object:
38
45
  *
@@ -230,24 +237,40 @@ export interface WunderbaumOptions {
230
237
  scrollIntoViewOnExpandClick?: boolean;
231
238
 
232
239
  // --- Extensions ------------------------------------------------------------
233
- dnd?: DndOptionsType; // = {};
234
- edit?: EditOptionsType; // = {};
235
- filter?: FilterOptionsType; // = {};
236
- grid?: any; // = {};
240
+
241
+ dnd?: DndOptionsType;
242
+ edit?: EditOptionsType;
243
+ filter?: FilterOptionsType;
244
+ // grid?: GridOptionsType;
245
+ // keynav?: KeynavOptionsType;
246
+ // logger?: LoggerOptionsType;
237
247
 
238
248
  // --- Events ----------------------------------------------------------------
239
249
 
240
250
  /**
241
- *
251
+ * `e.node` was activated.
242
252
  * @category Callback
243
253
  */
244
254
  activate?: (e: WbActivateEventType) => void;
255
+ /**
256
+ * `e.node` is about to be activated.
257
+ * Return `false` to prevent default handling, i.e. activating the node.
258
+ * See also `deactivate` event.
259
+ * @category Callback
260
+ */
261
+ beforeActivate?: (e: WbActivateEventType) => WbCancelableEventResultType;
262
+ /**
263
+ * `e.node` is about to be expanded/collapsed.
264
+ * Return `false` to prevent default handling, i.e. expanding/collapsing the node.
265
+ * @category Callback
266
+ */
267
+ beforeExpand?: (e: WbExpandEventType) => WbCancelableEventResultType;
245
268
  /**
246
269
  *
247
- * Return `false` to prevent default handling, e.g. activating the node.
270
+ * Return `false` to prevent default handling, i.e. (de)selecting the node.
248
271
  * @category Callback
249
272
  */
250
- beforeActivate?: (e: WbActivateEventType) => void;
273
+ beforeSelect?: (e: WbSelectEventType) => WbCancelableEventResultType;
251
274
  /**
252
275
  *
253
276
  * @category Callback
@@ -255,44 +278,46 @@ export interface WunderbaumOptions {
255
278
  change?: (e: WbChangeEventType) => void;
256
279
  /**
257
280
  *
258
- * Return `false` to prevent default handling, e.g. activating the node.
281
+ * Return `false` to prevent default behavior, e.g. expand/collapse, (de)selection, or activation.
259
282
  * @category Callback
260
283
  */
261
- click?: (e: WbClickEventType) => void;
284
+ click?: (e: WbClickEventType) => WbCancelableEventResultType;
262
285
  /**
263
- *
286
+ * Return `false` to prevent default behavior, e.g. expand/collapse.
264
287
  * @category Callback
265
288
  */
266
- dblclick?: (e: WbClickEventType) => void;
289
+ dblclick?: (e: WbClickEventType) => WbCancelableEventResultType;
267
290
  /**
291
+ * `e.node` was deactivated.
268
292
  *
269
293
  * Return `false` to prevent default handling, e.g. deactivating the node
270
294
  * and activating the next.
295
+ * See also `activate` event.
271
296
  * @category Callback
272
297
  */
273
- deactivate?: (e: WbDeactivateEventType) => void;
298
+ deactivate?: (e: WbDeactivateEventType) => WbCancelableEventResultType;
274
299
  /**
275
- *
300
+ * `e.node` was discarded from the viewport and its HTML markup removed.
276
301
  * @category Callback
277
302
  */
278
303
  discard?: (e: WbNodeEventType) => void;
279
304
  /**
280
- *
305
+ * `e.node` is about to be rendered. We can add a badge to the icon cell here.
281
306
  * @category Callback
282
307
  */
283
- iconBadge?: WbIconBadgeCallback;
284
- // /**
285
- // *
286
- // * @category Callback
287
- // */
288
- // enhanceTitle?: (e: WbEnhanceTitleEventType) => void;
308
+ iconBadge?: (e: WbIconBadgeCallback) => WbIconBadgeEventResultType;
289
309
  /**
290
- *
310
+ * An error occurred, e.g. during initialization or lazy loading.
291
311
  * @category Callback
292
312
  */
293
313
  error?: (e: WbErrorEventType) => void;
294
314
  /**
295
- *
315
+ * `e.node` was expanded (`e.flag === true`) or collapsed (`e.flag === false`)
316
+ * @category Callback
317
+ */
318
+ expand?: (e: WbTreeEventType) => void;
319
+ /**
320
+ * The tree received or lost focus.
296
321
  * Check `e.flag` for status.
297
322
  * @category Callback
298
323
  */
@@ -301,15 +326,17 @@ export interface WunderbaumOptions {
301
326
  * Fires when the tree markup was created and the initial source data was loaded.
302
327
  * Typical use cases would be activating a node, setting focus, enabling other
303
328
  * controls on the page, etc.<br>
304
- * Check `e.error` for status.
329
+ * Also sent if an error occured during initialization (check `e.error` for status).
305
330
  * @category Callback
306
331
  */
307
332
  init?: (e: WbInitEventType) => void;
308
333
  /**
309
- *
334
+ * Fires when a key was pressed while the tree has focus.
335
+ * `e.node` is set if a node is currently active.
336
+ * Return `false` to prevent default navigation.
310
337
  * @category Callback
311
338
  */
312
- keydown?: (e: WbKeydownEventType) => void;
339
+ keydown?: (e: WbKeydownEventType) => WbCancelableEventResultType;
313
340
  /**
314
341
  * Fires when a node that was marked 'lazy', is expanded for the first time.
315
342
  * Typically we return an endpoint URL or the Promise of a fetch request that
@@ -345,13 +372,12 @@ export interface WunderbaumOptions {
345
372
  */
346
373
  render?: (e: WbRenderEventType) => void;
347
374
  /**
348
- *
375
+ * Same as `render(e)`, but for the status nodes, i.e. `e.node.statusNodeType`.
349
376
  * @category Callback
350
377
  */
351
378
  renderStatusNode?: (e: WbRenderEventType) => void;
352
379
  /**
353
- *
354
- * Check `e.flag` for status.
380
+ *`e.node` was selected (`e.flag === true`) or deselected (`e.flag === false`)
355
381
  * @category Callback
356
382
  */
357
383
  select?: (e: WbNodeEventType) => void;
@@ -830,10 +830,16 @@ i.wb-icon {
830
830
 
831
831
  .wb-col input:invalid {
832
832
  // border-color: var(--wb-error-color);
833
- color: var(--wb-error-color);
833
+ // color: var(--wb-error-color);
834
834
  background-color: var(--wb-error-background-color);
835
835
  }
836
836
 
837
+ .wb-col.wb-invalid {
838
+ border: 1px dotted var(--wb-error-color);
839
+ // color: var(--wb-error-color);
840
+ // background-color: var(--wb-error-background-color);
841
+ }
842
+
837
843
  @keyframes wb-spin-animation {
838
844
  0% {
839
845
  transform: rotate(0deg);