wunderbaum 0.12.1 → 0.14.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/wb_ext_dnd.ts CHANGED
@@ -78,7 +78,7 @@ export class DndExtension extends WunderbaumExtension<DndOptionsType> {
78
78
  // this.$scrollParent = $temp.scrollParent();
79
79
  // $temp.remove();
80
80
  const tree = this.tree;
81
- const dndOpts = tree.options.dnd!;
81
+ const dndOpts = tree.options.dnd;
82
82
 
83
83
  // Enable drag support if dragStart() is specified:
84
84
  if (dndOpts.dragStart) {
@@ -134,7 +134,7 @@ export class DndExtension extends WunderbaumExtension<DndOptionsType> {
134
134
  e: DragEvent,
135
135
  allowed: DropRegionTypeSet | null
136
136
  ): DropRegionType | false {
137
- const rowHeight = this.tree.options.rowHeightPx!;
137
+ const rowHeight = this.tree.options.rowHeightPx;
138
138
  const dy = e.offsetY;
139
139
 
140
140
  if (!allowed) {
@@ -208,7 +208,7 @@ export class DndExtension extends WunderbaumExtension<DndOptionsType> {
208
208
  // `_isVoidDrop: ${srcNode} -> ${dropRegion} ${targetNode}`
209
209
  // );
210
210
  // TODO: should be checked on move only
211
- if (!this.treeOpts.dnd.preventVoidMoves || !srcNode) {
211
+ if (!this.treeOpts.dnd!.preventVoidMoves || !srcNode) {
212
212
  return false;
213
213
  }
214
214
  if (
@@ -225,7 +225,7 @@ export class DndExtension extends WunderbaumExtension<DndOptionsType> {
225
225
  /* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */
226
226
  protected _applyScrollDir(): void {
227
227
  if (this.isDragging() && this.currentScrollDir) {
228
- const dndOpts = this.tree.options.dnd!;
228
+ const dndOpts = this.tree.options.dnd;
229
229
  const sp = this.tree.element; // scroll parent
230
230
  const scrollTop = sp.scrollTop;
231
231
  if (this.currentScrollDir < 0) {
@@ -238,7 +238,7 @@ export class DndExtension extends WunderbaumExtension<DndOptionsType> {
238
238
  /* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */
239
239
  protected _autoScroll(viewportY: number): number {
240
240
  const tree = this.tree;
241
- const dndOpts = tree.options.dnd!;
241
+ const dndOpts = tree.options.dnd;
242
242
  const sensitivity = dndOpts.scrollSensitivity;
243
243
  const sp = tree.element; // scroll parent
244
244
  const headerHeight = tree.headerElement.clientHeight; // May be 0
@@ -284,7 +284,6 @@ export class DndExtension extends WunderbaumExtension<DndOptionsType> {
284
284
  * Handle dragstart, drag and dragend events for the source node.
285
285
  */
286
286
  protected onDragEvent(e: DragEvent) {
287
- // const tree = this.tree;
288
287
  const dndOpts: DndOptionsType = this.treeOpts.dnd;
289
288
  const srcNode = Wunderbaum.getNode(e);
290
289
 
@@ -312,7 +311,7 @@ export class DndExtension extends WunderbaumExtension<DndOptionsType> {
312
311
  return false;
313
312
  }
314
313
  const nodeData = srcNode.toDict(true, (n: any) => {
315
- // We don't want to re-use the key on drop:
314
+ // We don't want to reuse the key on drop:
316
315
  n._orgKey = n.key;
317
316
  delete n.key;
318
317
  });
@@ -377,6 +376,7 @@ export class DndExtension extends WunderbaumExtension<DndOptionsType> {
377
376
  };
378
377
  if (!targetNode) {
379
378
  this._leaveNode();
379
+ e.preventDefault(); // Don't open file in browser when dropped in empty area
380
380
  return;
381
381
  }
382
382
  if (["drop"].includes(e.type)) {
@@ -518,19 +518,20 @@ export class DndExtension extends WunderbaumExtension<DndOptionsType> {
518
518
  const srcNode = this.srcNode;
519
519
  const lastDropEffect = this.lastDropEffect;
520
520
 
521
- setTimeout(() => {
522
- // Decouple this call, because drop actions may prevent the dragend event
523
- // from being fired on some browsers
524
- targetNode._callEvent("dnd.drop", {
525
- event: e,
526
- region: region,
527
- suggestedDropMode: region === "over" ? "appendChild" : region,
528
- suggestedDropEffect: lastDropEffect,
529
- // suggestedDropEffect: e.dataTransfer?.dropEffect,
530
- sourceNode: srcNode,
531
- sourceNodeData: nodeData,
532
- });
533
- }, 10);
521
+ /* Before v0.14.0, we decoupled `_callEvent` like so:
522
+ Decouple this call, because drop actions may prevent the dragend
523
+ event from being fired on some browsers.
524
+ setTimeout(() => {...}, 10);
525
+ however this made e.dataTransfer.items inaccessible */
526
+ targetNode._callEvent("dnd.drop", {
527
+ event: e,
528
+ region: region,
529
+ suggestedDropMode: region === "over" ? "appendChild" : region,
530
+ suggestedDropEffect: lastDropEffect,
531
+ sourceNode: srcNode,
532
+ sourceNodeData: nodeData,
533
+ dataTransfer: e.dataTransfer,
534
+ });
534
535
  }
535
536
  return false;
536
537
  }
@@ -286,7 +286,7 @@ export class EditExtension extends WunderbaumExtension<EditOptionsType> {
286
286
  newValue = newValue.trim();
287
287
  }
288
288
  if (!node) {
289
- this.tree.logDebug("stopEditTitle: not in edit mode.");
289
+ // this.tree.logDebug("stopEditTitle: not in edit mode.");
290
290
  return;
291
291
  }
292
292
  node.logDebug(`stopEditTitle(${apply})`, options, focusElem, newValue);
@@ -381,7 +381,7 @@ export class EditExtension extends WunderbaumExtension<EditOptionsType> {
381
381
  this.relatedNode = node;
382
382
 
383
383
  // Don't filter new nodes:
384
- newNode.match = true;
384
+ newNode.match = -1;
385
385
 
386
386
  newNode.makeVisible({ noAnimation: true }).then(() => {
387
387
  this.startEditTitle(newNode);
@@ -13,6 +13,7 @@ import {
13
13
  onEvent,
14
14
  } from "./util";
15
15
  import {
16
+ FilterConnectType,
16
17
  FilterNodesOptions,
17
18
  FilterOptionsType,
18
19
  NodeFilterCallback,
@@ -29,7 +30,11 @@ const RE_START_MARKER = new RegExp(escapeRegex(START_MARKER), "g");
29
30
  const RE_END_MARTKER = new RegExp(escapeRegex(END_MARKER), "g");
30
31
 
31
32
  export class FilterExtension extends WunderbaumExtension<FilterOptionsType> {
32
- public queryInput?: HTMLInputElement;
33
+ public queryInput: HTMLInputElement | null = null;
34
+ public prevButton: HTMLElement | HTMLAnchorElement | null = null;
35
+ public nextButton: HTMLElement | HTMLAnchorElement | null = null;
36
+ public modeButton: HTMLButtonElement | null = null;
37
+ public matchInfoElem: HTMLElement | null = null;
33
38
  public lastFilterArgs: IArguments | null = null;
34
39
 
35
40
  constructor(tree: Wunderbaum) {
@@ -37,7 +42,7 @@ export class FilterExtension extends WunderbaumExtension<FilterOptionsType> {
37
42
  autoApply: true, // Re-apply last filter if lazy data is loaded
38
43
  autoExpand: false, // Expand all branches that contain matches while filtered
39
44
  matchBranch: false, // Whether to implicitly match all children of matched nodes
40
- connectInput: null, // Element or selector of an input control for filter query strings
45
+ connect: null, // Element or selector of an input control for filter query strings
41
46
  fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'
42
47
  hideExpanders: false, // Hide expanders if all child nodes are hidden by filter
43
48
  highlight: true, // Highlight matches by wrapping inside <mark> tags
@@ -49,35 +54,118 @@ export class FilterExtension extends WunderbaumExtension<FilterOptionsType> {
49
54
 
50
55
  init() {
51
56
  super.init();
52
- const connectInput = this.getPluginOption("connectInput");
53
- if (connectInput) {
54
- this.queryInput = elemFromSelector(connectInput) as HTMLInputElement;
55
- assert(
56
- this.queryInput,
57
- `Invalid 'filter.connectInput' option: ${connectInput}.`
58
- );
59
- onEvent(
60
- this.queryInput,
61
- "input",
62
- debounce((e) => {
63
- // this.tree.log("query", e);
64
- this.filterNodes(this.queryInput!.value.trim(), {});
65
- }, 700)
66
- );
57
+ const connect: FilterConnectType = this.getPluginOption("connect");
58
+ if (connect) {
59
+ this._connectControls();
67
60
  }
68
61
  }
69
62
 
70
63
  setPluginOption(name: string, value: any): void {
71
- // alert("filter opt=" + name + ", " + value)
72
64
  super.setPluginOption(name, value);
73
65
  switch (name) {
74
66
  case "mode":
75
- this.tree.filterMode = value === "hide" ? "hide" : "dim";
67
+ this.tree.filterMode =
68
+ value === "hide" ? "hide" : value === "mark" ? "mark" : "dim";
76
69
  this.tree.updateFilter();
77
70
  break;
78
71
  }
79
72
  }
80
73
 
74
+ _updatedConnectedControls() {
75
+ const filterActive = this.tree.filterMode !== null;
76
+ const activeNode = this.tree.getActiveNode();
77
+ const matchCount = filterActive ? this.countMatches() : 0;
78
+ const strings = this.treeOpts.strings;
79
+ let matchIdx: string | number = "?";
80
+
81
+ if (this.matchInfoElem) {
82
+ if (filterActive) {
83
+ let info;
84
+ if (matchCount === 0) {
85
+ info = strings.noMatch;
86
+ } else if (activeNode && activeNode.match! >= 1) {
87
+ matchIdx = activeNode.match ?? "?";
88
+ info = strings.matchIndex;
89
+ } else {
90
+ info = strings.queryResult;
91
+ }
92
+ info = info
93
+ .replace("${count}", this.tree.count().toLocaleString())
94
+ .replace("${match}", "" + matchIdx)
95
+ .replace("${matches}", matchCount.toLocaleString());
96
+ this.matchInfoElem.textContent = info;
97
+ } else {
98
+ this.matchInfoElem.textContent = "";
99
+ }
100
+ }
101
+
102
+ if (this.nextButton instanceof HTMLButtonElement) {
103
+ this.nextButton.disabled = !matchCount;
104
+ }
105
+ if (this.prevButton instanceof HTMLButtonElement) {
106
+ this.prevButton.disabled = !matchCount;
107
+ }
108
+ if (this.modeButton) {
109
+ this.modeButton.disabled = !filterActive;
110
+ this.modeButton.classList.toggle(
111
+ "wb-filter-hide",
112
+ this.tree.filterMode === "hide"
113
+ );
114
+ }
115
+ }
116
+ _connectControls() {
117
+ const tree = this.tree;
118
+ const connect: FilterConnectType = this.getPluginOption("connect");
119
+ if (!connect) {
120
+ return;
121
+ }
122
+ this.queryInput = elemFromSelector(connect.inputElem);
123
+ if (!this.queryInput) {
124
+ throw new Error(`Invalid 'filter.connect' option: ${connect.inputElem}.`);
125
+ }
126
+ this.prevButton = elemFromSelector(connect.prevButton!);
127
+ this.nextButton = elemFromSelector(connect.nextButton!);
128
+ this.modeButton = elemFromSelector(connect.modeButton!);
129
+ this.matchInfoElem = elemFromSelector(connect.matchInfoElem!);
130
+ if (this.prevButton) {
131
+ onEvent(this.prevButton, "click", () => {
132
+ tree.findRelatedNode(
133
+ tree.getActiveNode() || tree.getFirstChild()!,
134
+ "prevMatch"
135
+ );
136
+ this._updatedConnectedControls();
137
+ });
138
+ }
139
+ if (this.nextButton) {
140
+ onEvent(this.nextButton, "click", () => {
141
+ tree.findRelatedNode(
142
+ tree.getActiveNode() || tree.getFirstChild()!,
143
+ "nextMatch"
144
+ );
145
+ this._updatedConnectedControls();
146
+ });
147
+ }
148
+ if (this.modeButton) {
149
+ onEvent(this.modeButton, "click", (e) => {
150
+ if (!this.tree.filterMode) {
151
+ return;
152
+ }
153
+ this.setPluginOption(
154
+ "mode",
155
+ tree.filterMode === "dim" ? "hide" : "dim"
156
+ );
157
+ });
158
+ }
159
+ onEvent(
160
+ this.queryInput,
161
+ "input",
162
+ debounce((e) => {
163
+ this.filterNodes(this.queryInput!.value.trim(), {});
164
+ }, 700)
165
+ );
166
+ this._updatedConnectedControls();
167
+ }
168
+
81
169
  _applyFilterNoUpdate(
82
170
  filter: string | RegExp | NodeFilterCallback,
83
171
  _opts: FilterNodesOptions
@@ -98,7 +186,7 @@ export class FilterExtension extends WunderbaumExtension<FilterOptionsType> {
98
186
  const treeOpts = tree.options;
99
187
  const prevAutoCollapse = treeOpts.autoCollapse;
100
188
  // Use default options from `tree.options.filter`, but allow to override them
101
- const opts = extend({}, treeOpts.filter, _opts);
189
+ const opts: FilterOptionsType = extend({}, treeOpts.filter, _opts);
102
190
  const hideMode = opts.mode === "hide";
103
191
  const matchBranch = !!opts.matchBranch;
104
192
  const leavesOnly = !!opts.leavesOnly && !matchBranch;
@@ -176,12 +264,12 @@ export class FilterExtension extends WunderbaumExtension<FilterOptionsType> {
176
264
  };
177
265
  }
178
266
 
179
- tree.filterMode = opts.mode;
267
+ tree.filterMode = opts.mode ?? "dim";
180
268
  // eslint-disable-next-line prefer-rest-params
181
269
  this.lastFilterArgs = arguments;
182
270
 
183
271
  tree.element.classList.toggle("wb-ext-filter-hide", !!hideMode);
184
- tree.element.classList.toggle("wb-ext-filter-dim", !hideMode);
272
+ tree.element.classList.toggle("wb-ext-filter-dim", opts.mode === "dim");
185
273
  tree.element.classList.toggle(
186
274
  "wb-ext-filter-hide-expanders",
187
275
  !!opts.hideExpanders
@@ -193,10 +281,6 @@ export class FilterExtension extends WunderbaumExtension<FilterOptionsType> {
193
281
  delete node.titleWithHighlight;
194
282
  node.subMatchCount = 0;
195
283
  });
196
- // statusNode = tree.root.findDirectChild(KEY_NODATA);
197
- // if (statusNode) {
198
- // statusNode.remove();
199
- // }
200
284
  tree.setStatus(NodeStatusType.ok);
201
285
 
202
286
  // Adjust node.hide, .match, and .subMatchCount properties
@@ -210,7 +294,7 @@ export class FilterExtension extends WunderbaumExtension<FilterOptionsType> {
210
294
 
211
295
  if (res === "skip") {
212
296
  node.visit(function (c) {
213
- c.match = false;
297
+ c.match = undefined;
214
298
  }, true);
215
299
  return "skip";
216
300
  }
@@ -223,7 +307,7 @@ export class FilterExtension extends WunderbaumExtension<FilterOptionsType> {
223
307
 
224
308
  if (res) {
225
309
  count++;
226
- node.match = true;
310
+ node.match = count;
227
311
  node.visitParents((p) => {
228
312
  if (p !== node) {
229
313
  p.subMatchCount! += 1;
@@ -252,6 +336,8 @@ export class FilterExtension extends WunderbaumExtension<FilterOptionsType> {
252
336
  tree.logDebug(
253
337
  `Filter '${filter}' found ${count} nodes in ${Date.now() - start} ms.`
254
338
  );
339
+ this._updatedConnectedControls();
340
+
255
341
  return count;
256
342
  }
257
343
 
@@ -277,6 +363,10 @@ export class FilterExtension extends WunderbaumExtension<FilterOptionsType> {
277
363
  options.matchBranch === undefined,
278
364
  "filterBranches() is deprecated."
279
365
  );
366
+ this.tree.logDeprecate("filterBranches()", {
367
+ since: "0.9.0",
368
+ hint: "Use `filterNodes` instead and set `options.matchBranch: true`",
369
+ });
280
370
  options.matchBranch = true;
281
371
  return this._applyFilterNoUpdate(filter, options);
282
372
  }
@@ -309,6 +399,7 @@ export class FilterExtension extends WunderbaumExtension<FilterOptionsType> {
309
399
  } else {
310
400
  tree.logWarn("updateFilter(): no filter active.");
311
401
  }
402
+ this._updatedConnectedControls();
312
403
  }
313
404
 
314
405
  /**
@@ -316,30 +407,17 @@ export class FilterExtension extends WunderbaumExtension<FilterOptionsType> {
316
407
  */
317
408
  clearFilter() {
318
409
  const tree = this.tree;
319
- // statusNode = tree.root.findDirectChild(KEY_NODATA),
320
- // escapeTitles = tree.options.escapeTitles;
321
410
  tree.enableUpdate(false);
322
411
 
323
- // if (statusNode) {
324
- // statusNode.remove();
325
- // }
326
412
  tree.setStatus(NodeStatusType.ok);
327
413
  // we also counted root node's subMatchCount
328
414
  delete tree.root.match;
329
415
  delete tree.root.subMatchCount;
330
416
 
331
417
  tree.visit((node) => {
332
- // if (node.match && node._rowElem) {
333
- // let titleElem = node._rowElem.querySelector("span.wb-title")!;
334
- // node._callEvent("enhanceTitle", { titleElem: titleElem });
335
- // }
336
418
  delete node.match;
337
419
  delete node.subMatchCount;
338
420
  delete node.titleWithHighlight;
339
- // if (node.subMatchBadge) {
340
- // node.subMatchBadge.remove();
341
- // delete node.subMatchBadge;
342
- // }
343
421
  if (node._filterAutoExpanded && node.expanded) {
344
422
  node.setExpanded(false, {
345
423
  noAnimation: true,
@@ -355,13 +433,14 @@ export class FilterExtension extends WunderbaumExtension<FilterOptionsType> {
355
433
  "wb-ext-filter-dim",
356
434
  "wb-ext-filter-hide"
357
435
  );
358
- // tree._callHook("treeStructureChanged", this, "clearFilter");
436
+ this._updatedConnectedControls();
437
+
359
438
  tree.enableUpdate(true);
360
439
  }
361
440
  }
362
441
 
363
442
  /**
364
- * @description Marks the matching charecters of `text` either by `mark` or
443
+ * @description Marks the matching characters of `text` either by `mark` or
365
444
  * by exotic*Chars (if `escapeTitles` is `true`) based on `matches`
366
445
  * which is an array of matching groups.
367
446
  * @param {string} text
@@ -77,7 +77,7 @@ export class GridExtension extends WunderbaumExtension<GridOptionsType> {
77
77
  }
78
78
 
79
79
  /**
80
- * Hanldes drag and sragstop events for column resizing.
80
+ * Handles drag and sragstop events for column resizing.
81
81
  */
82
82
  protected handleDrag(e: DragCallbackArgType): void {
83
83
  const custom = e.customData;
@@ -5,15 +5,31 @@
5
5
  */
6
6
 
7
7
  import * as util from "./util";
8
+ import { DndExtension } from "./wb_ext_dnd";
9
+ import { EditExtension } from "./wb_ext_edit";
10
+ import { FilterExtension } from "./wb_ext_filter";
11
+ import { GridExtension } from "./wb_ext_grid";
12
+ import { KeynavExtension } from "./wb_ext_keynav";
13
+ import { LoggerExtension } from "./wb_ext_logger";
14
+ import { WunderbaumOptions } from "./wb_options";
8
15
  import { Wunderbaum } from "./wunderbaum";
9
16
 
10
- export type ExtensionsDict = { [key: string]: WunderbaumExtension<any> };
17
+ export type ExtensionsDict = {
18
+ dnd: DndExtension;
19
+ edit: EditExtension;
20
+ filter: FilterExtension;
21
+ grid: GridExtension;
22
+ keynav: KeynavExtension;
23
+ logger: LoggerExtension;
24
+
25
+ [key: string]: WunderbaumExtension<any>;
26
+ };
11
27
 
12
28
  export abstract class WunderbaumExtension<TOptions> {
13
29
  public enabled = true;
14
30
  readonly id: string;
15
31
  readonly tree: Wunderbaum;
16
- readonly treeOpts: any;
32
+ readonly treeOpts: WunderbaumOptions;
17
33
  readonly extensionOpts: any;
18
34
 
19
35
  constructor(tree: Wunderbaum, id: string, defaults: TOptions) {
@@ -23,7 +39,7 @@ export abstract class WunderbaumExtension<TOptions> {
23
39
 
24
40
  const opts = tree.options as any;
25
41
 
26
- if (this.treeOpts[id] === undefined) {
42
+ if ((<any>this.treeOpts)[id] === undefined) {
27
43
  opts[id] = this.extensionOpts = util.extend({}, defaults);
28
44
  } else {
29
45
  // TODO: do we break existing object instance references here?