react-arborist 3.10.4 → 3.10.6

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.
@@ -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)
@@ -648,12 +675,38 @@ export class TreeApi<T> {
648
675
  const index = this.idToIndex[id];
649
676
  if (index === undefined) return;
650
677
  this.list.current?.scrollToItem(index, align);
678
+ /* react-window only scrolls vertically. A deeply nested node is
679
+ indented by level * indent and can sit past the right edge when rows
680
+ overflow horizontally, so bring it into view ourselves (#220). */
681
+ this.scrollToNodeHorizontally(this.get(id));
651
682
  })
652
683
  .catch(() => {
653
684
  // Id: ${id} never appeared in the list.
654
685
  });
655
686
  }
656
687
 
688
+ /**
689
+ * Horizontally scroll the list so the node's indented content is in view.
690
+ * A no-op when the list doesn't overflow horizontally (the common case), so
691
+ * it never disturbs scrolling for trees that fit their width.
692
+ */
693
+ private scrollToNodeHorizontally(node: NodeApi<T> | null) {
694
+ const el = this.listEl.current;
695
+ if (!node || !el) return;
696
+ const maxScroll = el.scrollWidth - el.clientWidth;
697
+ if (maxScroll <= 0) return; // nothing to scroll
698
+ const left = node.level * this.indent;
699
+ const viewLeft = el.scrollLeft;
700
+ const viewRight = el.scrollLeft + el.clientWidth;
701
+ /* The visible range is half-open [viewLeft, viewRight): a pixel at viewRight
702
+ is already clipped. Only move when the node's indentation falls outside
703
+ it, aligning its content start to the left edge so the label is revealed,
704
+ clamped to the list's scrollable range. */
705
+ if (left < viewLeft || left >= viewRight) {
706
+ el.scrollLeft = Math.max(0, Math.min(left, maxScroll));
707
+ }
708
+ }
709
+
657
710
  /* State Checks */
658
711
 
659
712
  get isEditing() {
@@ -712,8 +765,8 @@ export class TreeApi<T> {
712
765
  return !utils.access(data, disabler);
713
766
  }
714
767
 
715
- isDragging(node: Identity) {
716
- const id = identifyNull(node);
768
+ isDragging(node: Identity | T) {
769
+ const id = this.identifyNull(node);
717
770
  if (!id) return false;
718
771
  return this.state.nodes.drag.id === id;
719
772
  }
@@ -726,8 +779,8 @@ export class TreeApi<T> {
726
779
  return this.matchFn(node);
727
780
  }
728
781
 
729
- willReceiveDrop(node: Identity) {
730
- const id = identifyNull(node);
782
+ willReceiveDrop(node: Identity | T) {
783
+ const id = this.identifyNull(node);
731
784
  if (!id) return false;
732
785
  const { destinationParentId, destinationIndex } = this.state.nodes.drag;
733
786
  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[];