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.
- package/dist/main/interfaces/tree-api.d.ts +39 -20
- package/dist/main/interfaces/tree-api.js +75 -23
- package/dist/main/interfaces/tree-api.test.js +100 -0
- package/dist/main/types/handlers.d.ts +1 -1
- package/dist/module/interfaces/tree-api.d.ts +39 -20
- package/dist/module/interfaces/tree-api.js +75 -23
- package/dist/module/interfaces/tree-api.test.js +100 -0
- package/dist/module/types/handlers.d.ts +1 -1
- package/package.json +1 -1
- package/src/interfaces/tree-api.test.ts +106 -0
- package/src/interfaces/tree-api.ts +94 -41
- package/src/types/handlers.ts +3 -1
|
@@ -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
|
|
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,20 @@ 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;
|
|
232
|
+
/**
|
|
233
|
+
* Horizontally scroll the list so the node's indented content is in view.
|
|
234
|
+
* A no-op when the list doesn't overflow horizontally (the common case), so
|
|
235
|
+
* it never disturbs scrolling for trees that fit their width.
|
|
236
|
+
*/
|
|
237
|
+
private scrollToNodeHorizontally;
|
|
219
238
|
get isEditing(): boolean;
|
|
220
239
|
get isFiltered(): boolean;
|
|
221
240
|
get hasFocus(): boolean;
|
|
@@ -228,10 +247,10 @@ export declare class TreeApi<T> {
|
|
|
228
247
|
isDraggable(data: T): boolean;
|
|
229
248
|
isSelectable(data: T): boolean;
|
|
230
249
|
private isActionPossible;
|
|
231
|
-
isDragging(node: Identity): boolean;
|
|
250
|
+
isDragging(node: Identity | T): boolean;
|
|
232
251
|
isFocused(id: string): boolean;
|
|
233
252
|
isMatch(node: NodeApi<T>): boolean;
|
|
234
|
-
willReceiveDrop(node: Identity): boolean;
|
|
253
|
+
willReceiveDrop(node: Identity | T): boolean;
|
|
235
254
|
onFocus(): void;
|
|
236
255
|
onBlur(): void;
|
|
237
256
|
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
|
|
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 =
|
|
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)
|
|
@@ -658,11 +683,38 @@ class TreeApi {
|
|
|
658
683
|
if (index === undefined)
|
|
659
684
|
return;
|
|
660
685
|
(_a = this.list.current) === null || _a === void 0 ? void 0 : _a.scrollToItem(index, align);
|
|
686
|
+
/* react-window only scrolls vertically. A deeply nested node is
|
|
687
|
+
indented by level * indent and can sit past the right edge when rows
|
|
688
|
+
overflow horizontally, so bring it into view ourselves (#220). */
|
|
689
|
+
this.scrollToNodeHorizontally(this.get(id));
|
|
661
690
|
})
|
|
662
691
|
.catch(() => {
|
|
663
692
|
// Id: ${id} never appeared in the list.
|
|
664
693
|
});
|
|
665
694
|
}
|
|
695
|
+
/**
|
|
696
|
+
* Horizontally scroll the list so the node's indented content is in view.
|
|
697
|
+
* A no-op when the list doesn't overflow horizontally (the common case), so
|
|
698
|
+
* it never disturbs scrolling for trees that fit their width.
|
|
699
|
+
*/
|
|
700
|
+
scrollToNodeHorizontally(node) {
|
|
701
|
+
const el = this.listEl.current;
|
|
702
|
+
if (!node || !el)
|
|
703
|
+
return;
|
|
704
|
+
const maxScroll = el.scrollWidth - el.clientWidth;
|
|
705
|
+
if (maxScroll <= 0)
|
|
706
|
+
return; // nothing to scroll
|
|
707
|
+
const left = node.level * this.indent;
|
|
708
|
+
const viewLeft = el.scrollLeft;
|
|
709
|
+
const viewRight = el.scrollLeft + el.clientWidth;
|
|
710
|
+
/* The visible range is half-open [viewLeft, viewRight): a pixel at viewRight
|
|
711
|
+
is already clipped. Only move when the node's indentation falls outside
|
|
712
|
+
it, aligning its content start to the left edge so the label is revealed,
|
|
713
|
+
clamped to the list's scrollable range. */
|
|
714
|
+
if (left < viewLeft || left >= viewRight) {
|
|
715
|
+
el.scrollLeft = Math.max(0, Math.min(left, maxScroll));
|
|
716
|
+
}
|
|
717
|
+
}
|
|
666
718
|
/* State Checks */
|
|
667
719
|
get isEditing() {
|
|
668
720
|
return this.state.nodes.edit.id !== null;
|
|
@@ -715,7 +767,7 @@ class TreeApi {
|
|
|
715
767
|
return !utils.access(data, disabler);
|
|
716
768
|
}
|
|
717
769
|
isDragging(node) {
|
|
718
|
-
const id = identifyNull(node);
|
|
770
|
+
const id = this.identifyNull(node);
|
|
719
771
|
if (!id)
|
|
720
772
|
return false;
|
|
721
773
|
return this.state.nodes.drag.id === id;
|
|
@@ -727,7 +779,7 @@ class TreeApi {
|
|
|
727
779
|
return this.matchFn(node);
|
|
728
780
|
}
|
|
729
781
|
willReceiveDrop(node) {
|
|
730
|
-
const id = identifyNull(node);
|
|
782
|
+
const id = this.identifyNull(node);
|
|
731
783
|
if (!id)
|
|
732
784
|
return false;
|
|
733
785
|
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);
|
|
@@ -119,3 +167,55 @@ describe("onSelect fires exactly once per selection method (#332)", () => {
|
|
|
119
167
|
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
120
168
|
});
|
|
121
169
|
});
|
|
170
|
+
describe("scrollTo brings a deeply nested node into view horizontally (#220)", () => {
|
|
171
|
+
// A folder tree where "deep" sits at level 2 (indented 2 * 24 = 48px).
|
|
172
|
+
const nestedData = [{ id: "root", children: [{ id: "mid", children: [{ id: "deep" }] }] }];
|
|
173
|
+
// react-window's scrollToItem only scrolls vertically; the horizontal scroll
|
|
174
|
+
// happens on the outer list element, which we stub here.
|
|
175
|
+
function setupWithListEl(el) {
|
|
176
|
+
const store = (0, redux_1.createStore)(root_reducer_1.rootReducer);
|
|
177
|
+
const list = { current: { scrollToItem: jest.fn() } };
|
|
178
|
+
const listEl = { current: el };
|
|
179
|
+
return new tree_api_1.TreeApi(store, { data: nestedData }, list, listEl);
|
|
180
|
+
}
|
|
181
|
+
test("scrolls right when the node is past the right edge", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
182
|
+
const el = { scrollWidth: 500, clientWidth: 40, scrollLeft: 0 };
|
|
183
|
+
const api = setupWithListEl(el);
|
|
184
|
+
yield api.scrollTo("deep");
|
|
185
|
+
// Aligns the node's indentation start (level 2 * indent 24) to the left edge.
|
|
186
|
+
expect(el.scrollLeft).toBe(48);
|
|
187
|
+
}));
|
|
188
|
+
test("scrolls left when the node is past the left edge", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
189
|
+
const el = { scrollWidth: 500, clientWidth: 100, scrollLeft: 200 };
|
|
190
|
+
const api = setupWithListEl(el);
|
|
191
|
+
yield api.scrollTo("deep");
|
|
192
|
+
expect(el.scrollLeft).toBe(48);
|
|
193
|
+
}));
|
|
194
|
+
test("scrolls when the node's start sits exactly on the right edge", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
195
|
+
// viewRight === left (48): the visible range is half-open, so the content
|
|
196
|
+
// start is already clipped and must be scrolled into view.
|
|
197
|
+
const el = { scrollWidth: 500, clientWidth: 48, scrollLeft: 0 };
|
|
198
|
+
const api = setupWithListEl(el);
|
|
199
|
+
yield api.scrollTo("deep");
|
|
200
|
+
expect(el.scrollLeft).toBe(48);
|
|
201
|
+
}));
|
|
202
|
+
test("clamps the target to the maximum scrollable distance", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
203
|
+
// left (48) exceeds maxScroll (60 - 40 = 20), so scrollLeft is clamped.
|
|
204
|
+
const el = { scrollWidth: 60, clientWidth: 40, scrollLeft: 0 };
|
|
205
|
+
const api = setupWithListEl(el);
|
|
206
|
+
yield api.scrollTo("deep");
|
|
207
|
+
expect(el.scrollLeft).toBe(20);
|
|
208
|
+
}));
|
|
209
|
+
test("leaves scroll untouched when the node is already in view", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
210
|
+
const el = { scrollWidth: 500, clientWidth: 200, scrollLeft: 0 };
|
|
211
|
+
const api = setupWithListEl(el);
|
|
212
|
+
yield api.scrollTo("deep");
|
|
213
|
+
expect(el.scrollLeft).toBe(0);
|
|
214
|
+
}));
|
|
215
|
+
test("no-ops when the list does not overflow horizontally", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
216
|
+
const el = { scrollWidth: 100, clientWidth: 100, scrollLeft: 0 };
|
|
217
|
+
const api = setupWithListEl(el);
|
|
218
|
+
yield api.scrollTo("deep");
|
|
219
|
+
expect(el.scrollLeft).toBe(0);
|
|
220
|
+
}));
|
|
221
|
+
});
|
|
@@ -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
|
|
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,20 @@ 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;
|
|
232
|
+
/**
|
|
233
|
+
* Horizontally scroll the list so the node's indented content is in view.
|
|
234
|
+
* A no-op when the list doesn't overflow horizontally (the common case), so
|
|
235
|
+
* it never disturbs scrolling for trees that fit their width.
|
|
236
|
+
*/
|
|
237
|
+
private scrollToNodeHorizontally;
|
|
219
238
|
get isEditing(): boolean;
|
|
220
239
|
get isFiltered(): boolean;
|
|
221
240
|
get hasFocus(): boolean;
|
|
@@ -228,10 +247,10 @@ export declare class TreeApi<T> {
|
|
|
228
247
|
isDraggable(data: T): boolean;
|
|
229
248
|
isSelectable(data: T): boolean;
|
|
230
249
|
private isActionPossible;
|
|
231
|
-
isDragging(node: Identity): boolean;
|
|
250
|
+
isDragging(node: Identity | T): boolean;
|
|
232
251
|
isFocused(id: string): boolean;
|
|
233
252
|
isMatch(node: NodeApi<T>): boolean;
|
|
234
|
-
willReceiveDrop(node: Identity): boolean;
|
|
253
|
+
willReceiveDrop(node: Identity | T): boolean;
|
|
235
254
|
onFocus(): void;
|
|
236
255
|
onBlur(): void;
|
|
237
256
|
onItemsRendered(args: ListOnItemsRenderedProps): void;
|