react-arborist 2.0.0-rc.1 → 2.0.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.
@@ -23,7 +23,7 @@ export declare type DragPreviewProps = {
23
23
  dragIds: string[];
24
24
  isDragging: boolean;
25
25
  };
26
- export declare type DropCursorProps = {
26
+ export declare type CursorProps = {
27
27
  top: number;
28
28
  left: number;
29
29
  indent: number;
@@ -15,7 +15,7 @@ export interface TreeProps<T extends IdObj> {
15
15
  children?: ElementType<renderers.NodeRendererProps<T>>;
16
16
  renderRow?: ElementType<renderers.RowRendererProps<T>>;
17
17
  renderDragPreview?: ElementType<renderers.DragPreviewProps>;
18
- renderCursor?: ElementType<renderers.DropCursorProps>;
18
+ renderCursor?: ElementType<renderers.CursorProps>;
19
19
  renderContainer?: ElementType<{}>;
20
20
  rowHeight?: number;
21
21
  width?: number;
@@ -33,11 +33,14 @@ export interface TreeProps<T extends IdObj> {
33
33
  onActivate?: (node: NodeApi<T>) => void;
34
34
  onSelect?: (nodes: NodeApi<T>[]) => void;
35
35
  onScroll?: (props: ListOnScrollProps) => void;
36
+ onToggle?: (id: string) => void;
37
+ onFocus?: (node: NodeApi<T>) => void;
36
38
  selection?: string;
37
39
  initialOpenState?: OpenMap;
38
40
  searchTerm?: string;
39
41
  searchMatch?: (node: NodeApi<T>, searchTerm: string) => boolean;
40
42
  className?: string | undefined;
43
+ rowClassName?: string | undefined;
41
44
  dndRootElement?: globalThis.Node | null;
42
45
  onClick?: MouseEventHandler;
43
46
  onContextMenu?: MouseEventHandler;
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { NodeApi } from "./interfaces/node-api";
2
+ import { TreeApi } from "./interfaces/tree-api";
2
3
  import { IdObj } from "./types/utils";
3
4
  export declare function bound(n: number, min: number, max: number): number;
4
5
  export declare function isItem(node: NodeApi<any> | null): boolean | null;
@@ -10,6 +11,7 @@ export declare const isDecendent: (a: NodeApi<any>, b: NodeApi<any>) => boolean;
10
11
  export declare const indexOf: (node: NodeApi<any>) => number;
11
12
  export declare function noop(): void;
12
13
  export declare function dfs(node: NodeApi<any>, id: string): NodeApi<any> | null;
14
+ export declare function walk(node: NodeApi<any>, fn: (node: NodeApi<any>) => void): void;
13
15
  export declare function focusNextElement(target: HTMLElement): void;
14
16
  export declare function focusPrevElement(target: HTMLElement): void;
15
17
  export declare function access<T = boolean>(obj: any, accessor: string | boolean | Function): T;
@@ -18,3 +20,5 @@ export declare function identify(obj: string | IdObj): string;
18
20
  export declare function mergeRefs(...refs: any): (instance: any) => void;
19
21
  export declare function safeRun<T extends (...args: any[]) => any>(fn: T | undefined, ...args: Parameters<T>): any;
20
22
  export declare function waitFor(fn: () => boolean): Promise<void>;
23
+ export declare function getInsertIndex(tree: TreeApi<any>): number;
24
+ export declare function getInsertParentId(tree: TreeApi<any>): string | null;
package/package.json CHANGED
@@ -1,13 +1,24 @@
1
1
  {
2
2
  "name": "react-arborist",
3
- "version": "2.0.0-rc.1",
3
+ "version": "2.0.0",
4
4
  "license": "MIT",
5
5
  "source": "src/index.ts",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/module.js",
8
8
  "types": "dist/index.d.ts",
9
- "repository": "github:brimdata/react-arborist",
9
+ "repository": "https://github.com/brimdata/react-arborist",
10
10
  "homepage": "https://react-arborist.netlify.app",
11
+ "keywords": [
12
+ "react",
13
+ "arborist",
14
+ "react-arborist",
15
+ "treeview",
16
+ "tree",
17
+ "vitualized",
18
+ "dnd",
19
+ "multiselection",
20
+ "filterable"
21
+ ],
11
22
  "dependencies": {
12
23
  "react-dnd": "^14.0.3",
13
24
  "react-dnd-html5-backend": "^14.0.1",
@@ -1,6 +1,6 @@
1
1
  import { useDndContext, useTreeApi } from "../context";
2
2
 
3
- export function DropCursor() {
3
+ export function Cursor() {
4
4
  const tree = useTreeApi();
5
5
  const state = useDndContext();
6
6
  const cursor = state.cursor;
@@ -1,5 +1,5 @@
1
1
  import React, { CSSProperties } from "react";
2
- import { DropCursorProps } from "../types/renderers";
2
+ import { CursorProps } from "../types/renderers";
3
3
 
4
4
  const placeholderStyle = {
5
5
  display: "flex",
@@ -25,7 +25,7 @@ export const DefaultCursor = React.memo(function DefaultCursor({
25
25
  top,
26
26
  left,
27
27
  indent,
28
- }: DropCursorProps) {
28
+ }: CursorProps) {
29
29
  const style: CSSProperties = {
30
30
  position: "absolute",
31
31
  pointerEvents: "none",
@@ -1,7 +1,7 @@
1
1
  import { forwardRef } from "react";
2
2
  import { useTreeApi } from "../context";
3
3
  import { treeBlur } from "../state/focus-slice";
4
- import { DropCursor } from "./cursor";
4
+ import { Cursor } from "./cursor";
5
5
 
6
6
  export const ListOuterElement = forwardRef(function Outer(
7
7
  props: React.HTMLProps<HTMLDivElement>,
@@ -35,11 +35,8 @@ const DropContainer = () => {
35
35
  left: "0",
36
36
  right: "0",
37
37
  }}
38
- onClick={(e) => {
39
- console.log(e.currentTarget, e.target);
40
- }}
41
38
  >
42
- <DropCursor />
39
+ <Cursor />
43
40
  </div>
44
41
  );
45
42
  };
@@ -63,6 +63,7 @@ export const RowContainer = React.memo(function RowContainer<T extends IdObj>({
63
63
  "aria-selected": node.isSelected,
64
64
  style: rowStyle,
65
65
  tabIndex: -1,
66
+ className: tree.props.rowClassName,
66
67
  };
67
68
 
68
69
  useEffect(() => {
@@ -1,6 +1,7 @@
1
1
  import { useDrop } from "react-dnd";
2
2
  import { useTreeApi } from "../context";
3
3
  import { DragItem } from "../types/dnd";
4
+ import { isDecendent } from "../utils";
4
5
  import { computeDrop } from "./compute-drop";
5
6
  import { DropResult } from "./drop-hook";
6
7
 
@@ -13,9 +14,28 @@ export function useOuterDrop() {
13
14
  accept: "NODE",
14
15
  hover: (item, m) => {
15
16
  if (!m.isOver({ shallow: true })) return;
17
+ if (m.canDrop()) {
18
+ const offset = m.getClientOffset();
19
+ if (!tree.listEl.current || !offset) return;
20
+ const { cursor } = computeDrop({
21
+ element: tree.listEl.current,
22
+ offset: offset,
23
+ indent: tree.indent,
24
+ node: null,
25
+ prevNode: tree.visibleNodes[tree.visibleNodes.length - 1],
26
+ nextNode: null,
27
+ });
28
+ if (cursor) tree.showCursor(cursor);
29
+ } else {
30
+ tree.hideCursor();
31
+ }
32
+ },
33
+ canDrop: (item, m) => {
34
+ if (!m.isOver({ shallow: true })) return false;
35
+ if (tree.isFiltered) return false;
16
36
  const offset = m.getClientOffset();
17
- if (!tree.listEl.current || !offset) return;
18
- const { cursor } = computeDrop({
37
+ if (!tree.listEl.current || !offset) return false;
38
+ const { drop } = computeDrop({
19
39
  element: tree.listEl.current,
20
40
  offset: offset,
21
41
  indent: tree.indent,
@@ -23,10 +43,16 @@ export function useOuterDrop() {
23
43
  prevNode: tree.visibleNodes[tree.visibleNodes.length - 1],
24
44
  nextNode: null,
25
45
  });
26
- if (cursor) tree.showCursor(cursor);
27
- },
28
- canDrop: (item, m) => {
29
- return m.isOver({ shallow: true });
46
+ if (!drop) return false;
47
+ const dropParent = tree.get(drop.parentId) ?? tree.root;
48
+
49
+ for (let id of item.dragIds) {
50
+ const drag = tree.get(id);
51
+ if (!drag) return false;
52
+ if (!dropParent) return false;
53
+ if (drag.isInternal && isDecendent(dropParent, drag)) return false;
54
+ }
55
+ return true;
30
56
  },
31
57
  drop: (item, m) => {
32
58
  if (m.didDrop()) return;
@@ -54,6 +54,10 @@ export class NodeApi<T extends IdObj = IdObj> {
54
54
  return this.isLeaf ? false : this.tree.isOpen(this.id);
55
55
  }
56
56
 
57
+ get isClosed() {
58
+ return this.isLeaf ? false : !this.tree.isOpen(this.id);
59
+ }
60
+
57
61
  get isEditing() {
58
62
  return this.tree.editingId === this.id;
59
63
  }
@@ -62,6 +66,10 @@ export class NodeApi<T extends IdObj = IdObj> {
62
66
  return this.tree.isSelected(this.id);
63
67
  }
64
68
 
69
+ get isOnlySelection() {
70
+ return this.isSelected && this.tree.hasOneSelection;
71
+ }
72
+
65
73
  get isSelectedStart() {
66
74
  return this.isSelected && !this.prev?.isSelected;
67
75
  }
@@ -84,13 +92,16 @@ export class NodeApi<T extends IdObj = IdObj> {
84
92
 
85
93
  get state() {
86
94
  return {
87
- isEditing: this.isEditing,
95
+ isClosed: this.isClosed,
88
96
  isDragging: this.isDragging,
89
- isSelected: this.isSelected,
90
- isSelectedStart: this.isSelectedStart,
91
- isSelectedEnd: this.isSelectedEnd,
97
+ isEditing: this.isEditing,
92
98
  isFocused: this.isFocused,
99
+ isInternal: this.isInternal,
100
+ isLeaf: this.isLeaf,
93
101
  isOpen: this.isOpen,
102
+ isSelected: this.isSelected,
103
+ isSelectedEnd: this.isSelectedEnd,
104
+ isSelectedStart: this.isSelectedStart,
94
105
  willReceiveDrop: this.willReceiveDrop,
95
106
  };
96
107
  }
@@ -68,19 +68,19 @@ export class TreeApi<T extends IdObj> {
68
68
  /* Tree Props */
69
69
 
70
70
  get width() {
71
- return this.props.width || 300;
71
+ return this.props.width ?? 300;
72
72
  }
73
73
 
74
74
  get height() {
75
- return this.props.height || 500;
75
+ return this.props.height ?? 500;
76
76
  }
77
77
 
78
78
  get indent() {
79
- return this.props.indent || 24;
79
+ return this.props.indent ?? 24;
80
80
  }
81
81
 
82
82
  get rowHeight() {
83
- return this.props.rowHeight || 24;
83
+ return this.props.rowHeight ?? 24;
84
84
  }
85
85
 
86
86
  get searchTerm() {
@@ -176,33 +176,27 @@ export class TreeApi<T extends IdObj> {
176
176
  }
177
177
 
178
178
  createInternal() {
179
- return this.create("internal");
179
+ return this.create({ type: "internal" });
180
180
  }
181
181
 
182
182
  createLeaf() {
183
- return this.create("leaf");
184
- }
185
-
186
- private async create(type: "internal" | "leaf") {
187
- let index;
188
- let parentId;
189
- const focus = this.focusedNode;
190
- if (focus && focus.parent) {
191
- if (focus.isInternal && focus.isOpen) {
192
- parentId = focus.id;
193
- index = 0;
194
- } else {
195
- index = focus.childIndex + 1;
196
- parentId = focus.parent.isRoot ? null : focus.parent.id;
197
- }
198
- } else {
199
- index = this.root?.children?.length || -1;
200
- parentId = null;
201
- }
183
+ return this.create({ type: "leaf" });
184
+ }
185
+
186
+ async create(
187
+ opts: {
188
+ type?: "internal" | "leaf";
189
+ parentId?: null | string;
190
+ index?: null | number;
191
+ } = {}
192
+ ) {
202
193
  const data = await safeRun(this.props.onCreate, {
203
- parentId,
204
- index,
205
- type,
194
+ type: opts.type ?? "leaf",
195
+ parentId:
196
+ opts.parentId === undefined
197
+ ? utils.getInsertParentId(this)
198
+ : opts.parentId,
199
+ index: opts.index ?? utils.getInsertIndex(this),
206
200
  });
207
201
  if (data) {
208
202
  this.focus(data);
@@ -284,6 +278,7 @@ export class TreeApi<T extends IdObj> {
284
278
  } else {
285
279
  this.dispatch(focus(identify(node)));
286
280
  if (opts.scroll !== false) this.scrollTo(node);
281
+ if (this.focusedNode) safeRun(this.props.onFocus, this.focusedNode);
287
282
  }
288
283
  }
289
284
 
@@ -321,6 +316,7 @@ export class TreeApi<T extends IdObj> {
321
316
  this.dispatch(selection.anchor(id));
322
317
  this.dispatch(selection.mostRecent(id));
323
318
  this.scrollTo(id, opts.align);
319
+ if (this.focusedNode) safeRun(this.props.onFocus, this.focusedNode);
324
320
  safeRun(this.props.onSelect, this.selectedNodes);
325
321
  }
326
322
 
@@ -338,6 +334,7 @@ export class TreeApi<T extends IdObj> {
338
334
  this.dispatch(selection.anchor(node.id));
339
335
  this.dispatch(selection.mostRecent(node.id));
340
336
  this.scrollTo(node);
337
+ if (this.focusedNode) safeRun(this.props.onFocus, this.focusedNode);
341
338
  safeRun(this.props.onSelect, this.selectedNodes);
342
339
  }
343
340
 
@@ -350,6 +347,7 @@ export class TreeApi<T extends IdObj> {
350
347
  this.dispatch(selection.add(this.nodesBetween(anchor, identifyNull(id))));
351
348
  this.dispatch(selection.mostRecent(id));
352
349
  this.scrollTo(id);
350
+ if (this.focusedNode) safeRun(this.props.onFocus, this.focusedNode);
353
351
  safeRun(this.props.onSelect, this.selectedNodes);
354
352
  }
355
353
 
@@ -365,6 +363,7 @@ export class TreeApi<T extends IdObj> {
365
363
  this.dispatch(focus(this.lastNode?.id));
366
364
  this.dispatch(selection.anchor(this.firstNode));
367
365
  this.dispatch(selection.mostRecent(this.lastNode));
366
+ if (this.focusedNode) safeRun(this.props.onFocus, this.focusedNode);
368
367
  safeRun(this.props.onSelect, this.selectedNodes);
369
368
  }
370
369
 
@@ -397,13 +396,17 @@ export class TreeApi<T extends IdObj> {
397
396
  open(identity: Identity) {
398
397
  const id = identifyNull(identity);
399
398
  if (!id) return;
399
+ if (this.isOpen(id)) return;
400
400
  this.dispatch(visibility.open(id, this.isFiltered));
401
+ safeRun(this.props.onToggle, id);
401
402
  }
402
403
 
403
404
  close(identity: Identity) {
404
405
  const id = identifyNull(identity);
405
406
  if (!id) return;
407
+ if (!this.isOpen(id)) return;
406
408
  this.dispatch(visibility.close(id, this.isFiltered));
409
+ safeRun(this.props.onToggle, id);
407
410
  }
408
411
 
409
412
  toggle(identity: Identity) {
@@ -439,6 +442,18 @@ export class TreeApi<T extends IdObj> {
439
442
  }
440
443
  }
441
444
 
445
+ openAll() {
446
+ utils.walk(this.root, (node) => {
447
+ if (node.isInternal) node.open();
448
+ });
449
+ }
450
+
451
+ closeAll() {
452
+ utils.walk(this.root, (node) => {
453
+ if (node.isInternal) node.close();
454
+ });
455
+ }
456
+
442
457
  /* Scrolling */
443
458
 
444
459
  scrollTo(identity: Identity, align: Align = "smart") {
@@ -471,6 +486,18 @@ export class TreeApi<T extends IdObj> {
471
486
  return this.state.nodes.focus.treeFocused;
472
487
  }
473
488
 
489
+ get hasNoSelection() {
490
+ return this.state.nodes.selection.ids.size === 0;
491
+ }
492
+
493
+ get hasOneSelection() {
494
+ return this.state.nodes.selection.ids.size === 1;
495
+ }
496
+
497
+ get hasMultipleSelections() {
498
+ return this.state.nodes.selection.ids.size > 1;
499
+ }
500
+
474
501
  isSelected(id?: string) {
475
502
  if (!id) return false;
476
503
  return this.state.nodes.selection.ids.has(id);
@@ -27,7 +27,7 @@ export type DragPreviewProps = {
27
27
  isDragging: boolean;
28
28
  };
29
29
 
30
- export type DropCursorProps = {
30
+ export type CursorProps = {
31
31
  top: number;
32
32
  left: number;
33
33
  indent: number;
@@ -21,7 +21,7 @@ export interface TreeProps<T extends IdObj> {
21
21
  children?: ElementType<renderers.NodeRendererProps<T>>;
22
22
  renderRow?: ElementType<renderers.RowRendererProps<T>>;
23
23
  renderDragPreview?: ElementType<renderers.DragPreviewProps>;
24
- renderCursor?: ElementType<renderers.DropCursorProps>;
24
+ renderCursor?: ElementType<renderers.CursorProps>;
25
25
  renderContainer?: ElementType<{}>;
26
26
 
27
27
  /* Sizes */
@@ -45,6 +45,8 @@ export interface TreeProps<T extends IdObj> {
45
45
  onActivate?: (node: NodeApi<T>) => void;
46
46
  onSelect?: (nodes: NodeApi<T>[]) => void;
47
47
  onScroll?: (props: ListOnScrollProps) => void;
48
+ onToggle?: (id: string) => void;
49
+ onFocus?: (node: NodeApi<T>) => void;
48
50
 
49
51
  /* Selection */
50
52
  selection?: string;
@@ -58,6 +60,8 @@ export interface TreeProps<T extends IdObj> {
58
60
 
59
61
  /* Extra */
60
62
  className?: string | undefined;
63
+ rowClassName?: string | undefined;
64
+
61
65
  dndRootElement?: globalThis.Node | null;
62
66
  onClick?: MouseEventHandler;
63
67
  onContextMenu?: MouseEventHandler;
package/src/utils.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { NodeApi } from "./interfaces/node-api";
2
+ import { TreeApi } from "./interfaces/tree-api";
2
3
  import { IdObj } from "./types/utils";
3
4
 
4
5
  export function bound(n: number, min: number, max: number) {
@@ -44,6 +45,18 @@ export function dfs(node: NodeApi<any>, id: string): NodeApi<any> | null {
44
45
  return null;
45
46
  }
46
47
 
48
+ export function walk(
49
+ node: NodeApi<any>,
50
+ fn: (node: NodeApi<any>) => void
51
+ ): void {
52
+ fn(node);
53
+ if (node.children) {
54
+ for (let child of node.children) {
55
+ walk(child, fn);
56
+ }
57
+ }
58
+ }
59
+
47
60
  export function focusNextElement(target: HTMLElement) {
48
61
  const elements = getFocusable(target);
49
62
 
@@ -147,3 +160,19 @@ export function waitFor(fn: () => boolean) {
147
160
  check();
148
161
  });
149
162
  }
163
+
164
+ export function getInsertIndex(tree: TreeApi<any>) {
165
+ const focus = tree.focusedNode;
166
+ if (!focus) return tree.root.children?.length ?? 0;
167
+ if (focus.isOpen) return 0;
168
+ if (focus.parent) return focus.childIndex + 1;
169
+ return 0;
170
+ }
171
+
172
+ export function getInsertParentId(tree: TreeApi<any>) {
173
+ const focus = tree.focusedNode;
174
+ if (!focus) return null;
175
+ if (focus.isOpen) return focus.id;
176
+ if (focus.parent) return focus.parent.id;
177
+ return null;
178
+ }