wunderbaum 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/common.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * @VERSION, @DATE (https://github.com/mar10/wunderbaum)
5
5
  */
6
6
 
7
- import { MatcherCallback } from "./types";
7
+ import { MatcherCallback, SourceListType, SourceObjectType } from "./types";
8
8
  import * as util from "./util";
9
9
  import { WunderbaumNode } from "./wb_node";
10
10
 
@@ -101,9 +101,10 @@ export const INPUT_KEYS: { [key: string]: Array<string> } = {
101
101
  /** Dict keys that are evaluated by source loader (others are added to `tree.data` instead). */
102
102
  export const RESERVED_TREE_SOURCE_KEYS: Set<string> = new Set([
103
103
  "_format", // reserved for future use
104
- "_keyMap", // reserved for future use
105
- "_positional", // reserved for future use
106
- "_typeList", // reserved for future use
104
+ "_keyMap", // Used for compressed data format
105
+ "_positional", // Used for compressed data format
106
+ "_typeList", // Used for compressed data format @deprecated
107
+ "_valueMap", // Used for compressed data format
107
108
  "_version", // reserved for future use
108
109
  "children",
109
110
  "columns",
@@ -146,7 +147,7 @@ export const KEY_TO_ACTION_DICT: { [key: string]: string } = {
146
147
 
147
148
  /** Return a callback that returns true if the node title matches the string
148
149
  * or regular expression.
149
- * @see {@link WunderbaumNode.findAll}
150
+ * @see {@link WunderbaumNode.findAll()}
150
151
  */
151
152
  export function makeNodeTitleMatcher(match: string | RegExp): MatcherCallback {
152
153
  if (match instanceof RegExp) {
@@ -184,8 +185,20 @@ export function nodeTitleSorter(a: WunderbaumNode, b: WunderbaumNode): number {
184
185
  return x === y ? 0 : x > y ? 1 : -1;
185
186
  }
186
187
 
187
- function unflattenSource(source: any): void {
188
- const { _format, _keyMap, _positional, children } = source;
188
+ /**
189
+ * Convert 'flat' to 'nested' format.
190
+ *
191
+ * Flat node entry format:
192
+ * [PARENT_ID, [POSITIONAL_ARGS]]
193
+ * or
194
+ * [PARENT_ID, [POSITIONAL_ARGS], {KEY_VALUE_ARGS}]
195
+ *
196
+ * 1. Parent-referencing list is converted to a list of nested dicts with
197
+ * optional `children` properties.
198
+ * 2. `[POSITIONAL_ARGS]` are added as dict attributes.
199
+ */
200
+ function unflattenSource(source: SourceObjectType): void {
201
+ const { _format, _keyMap = {}, _positional = [], children } = source;
189
202
 
190
203
  if (_format !== "flat") {
191
204
  throw new Error(`Expected source._format: "flat", but got ${_format}`);
@@ -195,31 +208,35 @@ function unflattenSource(source: any): void {
195
208
  `source._positional must not include "children": ${_positional}`
196
209
  );
197
210
  }
198
- // Inverse keyMap:
199
- const longToShort: any = {};
200
- if (_keyMap) {
211
+ let longToShort = _keyMap;
212
+ if (_keyMap.t) {
213
+ // Inverse keyMap was used (pre 0.7.0)
214
+ // TODO: raise Error on final 1.x release
215
+ const msg = `source._keyMap maps from long to short since v0.7.0. Flip key/value!`;
216
+ console.warn(msg); // eslint-disable-line no-console
217
+ longToShort = {};
201
218
  for (const [key, value] of Object.entries(_keyMap)) {
202
- longToShort[<string>value] = key;
219
+ longToShort[value] = key;
203
220
  }
204
221
  }
205
222
  const positionalShort = _positional.map((e: string) => longToShort[e]);
206
- const newChildren: any[] = [];
223
+ const newChildren: SourceListType = [];
207
224
  const keyToNodeMap: { [key: string]: number } = {};
208
225
  const indexToNodeMap: { [key: number]: any } = {};
209
226
  const keyAttrName = longToShort["key"] ?? "key";
210
227
  const childrenAttrName = longToShort["children"] ?? "children";
211
228
 
212
- for (const [index, node] of children.entries()) {
229
+ for (const [index, nodeTuple] of children.entries()) {
213
230
  // Node entry format:
214
231
  // [PARENT_ID, [POSITIONAL_ARGS]]
215
232
  // or
216
233
  // [PARENT_ID, [POSITIONAL_ARGS], {KEY_VALUE_ARGS}]
217
- const [parentId, args, kwargs = {}] = node;
234
+ const [parentId, args, kwargs = {}] = <any>nodeTuple;
218
235
 
219
236
  // Free up some memory as we go
220
- node[1] = null;
221
- if (node[2] != null) {
222
- node[2] = null;
237
+ nodeTuple[1] = null;
238
+ if (nodeTuple[2] != null) {
239
+ nodeTuple[2] = null;
223
240
  }
224
241
  // console.log("flatten", parentId, args, kwargs)
225
242
 
@@ -262,13 +279,52 @@ function unflattenSource(source: any): void {
262
279
  newChildren.push(kwargs);
263
280
  }
264
281
  }
265
-
266
- delete source.children;
267
282
  source.children = newChildren;
268
283
  }
269
284
 
270
- export function inflateSourceData(source: any): void {
271
- const { _format, _keyMap, _typeList } = source;
285
+ /**
286
+ * Decompresses the source data by
287
+ * - converting from 'flat' to 'nested' format
288
+ * - expanding short alias names to long names (if defined in _keyMap)
289
+ * - resolving value indexes to value strings (if defined in _valueMap)
290
+ *
291
+ * @param source - The source object to be decompressed.
292
+ * @returns void
293
+ */
294
+ export function decompressSourceData(source: SourceObjectType): void {
295
+ let { _format, _version = 1, _keyMap, _valueMap } = source;
296
+
297
+ util.assert(_version === 1, `Expected file version 1 instead of ${_version}`);
298
+
299
+ let longToShort = _keyMap;
300
+ let shortToLong: { [key: string]: string } = {};
301
+
302
+ if (longToShort) {
303
+ for (const [key, value] of Object.entries(longToShort)) {
304
+ shortToLong[value] = key;
305
+ }
306
+ }
307
+
308
+ // Fallback for old format (pre 0.7.0, using _keyMap in reverse direction)
309
+ // TODO: raise Error on final 1.x release
310
+ if (longToShort && longToShort.t) {
311
+ const msg = `source._keyMap maps from long to short since v0.7.0. Flip key/value!`;
312
+ console.warn(msg); // eslint-disable-line no-console
313
+ [longToShort, shortToLong] = [shortToLong, longToShort];
314
+ }
315
+
316
+ // Fallback for old format (pre 0.7.0, using _typeList instead of _valueMap)
317
+ // TODO: raise Error on final 1.x release
318
+ if ((<any>source)._typeList != null) {
319
+ const msg = `source._typeList is deprecated since v0.7.0: use source._valueMap: {"type": [...]} instead.`;
320
+ if (_valueMap != null) {
321
+ throw new Error(msg);
322
+ } else {
323
+ console.warn(msg); // eslint-disable-line no-console
324
+ _valueMap = { type: (<any>source)._typeList };
325
+ delete (<any>source)._typeList;
326
+ }
327
+ }
272
328
 
273
329
  if (_format === "flat") {
274
330
  unflattenSource(source);
@@ -276,33 +332,40 @@ export function inflateSourceData(source: any): void {
276
332
  delete source._format;
277
333
  delete source._version;
278
334
  delete source._keyMap;
279
- delete source._typeList;
335
+ delete source._valueMap;
280
336
  delete source._positional;
281
337
 
282
- function _iter(childList: any[]) {
338
+ function _iter(childList: SourceListType) {
283
339
  for (const node of childList) {
284
- // Expand short alias names
285
- if (_keyMap) {
286
- // Iterate over a list of names, because we modify inside the loop:
287
- Object.getOwnPropertyNames(node).forEach((propName) => {
288
- const long = _keyMap[propName] ?? propName;
289
- if (long !== propName) {
290
- node[long] = node[propName];
340
+ // Iterate over a list of names, because we modify inside the loop
341
+ // (for ... of ... does not allow this)
342
+ Object.getOwnPropertyNames(node).forEach((propName) => {
343
+ const value: any = node[propName];
344
+
345
+ // Replace short names with long names if defined in _keyMap
346
+ let longName = propName;
347
+ if (_keyMap && shortToLong[propName] != null) {
348
+ longName = shortToLong[propName];
349
+ if (longName !== propName) {
350
+ node[longName] = value;
291
351
  delete node[propName];
292
352
  }
293
- });
294
- }
295
- // `node` now has long attribute names
296
-
297
- // Resolve node type indexes
298
- const type = node.type;
299
- if (_typeList && type != null && typeof type === "number") {
300
- const newType = _typeList[type];
301
- if (newType == null) {
302
- throw new Error(`Expected typeList[${type}] entry in [${_typeList}]`);
303
353
  }
304
- node.type = newType;
305
- }
354
+ // Replace type index with type name if defined in _valueMap
355
+ if (
356
+ _valueMap &&
357
+ typeof value === "number" &&
358
+ _valueMap[longName] != null
359
+ ) {
360
+ const newValue = _valueMap[longName][value];
361
+ if (newValue == null) {
362
+ throw new Error(
363
+ `Expected valueMap[${longName}][${value}] entry in [${_valueMap[longName]}]`
364
+ );
365
+ }
366
+ node[longName] = newValue;
367
+ }
368
+ });
306
369
 
307
370
  // Recursion
308
371
  if (node.children) {
@@ -310,5 +373,7 @@ export function inflateSourceData(source: any): void {
310
373
  }
311
374
  }
312
375
  }
313
- _iter(source.children);
376
+ if (_keyMap || _valueMap) {
377
+ _iter(source.children);
378
+ }
314
379
  }
package/src/deferred.ts CHANGED
@@ -22,38 +22,38 @@ type finallyCallbackType = () => void;
22
22
  * }
23
23
  * ```
24
24
  */
25
- export class Deferred {
26
- private _promise: Promise<any>;
25
+ export class Deferred<T> {
26
+ private _promise: Promise<T>;
27
27
  protected _resolve: any;
28
28
  protected _reject: any;
29
29
 
30
30
  constructor() {
31
- this._promise = new Promise((resolve, reject) => {
31
+ this._promise = new Promise<T>((resolve, reject) => {
32
32
  this._resolve = resolve;
33
33
  this._reject = reject;
34
34
  });
35
35
  }
36
- /** Resolve the [[Promise]]. */
36
+ /** Resolve the Promise. */
37
37
  resolve(value?: any) {
38
38
  this._resolve(value);
39
39
  }
40
- /** Reject the [[Promise]]. */
40
+ /** Reject the Promise. */
41
41
  reject(reason?: any) {
42
42
  this._reject(reason);
43
43
  }
44
- /** Return the native [[Promise]] instance.*/
44
+ /** Return the native Promise instance.*/
45
45
  promise() {
46
46
  return this._promise;
47
47
  }
48
- /** Call [[Promise.then]] on the embedded promise instance.*/
48
+ /** Call Promise.then on the embedded promise instance.*/
49
49
  then(cb: PromiseCallbackType) {
50
50
  return this._promise.then(cb);
51
51
  }
52
- /** Call [[Promise.catch]] on the embedded promise instance.*/
52
+ /** Call Promise.catch on the embedded promise instance.*/
53
53
  catch(cb: PromiseCallbackType) {
54
54
  return this._promise.catch(cb);
55
55
  }
56
- /** Call [[Promise.finally]] on the embedded promise instance.*/
56
+ /** Call Promise.finally on the embedded promise instance.*/
57
57
  finally(cb: finallyCallbackType) {
58
58
  return this._promise.finally(cb);
59
59
  }
package/src/types.ts CHANGED
@@ -33,12 +33,14 @@ export interface SourceAjaxType {
33
33
  export type SourceListType = Array<WbNodeData>;
34
34
  export interface SourceObjectType {
35
35
  _format?: "nested" | "flat";
36
+ _version?: number;
36
37
  types?: NodeTypeDefinitionMap;
37
38
  columns?: ColumnDefinitionList;
38
39
  children: SourceListType;
39
40
  _keyMap?: { [key: string]: string };
40
- _typeList?: Array<string>;
41
41
  _positional?: Array<string>;
42
+ // _typeList?: Array<string>;
43
+ _valueMap?: { [key: string]: Array<string> };
42
44
  }
43
45
 
44
46
  /** Possible initilization for tree nodes. */
@@ -153,32 +155,67 @@ export interface WbActivateEventType extends WbNodeEventType {
153
155
  }
154
156
 
155
157
  export interface WbChangeEventType extends WbNodeEventType {
158
+ /** Additional information derived from the original change event. */
156
159
  info: WbEventInfo;
157
- inputElem: HTMLInputElement;
160
+ /** The embedded element that fired the change event. */
161
+ inputElem: HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
162
+ /** The new value of the embedded element, depending on the input element type. */
158
163
  inputValue: any;
164
+ /** Result of `inputElem.checkValidity()`. */
165
+ inputValid: boolean;
159
166
  }
160
167
 
161
168
  export interface WbClickEventType extends WbTreeEventType {
162
169
  /** The original event. */
163
170
  event: MouseEvent;
171
+ /** The clicked node if any. */
164
172
  node: WunderbaumNode;
173
+ /** Additional information derived from the original mouse event. */
165
174
  info: WbEventInfo;
166
175
  }
167
176
 
168
- export interface WbErrorEventType extends WbNodeEventType {
169
- error: any;
170
- }
171
-
172
177
  export interface WbDeactivateEventType extends WbNodeEventType {
173
178
  nextNode: WunderbaumNode;
174
179
  /** The original event. */
175
180
  event: Event;
176
181
  }
177
182
 
183
+ export interface WbEditApplyEventType extends WbNodeEventType {
184
+ /** Additional information derived from the original change event. */
185
+ info: WbEventInfo;
186
+ /** The input element of the node title that fired the change event. */
187
+ inputElem: HTMLInputElement;
188
+ /** The previous node title. */
189
+ oldValue: string;
190
+ /** The new node title. */
191
+ newValue: string;
192
+ /** Result of `inputElem.checkValidity()`. */
193
+ inputValid: boolean;
194
+ }
195
+
196
+ export interface WbEditEditEventType extends WbNodeEventType {
197
+ /** The input element of the node title that was just created. */
198
+ inputElem: HTMLInputElement;
199
+ }
200
+
178
201
  // export interface WbEnhanceTitleEventType extends WbNodeEventType {
179
202
  // titleSpan: HTMLSpanElement;
180
203
  // }
181
204
 
205
+ export interface WbErrorEventType extends WbNodeEventType {
206
+ error: any;
207
+ }
208
+ export interface WbExpandEventType extends WbNodeEventType {
209
+ flag: boolean;
210
+ }
211
+
212
+ export interface WbFocusEventType extends WbTreeEventType {
213
+ /** The original event. */
214
+ event: FocusEvent;
215
+ /** True if `focusin`, false if `focusout`. */
216
+ flag: boolean;
217
+ }
218
+
182
219
  export interface WbIconBadgeEventType extends WbNodeEventType {
183
220
  iconSpan: HTMLElement;
184
221
  }
@@ -191,33 +228,32 @@ export interface WbIconBadgeEventResultType {
191
228
  badgeTooltip?: string;
192
229
  }
193
230
 
194
- export interface WbFocusEventType extends WbTreeEventType {
195
- /** The original event. */
196
- event: FocusEvent;
197
- /** True if `focusin`, false if `focusout`. */
198
- flag: boolean;
231
+ export interface WbInitEventType extends WbTreeEventType {
232
+ error?: any;
199
233
  }
200
234
 
201
235
  export interface WbKeydownEventType extends WbTreeEventType {
202
236
  /** The original event. */
203
237
  event: KeyboardEvent;
204
238
  node: WunderbaumNode;
239
+ /** Additional information derived from the original keyboard event. */
205
240
  info: WbEventInfo;
206
241
  }
207
242
 
208
- export interface WbInitEventType extends WbTreeEventType {
209
- error?: any;
210
- }
211
-
212
243
  export interface WbReceiveEventType extends WbNodeEventType {
213
244
  response: any;
214
245
  }
246
+ export interface WbSelectEventType extends WbNodeEventType {
247
+ flag: boolean;
248
+ }
215
249
 
216
250
  export interface WbRenderEventType extends WbNodeEventType {
217
251
  /**
218
252
  * True if the node's markup was not yet created. In this case the render
219
253
  * event should create embedded input controls (in addition to update the
220
- * values according to to current node data).
254
+ * values according to to current node data). <br>
255
+ * False if the node's markup was already created. In this case the render
256
+ * event should only update the values according to to current node data.
221
257
  */
222
258
  isNew: boolean;
223
259
  /** The node's `<span class='wb-node'>` element. */
@@ -231,9 +267,20 @@ export interface WbRenderEventType extends WbNodeEventType {
231
267
  */
232
268
  allColInfosById: ColumnEventInfoMap;
233
269
  /**
234
- * Array of node's `<span class='wb-node'>` elements, *that should be rendered*.
270
+ * Array of node's `<span class='wb-node'>` elements,
271
+ * *that should be rendered by the event handler*.
235
272
  * In contrast to `allColInfosById`, the node title is not part of this array.
236
273
  * If node.isColspan() is true, this array is empty (`[]`).
274
+ * This allows to iterate over all relevant in a simple loop:
275
+ * ```
276
+ * for (const col of Object.values(e.renderColInfosById)) {
277
+ * switch (col.id) {
278
+ * default:
279
+ * // Assumption: we named column.id === node.data.NAME
280
+ * col.elem.textContent = node.data[col.id];
281
+ * break;
282
+ * }
283
+ * }
237
284
  */
238
285
  renderColInfosById: ColumnEventInfoMap;
239
286
  }
@@ -290,15 +337,17 @@ export interface ColumnDefinition {
290
337
  * elements of that column.
291
338
  */
292
339
  classes?: string;
293
- /** If `headerClasses` is a string, it will be used for the header element,
294
- * while `classes` is used for data elements.
340
+ /** If `headerClasses` is a set, it will be used for the header element only
341
+ * (unlike `classes`, which is used for body and header cells).
295
342
  */
296
343
  headerClasses?: string;
297
344
  /** Optional HTML content that is rendered into all `span.wb-col` elements of that column.*/
298
345
  html?: string;
299
- // Internal use:
346
+ /** @internal */
300
347
  _weight?: number;
348
+ /** @internal */
301
349
  _widthPx?: number;
350
+ /** @internal */
302
351
  _ofsPx?: number;
303
352
  // ... and more
304
353
  [key: string]: unknown;
@@ -323,7 +372,7 @@ export interface ColumnEventInfo {
323
372
  export type ColumnEventInfoMap = { [colId: string]: ColumnEventInfo };
324
373
 
325
374
  /**
326
- * Additional inforation derived from mouse or keyboard events.
375
+ * Additional information derived from mouse or keyboard events.
327
376
  * @see {@link Wunderbaum.getEventInfo}
328
377
  */
329
378
  export interface WbEventInfo {
@@ -540,23 +589,43 @@ export interface ScrollToOptions extends ScrollIntoViewOptions {
540
589
  node: WunderbaumNode;
541
590
  }
542
591
 
543
- /** Possible values for {@link WunderbaumNode.setActive()} `options` argument. */
592
+ /** Possible values for {@link WunderbaumNode.setActive} `options` argument. */
544
593
  export interface SetActiveOptions {
545
- /** Generate (de)activate event, even if node already has this status (default: false). */
594
+ /** Generate (de)activate event, even if node already has this status (@default: false). */
546
595
  retrigger?: boolean;
547
- /** Do not generate (de)activate event (default: false). */
596
+ /** Do not generate (de)activate event (@default: false). */
548
597
  noEvents?: boolean;
549
- /** Set node as focused node (default: true). */
550
- focusNode?: boolean;
551
- /** Set node as focused node (default: false). */
598
+ // /** Mark node as focused node (default: true).
599
+ // * Combine with `focusTree: true` to set keyboard focus to tree container.
600
+ // */
601
+ // focusNode?: boolean;
602
+ /** Call `tree.setFocus()` to acquire keyboard focus (@default: false). */
552
603
  focusTree?: boolean;
553
604
  /** Optional original event that will be passed to the (de)activate handler. */
554
605
  event?: Event;
555
- /** Call {@link Wunderbaum.setColumn}. */
556
- colIdx?: number;
606
+ /** Also call {@link Wunderbaum.setColumn()}. */
607
+ colIdx?: number | string;
608
+ /**
609
+ * Focus embedded input control of the grid cell if any (requires colIdx >= 0).
610
+ * If colIdx is 0 or '*', the node title is put into edit mode.
611
+ * Implies `focusTree: true`, requires `colIdx`.
612
+ */
613
+ edit?: boolean;
557
614
  }
558
615
 
559
- /** Possible values for {@link WunderbaumNode.setExpanded()} `options` argument. */
616
+ /** Possible values for {@link WunderbaumNode.setColumn()} `options` argument. */
617
+ export interface SetColumnOptions {
618
+ /**
619
+ * Focus embedded input control of the grid cell if any .
620
+ * If colIdx is 0 or '*', the node title is put into edit mode.
621
+ * @default false
622
+ */
623
+ edit?: boolean;
624
+ /** Horizontically scroll into view. @default: true */
625
+ scrollIntoView?: boolean;
626
+ }
627
+
628
+ /** Possible values for {@link WunderbaumNode.setExpanded} `options` argument. */
560
629
  export interface SetExpandedOptions {
561
630
  /** Ignore {@link WunderbaumOptions.minExpandLevel}. @default false */
562
631
  force?: boolean;
@@ -665,53 +734,70 @@ export type FilterOptionsType = {
665
734
  * Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
666
735
  * @default 'dim'
667
736
  */
668
- mode?: "dim" | "hide";
737
+ mode?: FilterModeType;
669
738
  /**
670
- * Display a 'no data' status node if result is empty
739
+ * Display a 'no data' status node if result is empty (hide-mode only).
740
+ * Pass false to simply show an empy pane, or pass a string to customize the message.
671
741
  * @default true
672
742
  */
673
- noData?: boolean;
743
+ noData?: boolean | string;
674
744
  };
675
745
 
746
+ /**
747
+ * Note: <br>
748
+ * This options are used for renaming node titles. <br>
749
+ * There is also the `tree.change` event to handle modifying node data from
750
+ * input controls that are embedded in grid cells.
751
+ */
676
752
  export type EditOptionsType = {
677
753
  /**
754
+ * Used to debounce the `change` event handler for grid cells [ms].
678
755
  * @default 100
679
756
  */
680
757
  debounce?: number;
681
758
  /**
759
+ * Minimum number of characters required for node title input field.
682
760
  * @default 1
683
761
  */
684
762
  minlength?: number;
685
763
  /**
764
+ * Maximum number of characters allowed for node title input field.
686
765
  * @default null;
687
766
  */
688
767
  maxlength?: null | number;
689
768
  /**
690
- * ["clickActive", "F2", "macEnter"],
769
+ * Array of strings to determine which user input should trigger edit mode.
770
+ * E.g. `["clickActive", "F2", "macEnter"]`: <br>
771
+ * 'clickActive': single click on active node title <br>
772
+ * 'F2': press F2 key <br>
773
+ * 'macEnter': press Enter (on macOS only) <br>
774
+ * Pass an empty array to disable edit mode.
691
775
  * @default []
692
776
  */
693
777
  trigger?: string[];
694
778
  /**
779
+ * Trim whitespace before saving a node title.
695
780
  * @default true
696
781
  */
697
782
  trim?: boolean;
698
783
  /**
784
+ * Select all text of a node title, so it can be overwritten by typing.
699
785
  * @default true
700
786
  */
701
787
  select?: boolean;
702
788
  /**
703
- * Handle 'clickActive' only if last click is less than this old (0: always)
789
+ * Handle 'clickActive' only if last click is less than this ms old (0: always)
704
790
  * @default 1000
705
791
  */
706
792
  slowClickDelay?: number;
707
793
  /**
708
- * Please enter a title",
794
+ * Permanently apply node title input validations (CSS and tooltip) on keydown.
709
795
  * @default true
710
796
  */
711
797
  validity?: boolean;
712
798
 
713
799
  // --- Events ---
714
- // (note: there is also the `tree.change` event.)
800
+
715
801
  /**
716
802
  * `beforeEdit(e)` may return an input HTML string. Otherwise use a default.
717
803
  */