wunderbaum 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/common.ts CHANGED
@@ -26,6 +26,8 @@ export const TITLE_SPAN_PAD_Y = 7;
26
26
  export const RENDER_MAX_PREFETCH = 5;
27
27
  /** Skip rendering new rows when we have at least N nodes rendeed above and below the viewport. */
28
28
  export const RENDER_MIN_PREFETCH = 5;
29
+ /** Minimum column width if not set otherwise. */
30
+ export const DEFAULT_MIN_COL_WIDTH = 4;
29
31
  /** Regular expression to detect if a string describes an image URL (in contrast
30
32
  * to a class name). Strings are considered image urls if they contain '.' or '/'.
31
33
  */
@@ -7,8 +7,15 @@
7
7
  export type DragCallbackArgType = {
8
8
  /** "dragstart", "drag", or "dragstop". */
9
9
  type: string;
10
- /** Original mouse or touch event that triggered the drag event. */
10
+ /** Original mousedown or touch event that triggered the dragstart event. */
11
+ startEvent: MouseEvent | TouchEvent;
12
+ /** Original mouse or touch event that triggered the current drag event.
13
+ * Note that this is not the same as `startEvent`, but a mousemove in case of
14
+ * a dragstart threshold.
15
+ */
11
16
  event: MouseEvent | TouchEvent;
17
+ /** Custom data that was passed to the DragObserver, typically on dragstart. */
18
+ customData: any;
12
19
  /** Element which is currently dragged. */
13
20
  dragElem: HTMLElement | null;
14
21
  /** Relative horizontal drag distance since start. */
@@ -38,7 +45,16 @@ type DragObserverOptionsType = {
38
45
  export class DragObserver {
39
46
  protected _handler;
40
47
  protected root: EventTarget;
41
- protected start = {
48
+ protected start: {
49
+ event: MouseEvent | TouchEvent | null;
50
+ x: number;
51
+ y: number;
52
+ altKey: boolean;
53
+ ctrlKey: boolean;
54
+ metaKey: boolean;
55
+ shiftKey: boolean;
56
+ } = {
57
+ event: null,
42
58
  x: 0,
43
59
  y: 0,
44
60
  altKey: false,
@@ -48,6 +64,7 @@ export class DragObserver {
48
64
  };
49
65
  protected dragElem: HTMLElement | null = null;
50
66
  protected dragging: boolean = false;
67
+ protected customData: object = {};
51
68
  // TODO: touch events
52
69
  protected events = ["mousedown", "mouseup", "mousemove", "keydown"];
53
70
  protected opts: DragObserverOptionsType;
@@ -81,10 +98,16 @@ export class DragObserver {
81
98
  public stopDrag(cb_event?: DragCallbackArgType): void {
82
99
  if (this.dragging && this.opts.dragstop && cb_event) {
83
100
  cb_event.type = "dragstop";
84
- this.opts.dragstop(cb_event);
101
+ try {
102
+ this.opts.dragstop(cb_event);
103
+ } catch (err) {
104
+ console.error("dragstop error", err); // eslint-disable-line no-console
105
+ }
85
106
  }
86
107
  this.dragElem = null;
87
108
  this.dragging = false;
109
+ this.start.event = null;
110
+ this.customData = {};
88
111
  }
89
112
 
90
113
  protected handleEvent(e: MouseEvent): boolean | void {
@@ -92,12 +115,17 @@ export class DragObserver {
92
115
  const opts = this.opts;
93
116
  const cb_event: DragCallbackArgType = {
94
117
  type: e.type,
118
+ startEvent: type === "mousedown" ? e : this.start.event!,
95
119
  event: e,
120
+ customData: this.customData,
96
121
  dragElem: this.dragElem,
97
122
  dx: e.pageX - this.start.x,
98
123
  dy: e.pageY - this.start.y,
99
124
  apply: undefined,
100
125
  };
126
+
127
+ // console.log("handleEvent", type, cb_event);
128
+
101
129
  switch (type) {
102
130
  case "keydown":
103
131
  this.stopDrag(cb_event);
@@ -120,6 +148,7 @@ export class DragObserver {
120
148
  }
121
149
  }
122
150
  }
151
+ this.start.event = e;
123
152
  this.start.x = e.pageX;
124
153
  this.start.y = e.pageY;
125
154
  this.start.altKey = e.altKey;
package/src/types.ts CHANGED
@@ -12,6 +12,8 @@ import { WunderbaumOptions } from "./wb_options";
12
12
  export type TristateType = boolean | undefined;
13
13
  /** Show/hide checkbox or display a radiobutton icon instead. */
14
14
  export type CheckboxOption = boolean | "radio";
15
+ /** A value that can either be true, false, or undefined. */
16
+ export type SortOrderType = "asc" | "desc" | undefined;
15
17
  /** An icon may either be
16
18
  * a string-tag that references an entry in the `iconMap` (e.g. `"folderOpen"`)),
17
19
  * an HTML string that contains a `<` and is used as-is,
@@ -348,6 +350,23 @@ export interface ColumnDefinition {
348
350
  * Default: `4px`.
349
351
  */
350
352
  minWidth?: string | number;
353
+ /** Allow user to resize the column.
354
+ * Default: false.
355
+ */
356
+ resizable?: boolean;
357
+ /** Optional custom column width when user resized by mouse drag.
358
+ * Default: unset.
359
+ */
360
+ customWidthPx?: number;
361
+ /** Allow user to sort the column. Default: false. <br>
362
+ * **Note:** Sorting is not implemented yet.
363
+ */
364
+ sortable?: boolean;
365
+ /** Optional custom column sort orde when user clicked the sort icon.
366
+ * Default: unset. <br>
367
+ * **Note:** Sorting is not implemented yet.
368
+ */
369
+ sortOrder?: SortOrderType;
351
370
  /** Optional class names that are added to all `span.wb-col` header AND data
352
371
  * elements of that column.
353
372
  */
package/src/util.ts CHANGED
@@ -521,7 +521,7 @@ export function isArray(obj: any) {
521
521
  return Array.isArray(obj);
522
522
  }
523
523
 
524
- /** Return true if `obj` is of type `Object` and has no propertied. */
524
+ /** Return true if `obj` is of type `Object` and has no properties. */
525
525
  export function isEmptyObject(obj: any) {
526
526
  return Object.keys(obj).length === 0 && obj.constructor === Object;
527
527
  }
@@ -764,6 +764,58 @@ export function toSet(val: any): Set<string> {
764
764
  throw new Error("Cannot convert to Set<string>: " + val);
765
765
  }
766
766
 
767
+ /** Convert a pixel string to number.
768
+ * We accept a number or a string like '123px'. If undefined, the first default
769
+ * value that is a number or a string ending with 'px' is returned.
770
+ *
771
+ * Example:
772
+ * ```js
773
+ * let x = undefined;
774
+ * let y = "123px";
775
+ * const width = util.toPixel(x, y, 100); // returns 123
776
+ * ```
777
+ */
778
+ export function toPixel(
779
+ // val: string | number | undefined | null,
780
+ ...defaults: (string | number | undefined | null)[]
781
+ ): number {
782
+ // if (typeof val === "number") {
783
+ // return val;
784
+ // }
785
+ for (const d of defaults) {
786
+ if (typeof d === "number") {
787
+ return d;
788
+ }
789
+ if (typeof d === "string" && d.endsWith("px")) {
790
+ return parseInt(d, 10);
791
+ }
792
+ assert(d == null, `Expected a number or string like '123px': ${d}`);
793
+ }
794
+ throw new Error(`Expected a string like '123px': ${defaults}`);
795
+ }
796
+
797
+ /** Return the the boolean value of the first non-null element.
798
+ * Example:
799
+ * ```js
800
+ * const opts = { flag: true };
801
+ * const value = util.toBool(opts.foo, opts.flag, false); // returns true
802
+ * ```
803
+ */
804
+ export function toBool(
805
+ // val: boolean | undefined | null,
806
+ ...boolDefaults: (boolean | undefined | null)[]
807
+ ): boolean {
808
+ // if (val != null) {
809
+ // return !!val;
810
+ // }
811
+ for (const d of boolDefaults) {
812
+ if (d != null) {
813
+ return !!d;
814
+ }
815
+ }
816
+ throw new Error("No default boolean value provided");
817
+ }
818
+
767
819
  // /** Check if a string is contained in an Array or Set. */
768
820
  // export function isAnyOf(s: string, items: Array<string>|Set<string>): boolean {
769
821
  // return Array.prototype.includes.call(items, s)
@@ -52,6 +52,10 @@ export class FilterExtension extends WunderbaumExtension<FilterOptionsType> {
52
52
  const connectInput = this.getPluginOption("connectInput");
53
53
  if (connectInput) {
54
54
  this.queryInput = elemFromSelector(connectInput) as HTMLInputElement;
55
+ assert(
56
+ this.queryInput,
57
+ `Invalid 'filter.connectInput' option: ${connectInput}.`
58
+ );
55
59
  onEvent(
56
60
  this.queryInput,
57
61
  "input",
@@ -6,7 +6,9 @@
6
6
  import { Wunderbaum } from "./wunderbaum";
7
7
  import { WunderbaumExtension } from "./wb_extension_base";
8
8
  import { DragCallbackArgType, DragObserver } from "./drag_observer";
9
- import { GridOptionsType } from "./types";
9
+ import { ChangeType, ColumnDefinition, GridOptionsType } from "./types";
10
+ import { DEFAULT_MIN_COL_WIDTH } from "./common";
11
+ import { toBool, toPixel } from "./util";
10
12
 
11
13
  export class GridExtension extends WunderbaumExtension<GridOptionsType> {
12
14
  protected observer: DragObserver;
@@ -18,11 +20,47 @@ export class GridExtension extends WunderbaumExtension<GridOptionsType> {
18
20
 
19
21
  this.observer = new DragObserver({
20
22
  root: window.document,
21
- selector: "span.wb-col-resizer",
23
+ selector: "span.wb-col-resizer-active",
22
24
  thresh: 4,
23
25
  // throttle: 400,
24
26
  dragstart: (e) => {
25
- return this.tree.element.contains(e.dragElem);
27
+ const info = Wunderbaum.getEventInfo(e.startEvent);
28
+ const colDef = info.colDef!;
29
+ const allow =
30
+ colDef &&
31
+ this.tree.element.contains(e.dragElem) &&
32
+ toBool(colDef.resizable, tree.options.resizableColumns, false);
33
+
34
+ // this.tree.log("dragstart", colDef, e, info);
35
+
36
+ this.tree.element.classList.toggle("wb-col-resizing", !!allow);
37
+ info.colElem!.classList.toggle("wb-col-resizing", !!allow);
38
+
39
+ // We start dagging, so we remember the actual width in *pixels*
40
+ // (which may be 'auto' or '100%').
41
+ // Since we we re-create the markup on each update, we also cannot store
42
+ // the original event or DOM element, but only the colDef object.
43
+ if (allow) {
44
+ // Store initial target column infos in customData
45
+ e.customData.colDef = colDef;
46
+ e.customData.orgCustomWidthPx = colDef.customWidthPx;
47
+ const curWidthPx = Number.parseInt(info.colElem!.style.width, 10);
48
+ e.customData.orgWidthPx = curWidthPx;
49
+ // Set custom width to current width, so that we can modify it
50
+ colDef.customWidthPx = curWidthPx;
51
+ // this.tree.log(
52
+ // `dragstart customWidthPx=${colDef.customWidthPx}`,
53
+ // e,
54
+ // info
55
+ // );
56
+ this.tree.update(ChangeType.colStructure);
57
+ // this.tree.log(
58
+ // `dragstart 2 customWidthPx=${colDef.customWidthPx}`,
59
+ // e,
60
+ // info
61
+ // );
62
+ }
63
+ return allow;
26
64
  },
27
65
  drag: (e) => {
28
66
  // TODO: throttle
@@ -38,9 +76,31 @@ export class GridExtension extends WunderbaumExtension<GridOptionsType> {
38
76
  super.init();
39
77
  }
40
78
 
79
+ /**
80
+ * Hanldes drag and sragstop events for column resizing.
81
+ */
41
82
  protected handleDrag(e: DragCallbackArgType): void {
42
- const info = Wunderbaum.getEventInfo(e.event);
43
- // this.tree.options.
44
- this.tree.log(`${e.type}(${e.dx})`, e, info);
83
+ const custom = e.customData;
84
+ const colDef = <ColumnDefinition>custom.colDef!;
85
+ // this.tree.log(`${e.type} (dx=${e.dx})`, e, info);
86
+
87
+ if (e.type === "dragstop" || e.type === "drag") {
88
+ this.tree.element.classList.remove("wb-col-resizing");
89
+ // info.colElem!.classList.remove("wb-col-resizing");
90
+ if (e.apply || e.type === "drag") {
91
+ const minWidth = toPixel(colDef.minWidth, DEFAULT_MIN_COL_WIDTH);
92
+ const newWidth = Math.max(minWidth, custom.orgWidthPx + e.dx);
93
+ colDef.customWidthPx = newWidth;
94
+ // this.tree.log(
95
+ // `${e.type} minWidth=${minWidth}, newWidth=${newWidth}`,
96
+ // colDef
97
+ // );
98
+ } else {
99
+ // Drag was cancelled
100
+ this.tree.log("Column resize cancelled", e);
101
+ colDef.customWidthPx = custom.orgCustomWidthPx; // Restore original width or undefined
102
+ }
103
+ this.tree.update(ChangeType.colStructure);
104
+ }
45
105
  }
46
106
  }
package/src/wb_options.ts CHANGED
@@ -217,6 +217,11 @@ export interface WunderbaumOptions {
217
217
  * Default: false
218
218
  */
219
219
  fixedCol?: boolean;
220
+ /**
221
+ * Default value for ColumnDefinition.resizable option.
222
+ * Default: false
223
+ */
224
+ resizableColumns?: boolean;
220
225
 
221
226
  // --- Selection ---
222
227
  /**
@@ -183,6 +183,8 @@ div.wunderbaum {
183
183
  position: sticky;
184
184
  top: 0;
185
185
  z-index: 2;
186
+ -webkit-user-select: none; /* Safari */
187
+ user-select: none;
186
188
  }
187
189
 
188
190
  div.wb-header,
@@ -384,7 +386,12 @@ div.wunderbaum {
384
386
  border: none;
385
387
  border-right: 2px solid var(--wb-border-color);
386
388
  height: 100%;
387
- cursor: col-resize;
389
+ -webkit-user-select: none; // Safari
390
+ user-select: none;
391
+ &.wb-col-resizer-active {
392
+ cursor: col-resize;
393
+ // border-right-color: red;
394
+ }
388
395
  }
389
396
  }
390
397
 
@@ -405,6 +412,7 @@ div.wunderbaum {
405
412
  }
406
413
 
407
414
  span.wb-node {
415
+ -webkit-user-select: none; // Safari
408
416
  user-select: none;
409
417
  // &:first-of-type {
410
418
  // margin-left: 8px; // leftmost icon gets a little margin
@@ -765,12 +773,12 @@ div.wunderbaum {
765
773
  }
766
774
 
767
775
  .wb-no-select {
776
+ -webkit-user-select: none; // Safari
768
777
  user-select: none;
769
- -webkit-user-select: none;
770
778
 
771
779
  span.wb-title {
780
+ -webkit-user-select: contain; // Safari
772
781
  user-select: contain;
773
- -webkit-user-select: contain;
774
782
  }
775
783
  }
776
784
 
package/src/wunderbaum.ts CHANGED
@@ -1602,6 +1602,14 @@ export class Wunderbaum {
1602
1602
  }
1603
1603
  }
1604
1604
 
1605
+ /** Reset column widths to default. */
1606
+ resetColumns() {
1607
+ this.columns.forEach((col) => {
1608
+ delete col.customWidthPx;
1609
+ });
1610
+ this.update(ChangeType.colStructure);
1611
+ }
1612
+
1605
1613
  /**
1606
1614
  * Make sure that this node is vertically scrolled into the viewport.
1607
1615
  *
@@ -2026,7 +2034,7 @@ export class Wunderbaum {
2026
2034
  this._columnsById = {};
2027
2035
  for (const col of columns) {
2028
2036
  this._columnsById[<string>col.id] = col;
2029
- const cw = col.width;
2037
+ const cw = col.customWidthPx ? `${col.customWidthPx}px` : col.width;
2030
2038
  if (col.id === "*" && col !== col0) {
2031
2039
  throw new Error(
2032
2040
  `Column id '*' must be defined only once: '${col.title}'.`
@@ -2139,7 +2147,12 @@ export class Wunderbaum {
2139
2147
  }
2140
2148
  let resizer = "";
2141
2149
  if (i < colCount - 1) {
2142
- resizer = '<span class="wb-col-resizer"></span>';
2150
+ if (util.toBool(col.resizable, this.options.resizableColumns, false)) {
2151
+ resizer =
2152
+ '<span class="wb-col-resizer wb-col-resizer-active"></span>';
2153
+ } else {
2154
+ resizer = '<span class="wb-col-resizer"></span>';
2155
+ }
2143
2156
  }
2144
2157
  colElem.innerHTML = `<span class="wb-col-title"${tooltip}>${title}</span>${resizer}`;
2145
2158
  if (this.isCellNav()) {
@@ -2229,6 +2242,10 @@ export class Wunderbaum {
2229
2242
  }
2230
2243
 
2231
2244
  if (this.options.connectTopBreadcrumb) {
2245
+ util.assert(
2246
+ this.options.connectTopBreadcrumb.textContent != null,
2247
+ `Invalid 'connectTopBreadcrumb' option (input element expected).`
2248
+ );
2232
2249
  let path = this.getTopmostVpNode(true)?.getPath(false, "title", " > ");
2233
2250
  path = path ? path + " >" : "";
2234
2251
  this.options.connectTopBreadcrumb.textContent = path;