react-arborist 3.10.4 → 3.10.5

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.
@@ -149,6 +149,19 @@ export declare class TreeApi<T> {
149
149
  get matchFn(): (node: NodeApi<T>) => boolean;
150
150
  accessChildren(data: T): readonly T[] | null;
151
151
  accessId(data: T): string;
152
+ /**
153
+ * Resolve an identifier to a node id. Public methods accept an id string, a
154
+ * NodeApi, or the raw row data; this is the one place that turns any of those
155
+ * into the string id used internally. Raw data is run through the configured
156
+ * `idAccessor` so a custom accessor (e.g. `uuid`) is honored everywhere, not
157
+ * just where nodes were built. A NodeApi already carries its accessor-derived
158
+ * `id`, so it is used directly rather than re-accessed (the accessor reads the
159
+ * underlying data, which a NodeApi does not expose under that key). Unlike
160
+ * `accessId`, an unresolved id comes back as `undefined` rather than throwing,
161
+ * preserving the previous behavior of the `id`-only lookup.
162
+ */
163
+ identify(identity: string | IdObj | T): string;
164
+ identifyNull(identity: Identity | T): string | null;
152
165
  get firstNode(): NodeApi<T>;
153
166
  get lastNode(): NodeApi<T>;
154
167
  get focusedNode(): NodeApi<T> | null;
@@ -158,7 +171,7 @@ export declare class TreeApi<T> {
158
171
  get(id: string | null): NodeApi<T> | null;
159
172
  at(index: number): NodeApi<T> | null;
160
173
  nodesBetween(startId: string | null, endId: string | null): NodeApi<T>[];
161
- indexOf(id: Identity): number | null;
174
+ indexOf(id: Identity | T): number | null;
162
175
  get editingId(): string | null;
163
176
  createInternal(): Promise<void>;
164
177
  createLeaf(): Promise<void>;
@@ -167,36 +180,36 @@ export declare class TreeApi<T> {
167
180
  parentId?: null | string;
168
181
  index?: null | number;
169
182
  }): Promise<void>;
170
- delete(node: Identity | string[] | IdObj[]): Promise<void>;
171
- edit(node: string | IdObj): Promise<EditResult>;
172
- submit(identity: Identity, value: string): Promise<void>;
183
+ delete(node: Identity | T | (string | IdObj | T)[]): Promise<void>;
184
+ edit(node: string | IdObj | T): Promise<EditResult>;
185
+ submit(identity: Identity | T, value: string): Promise<void>;
173
186
  reset(): void;
174
- activate(id: Identity): void;
187
+ activate(id: Identity | T): void;
175
188
  private resolveEdit;
176
189
  get selectedIds(): Set<string>;
177
190
  get selectedNodes(): NodeApi<T>[];
178
- focus(node: Identity, opts?: {
191
+ focus(node: Identity | T, opts?: {
179
192
  scroll?: boolean;
180
193
  }): void;
181
194
  pageUp(): void;
182
195
  pageDown(): void;
183
- select(node: Identity, opts?: {
196
+ select(node: Identity | T, opts?: {
184
197
  align?: Align;
185
198
  focus?: boolean;
186
199
  }): void;
187
- deselect(node: Identity): void;
188
- selectMulti(identity: Identity, opts?: {
200
+ deselect(node: Identity | T): void;
201
+ selectMulti(identity: Identity | T, opts?: {
189
202
  align?: Align;
190
203
  focus?: boolean;
191
204
  }): void;
192
- selectContiguous(identity: Identity): void;
205
+ selectContiguous(identity: Identity | T): void;
193
206
  deselectAll(): void;
194
207
  selectAll(): void;
195
208
  private filterSelectableNodes;
196
209
  setSelection(args: {
197
- ids: (IdObj | string)[] | null;
198
- anchor: Identity;
199
- mostRecent: Identity;
210
+ ids: (IdObj | string | T)[] | null;
211
+ anchor: Identity | T;
212
+ mostRecent: Identity | T;
200
213
  }): void;
201
214
  get cursorParentId(): string | null;
202
215
  get cursorOverFolder(): boolean;
@@ -208,14 +221,14 @@ export declare class TreeApi<T> {
208
221
  drop(): void;
209
222
  hideCursor(): void;
210
223
  showCursor(cursor: Cursor): void;
211
- open(identity: Identity, redraw?: boolean): void;
212
- close(identity: Identity, redraw?: boolean): void;
213
- toggle(identity: Identity): void;
214
- openParents(identity: Identity): void;
224
+ open(identity: Identity | T, redraw?: boolean): void;
225
+ close(identity: Identity | T, redraw?: boolean): void;
226
+ toggle(identity: Identity | T): void;
227
+ openParents(identity: Identity | T): void;
215
228
  openSiblings(node: NodeApi<T>): void;
216
229
  openAll(): void;
217
230
  closeAll(): void;
218
- scrollTo(identity: Identity, align?: Align): Promise<void> | undefined;
231
+ scrollTo(identity: Identity | T, align?: Align): Promise<void> | undefined;
219
232
  get isEditing(): boolean;
220
233
  get isFiltered(): boolean;
221
234
  get hasFocus(): boolean;
@@ -228,10 +241,10 @@ export declare class TreeApi<T> {
228
241
  isDraggable(data: T): boolean;
229
242
  isSelectable(data: T): boolean;
230
243
  private isActionPossible;
231
- isDragging(node: Identity): boolean;
244
+ isDragging(node: Identity | T): boolean;
232
245
  isFocused(id: string): boolean;
233
246
  isMatch(node: NodeApi<T>): boolean;
234
- willReceiveDrop(node: Identity): boolean;
247
+ willReceiveDrop(node: Identity | T): boolean;
235
248
  onFocus(): void;
236
249
  onBlur(): void;
237
250
  onItemsRendered(args: ListOnItemsRenderedProps): void;
@@ -37,6 +37,7 @@ const utils = __importStar(require("../utils"));
37
37
  const default_cursor_1 = require("../components/default-cursor");
38
38
  const default_row_1 = require("../components/default-row");
39
39
  const default_node_1 = require("../components/default-node");
40
+ const node_api_1 = require("./node-api");
40
41
  const edit_slice_1 = require("../state/edit-slice");
41
42
  const focus_slice_1 = require("../state/focus-slice");
42
43
  const create_root_1 = require("../data/create-root");
@@ -47,7 +48,7 @@ const default_drag_preview_1 = require("../components/default-drag-preview");
47
48
  const default_container_1 = require("../components/default-container");
48
49
  const create_list_1 = require("../data/create-list");
49
50
  const create_index_1 = require("../data/create-index");
50
- const { safeRun, identify, identifyNull } = utils;
51
+ const { safeRun } = utils;
51
52
  class TreeApi {
52
53
  constructor(store, props, list, listEl) {
53
54
  this.store = store;
@@ -189,6 +190,30 @@ class TreeApi {
189
190
  throw new Error("Data must contain an 'id' property or props.idAccessor must return a string");
190
191
  return id;
191
192
  }
193
+ /**
194
+ * Resolve an identifier to a node id. Public methods accept an id string, a
195
+ * NodeApi, or the raw row data; this is the one place that turns any of those
196
+ * into the string id used internally. Raw data is run through the configured
197
+ * `idAccessor` so a custom accessor (e.g. `uuid`) is honored everywhere, not
198
+ * just where nodes were built. A NodeApi already carries its accessor-derived
199
+ * `id`, so it is used directly rather than re-accessed (the accessor reads the
200
+ * underlying data, which a NodeApi does not expose under that key). Unlike
201
+ * `accessId`, an unresolved id comes back as `undefined` rather than throwing,
202
+ * preserving the previous behavior of the `id`-only lookup.
203
+ */
204
+ identify(identity) {
205
+ if (typeof identity === "string")
206
+ return identity;
207
+ if (identity instanceof node_api_1.NodeApi)
208
+ return identity.id;
209
+ const get = this.props.idAccessor || "id";
210
+ return utils.access(identity, get);
211
+ }
212
+ identifyNull(identity) {
213
+ if (identity === null || identity === undefined)
214
+ return null;
215
+ return this.identify(identity);
216
+ }
192
217
  /* Node Access */
193
218
  get firstNode() {
194
219
  var _a;
@@ -244,7 +269,7 @@ class TreeApi {
244
269
  return this.visibleNodes.slice(start, end + 1);
245
270
  }
246
271
  indexOf(id) {
247
- const key = utils.identifyNull(id);
272
+ const key = this.identifyNull(id);
248
273
  if (!key)
249
274
  return null;
250
275
  return this.idToIndex[key];
@@ -287,7 +312,7 @@ class TreeApi {
287
312
  if (!node)
288
313
  return;
289
314
  const idents = Array.isArray(node) ? node : [node];
290
- const ids = idents.map(identify);
315
+ const ids = idents.map((i) => this.identify(i));
291
316
  const nodes = ids.map((id) => this.get(id)).filter((n) => !!n);
292
317
  /* Guard against Math.min(...[]) === Infinity when no ids resolve to nodes. */
293
318
  const fromIndex = nodes.length ? Math.min(...nodes.map((n) => { var _a; return (_a = n.rowIndex) !== null && _a !== void 0 ? _a : 0; })) : 0;
@@ -297,7 +322,7 @@ class TreeApi {
297
322
  }
298
323
  edit(node) {
299
324
  var _a, _b;
300
- const id = identify(node);
325
+ const id = this.identify(node);
301
326
  this.resolveEdit({ cancelled: true });
302
327
  this.scrollTo(id);
303
328
  this.dispatch((0, edit_slice_1.edit)(id));
@@ -311,7 +336,7 @@ class TreeApi {
311
336
  var _a, _b;
312
337
  if (!identity)
313
338
  return;
314
- const id = identify(identity);
339
+ const id = this.identify(identity);
315
340
  yield safeRun(this.props.onRename, {
316
341
  id,
317
342
  name: value,
@@ -330,7 +355,7 @@ class TreeApi {
330
355
  setTimeout(() => this.onFocus()); // Return focus to element;
331
356
  }
332
357
  activate(id) {
333
- const node = this.get(identifyNull(id));
358
+ const node = this.get(this.identifyNull(id));
334
359
  if (!node)
335
360
  return;
336
361
  safeRun(this.props.onActivate, node);
@@ -364,7 +389,7 @@ class TreeApi {
364
389
  this.select(node);
365
390
  }
366
391
  else {
367
- this.dispatch((0, focus_slice_1.focus)(identify(node)));
392
+ this.dispatch((0, focus_slice_1.focus)(this.identify(node)));
368
393
  if (opts.scroll !== false)
369
394
  this.scrollTo(node);
370
395
  if (this.focusedNode)
@@ -404,7 +429,7 @@ class TreeApi {
404
429
  if (!node)
405
430
  return;
406
431
  const changeFocus = opts.focus !== false;
407
- const id = identify(node);
432
+ const id = this.identify(node);
408
433
  if (changeFocus)
409
434
  this.dispatch((0, focus_slice_1.focus)(id));
410
435
  if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
@@ -422,12 +447,12 @@ class TreeApi {
422
447
  deselect(node) {
423
448
  if (!node)
424
449
  return;
425
- const id = identify(node);
450
+ const id = this.identify(node);
426
451
  this.dispatch(selection_slice_1.actions.remove(id));
427
452
  safeRun(this.props.onSelect, this.selectedNodes);
428
453
  }
429
454
  selectMulti(identity, opts = {}) {
430
- const node = this.get(identifyNull(identity));
455
+ const node = this.get(this.identifyNull(identity));
431
456
  if (!node)
432
457
  return;
433
458
  const changeFocus = opts.focus !== false;
@@ -448,11 +473,11 @@ class TreeApi {
448
473
  var _a;
449
474
  if (!identity)
450
475
  return;
451
- const id = identify(identity);
476
+ const id = this.identify(identity);
452
477
  this.dispatch((0, focus_slice_1.focus)(id));
453
478
  if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
454
479
  const { anchor, mostRecent } = this.state.nodes.selection;
455
- const selectableNodes = this.filterSelectableNodes(this.nodesBetween(anchor, identifyNull(id)));
480
+ const selectableNodes = this.filterSelectableNodes(this.nodesBetween(anchor, this.identifyNull(id)));
456
481
  this.dispatch(selection_slice_1.actions.remove(this.nodesBetween(anchor, mostRecent)));
457
482
  this.dispatch(selection_slice_1.actions.add(selectableNodes));
458
483
  this.dispatch(selection_slice_1.actions.mostRecent(id));
@@ -481,14 +506,14 @@ class TreeApi {
481
506
  }
482
507
  filterSelectableNodes(nodes) {
483
508
  return nodes
484
- .map((n) => this.get(identify(n)))
509
+ .map((n) => this.get(this.identify(n)))
485
510
  .filter((n) => !!n && n.isSelectable);
486
511
  }
487
512
  setSelection(args) {
488
513
  var _a;
489
- const ids = new Set((_a = args.ids) === null || _a === void 0 ? void 0 : _a.map(identify));
490
- const anchor = identifyNull(args.anchor);
491
- const mostRecent = identifyNull(args.mostRecent);
514
+ const ids = new Set((_a = args.ids) === null || _a === void 0 ? void 0 : _a.map((i) => this.identify(i)));
515
+ const anchor = this.identifyNull(args.anchor);
516
+ const mostRecent = this.identifyNull(args.mostRecent);
492
517
  this.dispatch(selection_slice_1.actions.set({ ids, anchor, mostRecent }));
493
518
  safeRun(this.props.onSelect, this.selectedNodes);
494
519
  }
@@ -571,7 +596,7 @@ class TreeApi {
571
596
  /* Visibility */
572
597
  open(identity, redraw = true) {
573
598
  var _a, _b;
574
- const id = identifyNull(identity);
599
+ const id = this.identifyNull(identity);
575
600
  if (!id)
576
601
  return;
577
602
  if (this.isOpen(id))
@@ -583,7 +608,7 @@ class TreeApi {
583
608
  }
584
609
  close(identity, redraw = true) {
585
610
  var _a, _b;
586
- const id = identifyNull(identity);
611
+ const id = this.identifyNull(identity);
587
612
  if (!id)
588
613
  return;
589
614
  if (!this.isOpen(id))
@@ -594,13 +619,13 @@ class TreeApi {
594
619
  safeRun(this.props.onToggle, id);
595
620
  }
596
621
  toggle(identity) {
597
- const id = identifyNull(identity);
622
+ const id = this.identifyNull(identity);
598
623
  if (!id)
599
624
  return;
600
625
  return this.isOpen(id) ? this.close(id) : this.open(id);
601
626
  }
602
627
  openParents(identity) {
603
- const id = identifyNull(identity);
628
+ const id = this.identifyNull(identity);
604
629
  if (!id)
605
630
  return;
606
631
  const node = utils.dfs(this.root, id);
@@ -648,7 +673,7 @@ class TreeApi {
648
673
  scrollTo(identity, align = "smart") {
649
674
  if (!identity)
650
675
  return;
651
- const id = identify(identity);
676
+ const id = this.identify(identity);
652
677
  this.openParents(id);
653
678
  return utils
654
679
  .waitFor(() => id in this.idToIndex)
@@ -715,7 +740,7 @@ class TreeApi {
715
740
  return !utils.access(data, disabler);
716
741
  }
717
742
  isDragging(node) {
718
- const id = identifyNull(node);
743
+ const id = this.identifyNull(node);
719
744
  if (!id)
720
745
  return false;
721
746
  return this.state.nodes.drag.id === id;
@@ -727,7 +752,7 @@ class TreeApi {
727
752
  return this.matchFn(node);
728
753
  }
729
754
  willReceiveDrop(node) {
730
- const id = identifyNull(node);
755
+ const id = this.identifyNull(node);
731
756
  if (!id)
732
757
  return false;
733
758
  const { destinationParentId, destinationIndex } = this.state.nodes.drag;
@@ -1,4 +1,13 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  Object.defineProperty(exports, "__esModule", { value: true });
3
12
  const redux_1 = require("redux");
4
13
  const root_reducer_1 = require("../state/root-reducer");
@@ -38,6 +47,45 @@ describe("tree.drop() fires onMove (#313)", () => {
38
47
  expect(onMove).toHaveBeenCalledWith(expect.objectContaining({ parentId: "folder", index: 0 }));
39
48
  });
40
49
  });
50
+ describe("custom idAccessor is honored when methods receive raw data (#347)", () => {
51
+ const uuidData = [{ uuid: "a" }, { uuid: "b" }, { uuid: "c" }];
52
+ test("select(data) resolves the id through idAccessor", () => {
53
+ const onSelect = jest.fn();
54
+ const api = setupApi({ data: uuidData, idAccessor: "uuid", onSelect });
55
+ api.select(uuidData[1]);
56
+ expect(api.selectedIds.has("b")).toBe(true);
57
+ expect(api.selectedNodes.map((n) => n.id)).toEqual(["b"]);
58
+ });
59
+ test("focus(data) resolves the id through idAccessor", () => {
60
+ var _a;
61
+ const api = setupApi({ data: uuidData, idAccessor: "uuid" });
62
+ api.focus(uuidData[2]);
63
+ expect((_a = api.focusedNode) === null || _a === void 0 ? void 0 : _a.id).toBe("c");
64
+ });
65
+ test("delete(data) passes the accessor-derived id to onDelete", () => {
66
+ const onDelete = jest.fn();
67
+ const api = setupApi({ data: uuidData, idAccessor: "uuid", onDelete });
68
+ api.delete(uuidData[0]);
69
+ expect(onDelete).toHaveBeenCalledWith(expect.objectContaining({ ids: ["a"] }));
70
+ });
71
+ test("create() focuses the new node by its accessor-derived id", () => __awaiter(void 0, void 0, void 0, function* () {
72
+ // create() passes the raw row data returned by onCreate straight to
73
+ // focus/edit/select; before the fix these read `.id` and lost the node.
74
+ const onCreate = () => ({ uuid: "new" });
75
+ const api = setupApi({ data: uuidData, idAccessor: "uuid", onCreate });
76
+ yield api.create();
77
+ expect(api.state.nodes.focus.id).toBe("new");
78
+ }));
79
+ test("a function idAccessor is honored too", () => {
80
+ const fnData = [{ meta: { key: "x" } }, { meta: { key: "y" } }];
81
+ const api = setupApi({
82
+ data: fnData,
83
+ idAccessor: (d) => d.meta.key,
84
+ });
85
+ api.select(fnData[1]);
86
+ expect(api.selectedIds.has("y")).toBe(true);
87
+ });
88
+ });
41
89
  test("rowHeight defaults to 24", () => {
42
90
  const api = setupApi({});
43
91
  expect(api.rowHeight).toBe(24);
@@ -5,7 +5,7 @@ export type CreateHandler<T> = (args: {
5
5
  parentNode: NodeApi<T> | null;
6
6
  index: number;
7
7
  type: "internal" | "leaf";
8
- }) => (IdObj | null) | Promise<IdObj | null>;
8
+ }) => (T | IdObj | null) | Promise<T | IdObj | null>;
9
9
  export type MoveHandler<T> = (args: {
10
10
  dragIds: string[];
11
11
  dragNodes: NodeApi<T>[];
@@ -149,6 +149,19 @@ export declare class TreeApi<T> {
149
149
  get matchFn(): (node: NodeApi<T>) => boolean;
150
150
  accessChildren(data: T): readonly T[] | null;
151
151
  accessId(data: T): string;
152
+ /**
153
+ * Resolve an identifier to a node id. Public methods accept an id string, a
154
+ * NodeApi, or the raw row data; this is the one place that turns any of those
155
+ * into the string id used internally. Raw data is run through the configured
156
+ * `idAccessor` so a custom accessor (e.g. `uuid`) is honored everywhere, not
157
+ * just where nodes were built. A NodeApi already carries its accessor-derived
158
+ * `id`, so it is used directly rather than re-accessed (the accessor reads the
159
+ * underlying data, which a NodeApi does not expose under that key). Unlike
160
+ * `accessId`, an unresolved id comes back as `undefined` rather than throwing,
161
+ * preserving the previous behavior of the `id`-only lookup.
162
+ */
163
+ identify(identity: string | IdObj | T): string;
164
+ identifyNull(identity: Identity | T): string | null;
152
165
  get firstNode(): NodeApi<T>;
153
166
  get lastNode(): NodeApi<T>;
154
167
  get focusedNode(): NodeApi<T> | null;
@@ -158,7 +171,7 @@ export declare class TreeApi<T> {
158
171
  get(id: string | null): NodeApi<T> | null;
159
172
  at(index: number): NodeApi<T> | null;
160
173
  nodesBetween(startId: string | null, endId: string | null): NodeApi<T>[];
161
- indexOf(id: Identity): number | null;
174
+ indexOf(id: Identity | T): number | null;
162
175
  get editingId(): string | null;
163
176
  createInternal(): Promise<void>;
164
177
  createLeaf(): Promise<void>;
@@ -167,36 +180,36 @@ export declare class TreeApi<T> {
167
180
  parentId?: null | string;
168
181
  index?: null | number;
169
182
  }): Promise<void>;
170
- delete(node: Identity | string[] | IdObj[]): Promise<void>;
171
- edit(node: string | IdObj): Promise<EditResult>;
172
- submit(identity: Identity, value: string): Promise<void>;
183
+ delete(node: Identity | T | (string | IdObj | T)[]): Promise<void>;
184
+ edit(node: string | IdObj | T): Promise<EditResult>;
185
+ submit(identity: Identity | T, value: string): Promise<void>;
173
186
  reset(): void;
174
- activate(id: Identity): void;
187
+ activate(id: Identity | T): void;
175
188
  private resolveEdit;
176
189
  get selectedIds(): Set<string>;
177
190
  get selectedNodes(): NodeApi<T>[];
178
- focus(node: Identity, opts?: {
191
+ focus(node: Identity | T, opts?: {
179
192
  scroll?: boolean;
180
193
  }): void;
181
194
  pageUp(): void;
182
195
  pageDown(): void;
183
- select(node: Identity, opts?: {
196
+ select(node: Identity | T, opts?: {
184
197
  align?: Align;
185
198
  focus?: boolean;
186
199
  }): void;
187
- deselect(node: Identity): void;
188
- selectMulti(identity: Identity, opts?: {
200
+ deselect(node: Identity | T): void;
201
+ selectMulti(identity: Identity | T, opts?: {
189
202
  align?: Align;
190
203
  focus?: boolean;
191
204
  }): void;
192
- selectContiguous(identity: Identity): void;
205
+ selectContiguous(identity: Identity | T): void;
193
206
  deselectAll(): void;
194
207
  selectAll(): void;
195
208
  private filterSelectableNodes;
196
209
  setSelection(args: {
197
- ids: (IdObj | string)[] | null;
198
- anchor: Identity;
199
- mostRecent: Identity;
210
+ ids: (IdObj | string | T)[] | null;
211
+ anchor: Identity | T;
212
+ mostRecent: Identity | T;
200
213
  }): void;
201
214
  get cursorParentId(): string | null;
202
215
  get cursorOverFolder(): boolean;
@@ -208,14 +221,14 @@ export declare class TreeApi<T> {
208
221
  drop(): void;
209
222
  hideCursor(): void;
210
223
  showCursor(cursor: Cursor): void;
211
- open(identity: Identity, redraw?: boolean): void;
212
- close(identity: Identity, redraw?: boolean): void;
213
- toggle(identity: Identity): void;
214
- openParents(identity: Identity): void;
224
+ open(identity: Identity | T, redraw?: boolean): void;
225
+ close(identity: Identity | T, redraw?: boolean): void;
226
+ toggle(identity: Identity | T): void;
227
+ openParents(identity: Identity | T): void;
215
228
  openSiblings(node: NodeApi<T>): void;
216
229
  openAll(): void;
217
230
  closeAll(): void;
218
- scrollTo(identity: Identity, align?: Align): Promise<void> | undefined;
231
+ scrollTo(identity: Identity | T, align?: Align): Promise<void> | undefined;
219
232
  get isEditing(): boolean;
220
233
  get isFiltered(): boolean;
221
234
  get hasFocus(): boolean;
@@ -228,10 +241,10 @@ export declare class TreeApi<T> {
228
241
  isDraggable(data: T): boolean;
229
242
  isSelectable(data: T): boolean;
230
243
  private isActionPossible;
231
- isDragging(node: Identity): boolean;
244
+ isDragging(node: Identity | T): boolean;
232
245
  isFocused(id: string): boolean;
233
246
  isMatch(node: NodeApi<T>): boolean;
234
- willReceiveDrop(node: Identity): boolean;
247
+ willReceiveDrop(node: Identity | T): boolean;
235
248
  onFocus(): void;
236
249
  onBlur(): void;
237
250
  onItemsRendered(args: ListOnItemsRenderedProps): void;
@@ -11,6 +11,7 @@ import * as utils from "../utils";
11
11
  import { DefaultCursor } from "../components/default-cursor";
12
12
  import { DefaultRow } from "../components/default-row";
13
13
  import { DefaultNode } from "../components/default-node";
14
+ import { NodeApi } from "./node-api";
14
15
  import { edit } from "../state/edit-slice";
15
16
  import { focus, treeBlur } from "../state/focus-slice";
16
17
  import { createRoot, ROOT_ID } from "../data/create-root";
@@ -21,7 +22,7 @@ import { DefaultDragPreview } from "../components/default-drag-preview";
21
22
  import { DefaultContainer } from "../components/default-container";
22
23
  import { createList } from "../data/create-list";
23
24
  import { createIndex } from "../data/create-index";
24
- const { safeRun, identify, identifyNull } = utils;
25
+ const { safeRun } = utils;
25
26
  export class TreeApi {
26
27
  constructor(store, props, list, listEl) {
27
28
  this.store = store;
@@ -163,6 +164,30 @@ export class TreeApi {
163
164
  throw new Error("Data must contain an 'id' property or props.idAccessor must return a string");
164
165
  return id;
165
166
  }
167
+ /**
168
+ * Resolve an identifier to a node id. Public methods accept an id string, a
169
+ * NodeApi, or the raw row data; this is the one place that turns any of those
170
+ * into the string id used internally. Raw data is run through the configured
171
+ * `idAccessor` so a custom accessor (e.g. `uuid`) is honored everywhere, not
172
+ * just where nodes were built. A NodeApi already carries its accessor-derived
173
+ * `id`, so it is used directly rather than re-accessed (the accessor reads the
174
+ * underlying data, which a NodeApi does not expose under that key). Unlike
175
+ * `accessId`, an unresolved id comes back as `undefined` rather than throwing,
176
+ * preserving the previous behavior of the `id`-only lookup.
177
+ */
178
+ identify(identity) {
179
+ if (typeof identity === "string")
180
+ return identity;
181
+ if (identity instanceof NodeApi)
182
+ return identity.id;
183
+ const get = this.props.idAccessor || "id";
184
+ return utils.access(identity, get);
185
+ }
186
+ identifyNull(identity) {
187
+ if (identity === null || identity === undefined)
188
+ return null;
189
+ return this.identify(identity);
190
+ }
166
191
  /* Node Access */
167
192
  get firstNode() {
168
193
  var _a;
@@ -218,7 +243,7 @@ export class TreeApi {
218
243
  return this.visibleNodes.slice(start, end + 1);
219
244
  }
220
245
  indexOf(id) {
221
- const key = utils.identifyNull(id);
246
+ const key = this.identifyNull(id);
222
247
  if (!key)
223
248
  return null;
224
249
  return this.idToIndex[key];
@@ -261,7 +286,7 @@ export class TreeApi {
261
286
  if (!node)
262
287
  return;
263
288
  const idents = Array.isArray(node) ? node : [node];
264
- const ids = idents.map(identify);
289
+ const ids = idents.map((i) => this.identify(i));
265
290
  const nodes = ids.map((id) => this.get(id)).filter((n) => !!n);
266
291
  /* Guard against Math.min(...[]) === Infinity when no ids resolve to nodes. */
267
292
  const fromIndex = nodes.length ? Math.min(...nodes.map((n) => { var _a; return (_a = n.rowIndex) !== null && _a !== void 0 ? _a : 0; })) : 0;
@@ -271,7 +296,7 @@ export class TreeApi {
271
296
  }
272
297
  edit(node) {
273
298
  var _a, _b;
274
- const id = identify(node);
299
+ const id = this.identify(node);
275
300
  this.resolveEdit({ cancelled: true });
276
301
  this.scrollTo(id);
277
302
  this.dispatch(edit(id));
@@ -285,7 +310,7 @@ export class TreeApi {
285
310
  var _a, _b;
286
311
  if (!identity)
287
312
  return;
288
- const id = identify(identity);
313
+ const id = this.identify(identity);
289
314
  yield safeRun(this.props.onRename, {
290
315
  id,
291
316
  name: value,
@@ -304,7 +329,7 @@ export class TreeApi {
304
329
  setTimeout(() => this.onFocus()); // Return focus to element;
305
330
  }
306
331
  activate(id) {
307
- const node = this.get(identifyNull(id));
332
+ const node = this.get(this.identifyNull(id));
308
333
  if (!node)
309
334
  return;
310
335
  safeRun(this.props.onActivate, node);
@@ -338,7 +363,7 @@ export class TreeApi {
338
363
  this.select(node);
339
364
  }
340
365
  else {
341
- this.dispatch(focus(identify(node)));
366
+ this.dispatch(focus(this.identify(node)));
342
367
  if (opts.scroll !== false)
343
368
  this.scrollTo(node);
344
369
  if (this.focusedNode)
@@ -378,7 +403,7 @@ export class TreeApi {
378
403
  if (!node)
379
404
  return;
380
405
  const changeFocus = opts.focus !== false;
381
- const id = identify(node);
406
+ const id = this.identify(node);
382
407
  if (changeFocus)
383
408
  this.dispatch(focus(id));
384
409
  if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
@@ -396,12 +421,12 @@ export class TreeApi {
396
421
  deselect(node) {
397
422
  if (!node)
398
423
  return;
399
- const id = identify(node);
424
+ const id = this.identify(node);
400
425
  this.dispatch(selection.remove(id));
401
426
  safeRun(this.props.onSelect, this.selectedNodes);
402
427
  }
403
428
  selectMulti(identity, opts = {}) {
404
- const node = this.get(identifyNull(identity));
429
+ const node = this.get(this.identifyNull(identity));
405
430
  if (!node)
406
431
  return;
407
432
  const changeFocus = opts.focus !== false;
@@ -422,11 +447,11 @@ export class TreeApi {
422
447
  var _a;
423
448
  if (!identity)
424
449
  return;
425
- const id = identify(identity);
450
+ const id = this.identify(identity);
426
451
  this.dispatch(focus(id));
427
452
  if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
428
453
  const { anchor, mostRecent } = this.state.nodes.selection;
429
- const selectableNodes = this.filterSelectableNodes(this.nodesBetween(anchor, identifyNull(id)));
454
+ const selectableNodes = this.filterSelectableNodes(this.nodesBetween(anchor, this.identifyNull(id)));
430
455
  this.dispatch(selection.remove(this.nodesBetween(anchor, mostRecent)));
431
456
  this.dispatch(selection.add(selectableNodes));
432
457
  this.dispatch(selection.mostRecent(id));
@@ -455,14 +480,14 @@ export class TreeApi {
455
480
  }
456
481
  filterSelectableNodes(nodes) {
457
482
  return nodes
458
- .map((n) => this.get(identify(n)))
483
+ .map((n) => this.get(this.identify(n)))
459
484
  .filter((n) => !!n && n.isSelectable);
460
485
  }
461
486
  setSelection(args) {
462
487
  var _a;
463
- const ids = new Set((_a = args.ids) === null || _a === void 0 ? void 0 : _a.map(identify));
464
- const anchor = identifyNull(args.anchor);
465
- const mostRecent = identifyNull(args.mostRecent);
488
+ const ids = new Set((_a = args.ids) === null || _a === void 0 ? void 0 : _a.map((i) => this.identify(i)));
489
+ const anchor = this.identifyNull(args.anchor);
490
+ const mostRecent = this.identifyNull(args.mostRecent);
466
491
  this.dispatch(selection.set({ ids, anchor, mostRecent }));
467
492
  safeRun(this.props.onSelect, this.selectedNodes);
468
493
  }
@@ -545,7 +570,7 @@ export class TreeApi {
545
570
  /* Visibility */
546
571
  open(identity, redraw = true) {
547
572
  var _a, _b;
548
- const id = identifyNull(identity);
573
+ const id = this.identifyNull(identity);
549
574
  if (!id)
550
575
  return;
551
576
  if (this.isOpen(id))
@@ -557,7 +582,7 @@ export class TreeApi {
557
582
  }
558
583
  close(identity, redraw = true) {
559
584
  var _a, _b;
560
- const id = identifyNull(identity);
585
+ const id = this.identifyNull(identity);
561
586
  if (!id)
562
587
  return;
563
588
  if (!this.isOpen(id))
@@ -568,13 +593,13 @@ export class TreeApi {
568
593
  safeRun(this.props.onToggle, id);
569
594
  }
570
595
  toggle(identity) {
571
- const id = identifyNull(identity);
596
+ const id = this.identifyNull(identity);
572
597
  if (!id)
573
598
  return;
574
599
  return this.isOpen(id) ? this.close(id) : this.open(id);
575
600
  }
576
601
  openParents(identity) {
577
- const id = identifyNull(identity);
602
+ const id = this.identifyNull(identity);
578
603
  if (!id)
579
604
  return;
580
605
  const node = utils.dfs(this.root, id);
@@ -622,7 +647,7 @@ export class TreeApi {
622
647
  scrollTo(identity, align = "smart") {
623
648
  if (!identity)
624
649
  return;
625
- const id = identify(identity);
650
+ const id = this.identify(identity);
626
651
  this.openParents(id);
627
652
  return utils
628
653
  .waitFor(() => id in this.idToIndex)
@@ -689,7 +714,7 @@ export class TreeApi {
689
714
  return !utils.access(data, disabler);
690
715
  }
691
716
  isDragging(node) {
692
- const id = identifyNull(node);
717
+ const id = this.identifyNull(node);
693
718
  if (!id)
694
719
  return false;
695
720
  return this.state.nodes.drag.id === id;
@@ -701,7 +726,7 @@ export class TreeApi {
701
726
  return this.matchFn(node);
702
727
  }
703
728
  willReceiveDrop(node) {
704
- const id = identifyNull(node);
729
+ const id = this.identifyNull(node);
705
730
  if (!id)
706
731
  return false;
707
732
  const { destinationParentId, destinationIndex } = this.state.nodes.drag;
@@ -1,3 +1,12 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
1
10
  import { createStore } from "redux";
2
11
  import { rootReducer } from "../state/root-reducer";
3
12
  import { actions as dnd } from "../state/dnd-slice";
@@ -36,6 +45,45 @@ describe("tree.drop() fires onMove (#313)", () => {
36
45
  expect(onMove).toHaveBeenCalledWith(expect.objectContaining({ parentId: "folder", index: 0 }));
37
46
  });
38
47
  });
48
+ describe("custom idAccessor is honored when methods receive raw data (#347)", () => {
49
+ const uuidData = [{ uuid: "a" }, { uuid: "b" }, { uuid: "c" }];
50
+ test("select(data) resolves the id through idAccessor", () => {
51
+ const onSelect = jest.fn();
52
+ const api = setupApi({ data: uuidData, idAccessor: "uuid", onSelect });
53
+ api.select(uuidData[1]);
54
+ expect(api.selectedIds.has("b")).toBe(true);
55
+ expect(api.selectedNodes.map((n) => n.id)).toEqual(["b"]);
56
+ });
57
+ test("focus(data) resolves the id through idAccessor", () => {
58
+ var _a;
59
+ const api = setupApi({ data: uuidData, idAccessor: "uuid" });
60
+ api.focus(uuidData[2]);
61
+ expect((_a = api.focusedNode) === null || _a === void 0 ? void 0 : _a.id).toBe("c");
62
+ });
63
+ test("delete(data) passes the accessor-derived id to onDelete", () => {
64
+ const onDelete = jest.fn();
65
+ const api = setupApi({ data: uuidData, idAccessor: "uuid", onDelete });
66
+ api.delete(uuidData[0]);
67
+ expect(onDelete).toHaveBeenCalledWith(expect.objectContaining({ ids: ["a"] }));
68
+ });
69
+ test("create() focuses the new node by its accessor-derived id", () => __awaiter(void 0, void 0, void 0, function* () {
70
+ // create() passes the raw row data returned by onCreate straight to
71
+ // focus/edit/select; before the fix these read `.id` and lost the node.
72
+ const onCreate = () => ({ uuid: "new" });
73
+ const api = setupApi({ data: uuidData, idAccessor: "uuid", onCreate });
74
+ yield api.create();
75
+ expect(api.state.nodes.focus.id).toBe("new");
76
+ }));
77
+ test("a function idAccessor is honored too", () => {
78
+ const fnData = [{ meta: { key: "x" } }, { meta: { key: "y" } }];
79
+ const api = setupApi({
80
+ data: fnData,
81
+ idAccessor: (d) => d.meta.key,
82
+ });
83
+ api.select(fnData[1]);
84
+ expect(api.selectedIds.has("y")).toBe(true);
85
+ });
86
+ });
39
87
  test("rowHeight defaults to 24", () => {
40
88
  const api = setupApi({});
41
89
  expect(api.rowHeight).toBe(24);
@@ -5,7 +5,7 @@ export type CreateHandler<T> = (args: {
5
5
  parentNode: NodeApi<T> | null;
6
6
  index: number;
7
7
  type: "internal" | "leaf";
8
- }) => (IdObj | null) | Promise<IdObj | null>;
8
+ }) => (T | IdObj | null) | Promise<T | IdObj | null>;
9
9
  export type MoveHandler<T> = (args: {
10
10
  dragIds: string[];
11
11
  dragNodes: NodeApi<T>[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-arborist",
3
- "version": "3.10.4",
3
+ "version": "3.10.5",
4
4
  "keywords": [
5
5
  "arborist",
6
6
  "dnd",
@@ -47,6 +47,52 @@ describe("tree.drop() fires onMove (#313)", () => {
47
47
  });
48
48
  });
49
49
 
50
+ describe("custom idAccessor is honored when methods receive raw data (#347)", () => {
51
+ const uuidData = [{ uuid: "a" }, { uuid: "b" }, { uuid: "c" }];
52
+
53
+ test("select(data) resolves the id through idAccessor", () => {
54
+ const onSelect = jest.fn();
55
+ const api = setupApi({ data: uuidData, idAccessor: "uuid", onSelect });
56
+ api.select(uuidData[1]);
57
+ expect(api.selectedIds.has("b")).toBe(true);
58
+ expect(api.selectedNodes.map((n) => n.id)).toEqual(["b"]);
59
+ });
60
+
61
+ test("focus(data) resolves the id through idAccessor", () => {
62
+ const api = setupApi({ data: uuidData, idAccessor: "uuid" });
63
+ api.focus(uuidData[2]);
64
+ expect(api.focusedNode?.id).toBe("c");
65
+ });
66
+
67
+ test("delete(data) passes the accessor-derived id to onDelete", () => {
68
+ const onDelete = jest.fn();
69
+ const api = setupApi({ data: uuidData, idAccessor: "uuid", onDelete });
70
+ api.delete(uuidData[0]);
71
+ expect(onDelete).toHaveBeenCalledWith(
72
+ expect.objectContaining({ ids: ["a"] }),
73
+ );
74
+ });
75
+
76
+ test("create() focuses the new node by its accessor-derived id", async () => {
77
+ // create() passes the raw row data returned by onCreate straight to
78
+ // focus/edit/select; before the fix these read `.id` and lost the node.
79
+ const onCreate = () => ({ uuid: "new" });
80
+ const api = setupApi({ data: uuidData, idAccessor: "uuid", onCreate });
81
+ await api.create();
82
+ expect(api.state.nodes.focus.id).toBe("new");
83
+ });
84
+
85
+ test("a function idAccessor is honored too", () => {
86
+ const fnData = [{ meta: { key: "x" } }, { meta: { key: "y" } }];
87
+ const api = setupApi({
88
+ data: fnData,
89
+ idAccessor: (d: any) => d.meta.key,
90
+ });
91
+ api.select(fnData[1]);
92
+ expect(api.selectedIds.has("y")).toBe(true);
93
+ });
94
+ });
95
+
50
96
  test("rowHeight defaults to 24", () => {
51
97
  const api = setupApi({});
52
98
  expect(api.rowHeight).toBe(24);
@@ -22,7 +22,7 @@ import { Store } from "redux";
22
22
  import { createList } from "../data/create-list";
23
23
  import { createIndex } from "../data/create-index";
24
24
 
25
- const { safeRun, identify, identifyNull } = utils;
25
+ const { safeRun } = utils;
26
26
  export class TreeApi<T> {
27
27
  static editPromise: null | ((args: EditResult) => void);
28
28
  root: NodeApi<T>;
@@ -187,6 +187,29 @@ export class TreeApi<T> {
187
187
  return id;
188
188
  }
189
189
 
190
+ /**
191
+ * Resolve an identifier to a node id. Public methods accept an id string, a
192
+ * NodeApi, or the raw row data; this is the one place that turns any of those
193
+ * into the string id used internally. Raw data is run through the configured
194
+ * `idAccessor` so a custom accessor (e.g. `uuid`) is honored everywhere, not
195
+ * just where nodes were built. A NodeApi already carries its accessor-derived
196
+ * `id`, so it is used directly rather than re-accessed (the accessor reads the
197
+ * underlying data, which a NodeApi does not expose under that key). Unlike
198
+ * `accessId`, an unresolved id comes back as `undefined` rather than throwing,
199
+ * preserving the previous behavior of the `id`-only lookup.
200
+ */
201
+ identify(identity: string | IdObj | T): string {
202
+ if (typeof identity === "string") return identity;
203
+ if (identity instanceof NodeApi) return identity.id;
204
+ const get = this.props.idAccessor || "id";
205
+ return utils.access<string>(identity, get);
206
+ }
207
+
208
+ identifyNull(identity: Identity | T): string | null {
209
+ if (identity === null || identity === undefined) return null;
210
+ return this.identify(identity);
211
+ }
212
+
190
213
  /* Node Access */
191
214
 
192
215
  get firstNode() {
@@ -237,8 +260,8 @@ export class TreeApi<T> {
237
260
  return this.visibleNodes.slice(start, end + 1);
238
261
  }
239
262
 
240
- indexOf(id: Identity) {
241
- const key = utils.identifyNull(id);
263
+ indexOf(id: Identity | T) {
264
+ const key = this.identifyNull(id);
242
265
  if (!key) return null;
243
266
  return this.idToIndex[key];
244
267
  }
@@ -284,10 +307,10 @@ export class TreeApi<T> {
284
307
  }
285
308
  }
286
309
 
287
- async delete(node: Identity | string[] | IdObj[]) {
310
+ async delete(node: Identity | T | (string | IdObj | T)[]) {
288
311
  if (!node) return;
289
312
  const idents = Array.isArray(node) ? node : [node];
290
- const ids = idents.map(identify);
313
+ const ids = idents.map((i) => this.identify(i));
291
314
  const nodes = ids.map((id) => this.get(id)!).filter((n) => !!n);
292
315
  /* Guard against Math.min(...[]) === Infinity when no ids resolve to nodes. */
293
316
  const fromIndex = nodes.length ? Math.min(...nodes.map((n) => n.rowIndex ?? 0)) : 0;
@@ -295,8 +318,8 @@ export class TreeApi<T> {
295
318
  this.redrawList(fromIndex);
296
319
  }
297
320
 
298
- edit(node: string | IdObj): Promise<EditResult> {
299
- const id = identify(node);
321
+ edit(node: string | IdObj | T): Promise<EditResult> {
322
+ const id = this.identify(node);
300
323
  this.resolveEdit({ cancelled: true });
301
324
  this.scrollTo(id);
302
325
  this.dispatch(edit(id));
@@ -306,9 +329,9 @@ export class TreeApi<T> {
306
329
  });
307
330
  }
308
331
 
309
- async submit(identity: Identity, value: string) {
332
+ async submit(identity: Identity | T, value: string) {
310
333
  if (!identity) return;
311
- const id = identify(identity);
334
+ const id = this.identify(identity);
312
335
  await safeRun(this.props.onRename, {
313
336
  id,
314
337
  name: value,
@@ -327,8 +350,8 @@ export class TreeApi<T> {
327
350
  setTimeout(() => this.onFocus()); // Return focus to element;
328
351
  }
329
352
 
330
- activate(id: Identity) {
331
- const node = this.get(identifyNull(id));
353
+ activate(id: Identity | T) {
354
+ const node = this.get(this.identifyNull(id));
332
355
  if (!node) return;
333
356
  safeRun(this.props.onActivate, node);
334
357
  }
@@ -354,7 +377,7 @@ export class TreeApi<T> {
354
377
  return nodes;
355
378
  }
356
379
 
357
- focus(node: Identity, opts: { scroll?: boolean } = {}) {
380
+ focus(node: Identity | T, opts: { scroll?: boolean } = {}) {
358
381
  if (!node) return;
359
382
  /* Focus is responsible for scrolling, while selection is
360
383
  * responsible for focus. If selectionFollowsFocus, then
@@ -362,7 +385,7 @@ export class TreeApi<T> {
362
385
  if (this.props.selectionFollowsFocus) {
363
386
  this.select(node);
364
387
  } else {
365
- this.dispatch(focus(identify(node)));
388
+ this.dispatch(focus(this.identify(node)));
366
389
  if (opts.scroll !== false) this.scrollTo(node);
367
390
  if (this.focusedNode) safeRun(this.props.onFocus, this.focusedNode);
368
391
  }
@@ -394,10 +417,10 @@ export class TreeApi<T> {
394
417
  this.focus(this.at(index));
395
418
  }
396
419
 
397
- select(node: Identity, opts: { align?: Align; focus?: boolean } = {}) {
420
+ select(node: Identity | T, opts: { align?: Align; focus?: boolean } = {}) {
398
421
  if (!node) return;
399
422
  const changeFocus = opts.focus !== false;
400
- const id = identify(node);
423
+ const id = this.identify(node);
401
424
  if (changeFocus) this.dispatch(focus(id));
402
425
  if (this.get(id)?.isSelectable) {
403
426
  this.setSelection({
@@ -412,15 +435,15 @@ export class TreeApi<T> {
412
435
  }
413
436
  }
414
437
 
415
- deselect(node: Identity) {
438
+ deselect(node: Identity | T) {
416
439
  if (!node) return;
417
- const id = identify(node);
440
+ const id = this.identify(node);
418
441
  this.dispatch(selection.remove(id));
419
442
  safeRun(this.props.onSelect, this.selectedNodes);
420
443
  }
421
444
 
422
- selectMulti(identity: Identity, opts: { align?: Align; focus?: boolean } = {}) {
423
- const node = this.get(identifyNull(identity));
445
+ selectMulti(identity: Identity | T, opts: { align?: Align; focus?: boolean } = {}) {
446
+ const node = this.get(this.identifyNull(identity));
424
447
  if (!node) return;
425
448
  const changeFocus = opts.focus !== false;
426
449
  if (changeFocus) this.dispatch(focus(node.id));
@@ -436,14 +459,14 @@ export class TreeApi<T> {
436
459
  safeRun(this.props.onSelect, this.selectedNodes);
437
460
  }
438
461
 
439
- selectContiguous(identity: Identity) {
462
+ selectContiguous(identity: Identity | T) {
440
463
  if (!identity) return;
441
- const id = identify(identity);
464
+ const id = this.identify(identity);
442
465
  this.dispatch(focus(id));
443
466
  if (this.get(id)?.isSelectable) {
444
467
  const { anchor, mostRecent } = this.state.nodes.selection;
445
468
  const selectableNodes = this.filterSelectableNodes(
446
- this.nodesBetween(anchor, identifyNull(id)),
469
+ this.nodesBetween(anchor, this.identifyNull(id)),
447
470
  );
448
471
  this.dispatch(selection.remove(this.nodesBetween(anchor, mostRecent)));
449
472
  this.dispatch(selection.add(selectableNodes));
@@ -473,14 +496,18 @@ export class TreeApi<T> {
473
496
 
474
497
  private filterSelectableNodes(nodes: (IdObj | string)[]) {
475
498
  return nodes
476
- .map((n) => this.get(identify(n)))
499
+ .map((n) => this.get(this.identify(n)))
477
500
  .filter((n): n is NodeApi<T> => !!n && n.isSelectable);
478
501
  }
479
502
 
480
- setSelection(args: { ids: (IdObj | string)[] | null; anchor: Identity; mostRecent: Identity }) {
481
- const ids = new Set(args.ids?.map(identify));
482
- const anchor = identifyNull(args.anchor);
483
- const mostRecent = identifyNull(args.mostRecent);
503
+ setSelection(args: {
504
+ ids: (IdObj | string | T)[] | null;
505
+ anchor: Identity | T;
506
+ mostRecent: Identity | T;
507
+ }) {
508
+ const ids = new Set(args.ids?.map((i) => this.identify(i)));
509
+ const anchor = this.identifyNull(args.anchor);
510
+ const mostRecent = this.identifyNull(args.mostRecent);
484
511
  this.dispatch(selection.set({ ids, anchor, mostRecent }));
485
512
  safeRun(this.props.onSelect, this.selectedNodes);
486
513
  }
@@ -568,8 +595,8 @@ export class TreeApi<T> {
568
595
 
569
596
  /* Visibility */
570
597
 
571
- open(identity: Identity, redraw: boolean = true) {
572
- const id = identifyNull(identity);
598
+ open(identity: Identity | T, redraw: boolean = true) {
599
+ const id = this.identifyNull(identity);
573
600
  if (!id) return;
574
601
  if (this.isOpen(id)) return;
575
602
  this.dispatch(visibility.open(id, this.isFiltered));
@@ -577,8 +604,8 @@ export class TreeApi<T> {
577
604
  safeRun(this.props.onToggle, id);
578
605
  }
579
606
 
580
- close(identity: Identity, redraw: boolean = true) {
581
- const id = identifyNull(identity);
607
+ close(identity: Identity | T, redraw: boolean = true) {
608
+ const id = this.identifyNull(identity);
582
609
  if (!id) return;
583
610
  if (!this.isOpen(id)) return;
584
611
  this.dispatch(visibility.close(id, this.isFiltered));
@@ -586,14 +613,14 @@ export class TreeApi<T> {
586
613
  safeRun(this.props.onToggle, id);
587
614
  }
588
615
 
589
- toggle(identity: Identity) {
590
- const id = identifyNull(identity);
616
+ toggle(identity: Identity | T) {
617
+ const id = this.identifyNull(identity);
591
618
  if (!id) return;
592
619
  return this.isOpen(id) ? this.close(id) : this.open(id);
593
620
  }
594
621
 
595
- openParents(identity: Identity) {
596
- const id = identifyNull(identity);
622
+ openParents(identity: Identity | T) {
623
+ const id = this.identifyNull(identity);
597
624
  if (!id) return;
598
625
  const node = utils.dfs(this.root, id);
599
626
  let parent = node?.parent;
@@ -638,9 +665,9 @@ export class TreeApi<T> {
638
665
 
639
666
  /* Scrolling */
640
667
 
641
- scrollTo(identity: Identity, align: Align = "smart") {
668
+ scrollTo(identity: Identity | T, align: Align = "smart") {
642
669
  if (!identity) return;
643
- const id = identify(identity);
670
+ const id = this.identify(identity);
644
671
  this.openParents(id);
645
672
  return utils
646
673
  .waitFor(() => id in this.idToIndex)
@@ -712,8 +739,8 @@ export class TreeApi<T> {
712
739
  return !utils.access(data, disabler);
713
740
  }
714
741
 
715
- isDragging(node: Identity) {
716
- const id = identifyNull(node);
742
+ isDragging(node: Identity | T) {
743
+ const id = this.identifyNull(node);
717
744
  if (!id) return false;
718
745
  return this.state.nodes.drag.id === id;
719
746
  }
@@ -726,8 +753,8 @@ export class TreeApi<T> {
726
753
  return this.matchFn(node);
727
754
  }
728
755
 
729
- willReceiveDrop(node: Identity) {
730
- const id = identifyNull(node);
756
+ willReceiveDrop(node: Identity | T) {
757
+ const id = this.identifyNull(node);
731
758
  if (!id) return false;
732
759
  const { destinationParentId, destinationIndex } = this.state.nodes.drag;
733
760
  return id === destinationParentId && destinationIndex === null;
@@ -1,12 +1,14 @@
1
1
  import { NodeApi } from "../interfaces/node-api";
2
2
  import { IdObj } from "./utils";
3
3
 
4
+ // Returns the newly created row data, whose id is read via idAccessor. `IdObj`
5
+ // is kept for back-compat with handlers that return a bare `{ id }` (#347).
4
6
  export type CreateHandler<T> = (args: {
5
7
  parentId: string | null;
6
8
  parentNode: NodeApi<T> | null;
7
9
  index: number;
8
10
  type: "internal" | "leaf";
9
- }) => (IdObj | null) | Promise<IdObj | null>;
11
+ }) => (T | IdObj | null) | Promise<T | IdObj | null>;
10
12
 
11
13
  export type MoveHandler<T> = (args: {
12
14
  dragIds: string[];