@nanahoshi/mona-ui 0.23.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/nanahoshi-mona-ui-dropdown-list.mjs +1 -1
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-footer-row.component--nF_Q7Bd.mjs → nanahoshi-mona-ui-grid-grid-footer-row.component-CIMXz60d.mjs} +2 -2
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-list.component-S2qxwP2M.mjs → nanahoshi-mona-ui-grid-grid-list.component-CpeheGXU.mjs} +4 -4
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-row.component-Dwy8hCm7.mjs → nanahoshi-mona-ui-grid-grid-row.component-B_5ay_Nz.mjs} +2 -2
- package/fesm2022/{nanahoshi-mona-ui-grid-grid-virtual-list.component-DArxdAah.mjs → nanahoshi-mona-ui-grid-grid-virtual-list.component-BzKywU5b.mjs} +4 -4
- package/fesm2022/{nanahoshi-mona-ui-grid-nanahoshi-mona-ui-grid-bduDEm-x.mjs → nanahoshi-mona-ui-grid-nanahoshi-mona-ui-grid-IewqeT4m.mjs} +8 -8
- package/fesm2022/nanahoshi-mona-ui-grid.mjs +1 -1
- package/fesm2022/nanahoshi-mona-ui-internal-filter-input.mjs +47 -7
- package/fesm2022/nanahoshi-mona-ui-internal-list.mjs +386 -180
- package/fesm2022/nanahoshi-mona-ui-internal-tree.mjs +266 -99
- package/fesm2022/nanahoshi-mona-ui-internal.mjs +35 -32
- package/fesm2022/nanahoshi-mona-ui-locales.mjs +13 -0
- package/fesm2022/nanahoshi-mona-ui-popup-menu.mjs +1 -1
- package/fesm2022/nanahoshi-mona-ui-tree-view.mjs +1 -1
- package/package.json +1 -1
- package/types/nanahoshi-mona-ui-i18n.d.ts +1 -0
- package/types/nanahoshi-mona-ui-internal-filter-input.d.ts +35 -1
- package/types/nanahoshi-mona-ui-internal-list.d.ts +82 -16
- package/types/nanahoshi-mona-ui-internal-tree.d.ts +50 -3
- package/types/nanahoshi-mona-ui-internal.d.ts +18 -4
- package/types/nanahoshi-mona-ui-locales.d.ts +13 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { FocusMonitor } from '@angular/cdk/a11y';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { Directive, signal, computed,
|
|
3
|
+
import { Directive, signal, computed, inject, DestroyRef, effect, untracked, Injectable, Pipe, input, ChangeDetectionStrategy, Component, DOCUMENT, NgZone, ElementRef, afterRenderEffect, contentChild, TemplateRef, afterNextRender } from '@angular/core';
|
|
4
4
|
import { toObservable, toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
5
5
|
import { MonaI18nService, injectComponentDirection } from '@nanahoshi/mona-ui/i18n';
|
|
6
|
+
import { themeOverlaySurfaceClasses, isTypeaheadKey, setupTypeahead } from '@nanahoshi/mona-ui/internal';
|
|
6
7
|
import { Subject, BehaviorSubject, ReplaySubject, switchMap, debounceTime, distinctUntilChanged, Observable, take, filter, tap, map, asapScheduler, fromEvent, takeWhile } from 'rxjs';
|
|
7
8
|
import { PreventableEvent } from '@nanahoshi/mona-ui/common';
|
|
8
|
-
import { ImmutableSet, List, from, sequenceEqual,
|
|
9
|
+
import { ImmutableSet, List, from, sequenceEqual, EnumerableSet, forEach } from '@mirei/ts-collections';
|
|
9
10
|
import { v4 } from 'uuid';
|
|
10
11
|
import { cva } from 'class-variance-authority';
|
|
11
|
-
import { themeOverlaySurfaceClasses } from '@nanahoshi/mona-ui/internal';
|
|
12
12
|
import { CdkDropList, CdkDrag, CdkDragPreview } from '@angular/cdk/drag-drop';
|
|
13
13
|
import { LucideArrowDown, LucideArrowUp, LucideBan, LucideChevronDown, LucideChevronRight, LucideLoaderCircle, LucidePlus } from '@lucide/angular';
|
|
14
14
|
import * as i1 from '@angular/forms';
|
|
@@ -66,6 +66,11 @@ class TreeNode {
|
|
|
66
66
|
this.loading = signal(false, /* @ts-ignore */
|
|
67
67
|
...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
68
68
|
this.uid = v4();
|
|
69
|
+
/**
|
|
70
|
+
* Whether children from an asynchronous children selector have been loaded. Loaded children are cached,
|
|
71
|
+
* so collapsing and re-expanding the node does not request them again.
|
|
72
|
+
*/
|
|
73
|
+
this.childrenLoaded = false;
|
|
69
74
|
this.index = "";
|
|
70
75
|
this.parent = null;
|
|
71
76
|
this.data = data;
|
|
@@ -103,6 +108,10 @@ function getTreeNodeElementId(uid) {
|
|
|
103
108
|
var _a;
|
|
104
109
|
class TreeService {
|
|
105
110
|
#checkedKeys;
|
|
111
|
+
/**
|
|
112
|
+
* Pending asynchronous child loads by node uid, so a new load for a node cancels the previous one.
|
|
113
|
+
*/
|
|
114
|
+
#childLoads;
|
|
106
115
|
#data;
|
|
107
116
|
#filteredExpandedKeys;
|
|
108
117
|
#flatIdField;
|
|
@@ -117,6 +126,10 @@ class TreeService {
|
|
|
117
126
|
constructor() {
|
|
118
127
|
this.#checkedKeys = signal(ImmutableSet.create(), /* @ts-ignore */
|
|
119
128
|
...(ngDevMode ? [{ debugName: "#checkedKeys" }] : /* istanbul ignore next */ []));
|
|
129
|
+
/**
|
|
130
|
+
* Pending asynchronous child loads by node uid, so a new load for a node cancels the previous one.
|
|
131
|
+
*/
|
|
132
|
+
this.#childLoads = new Map();
|
|
120
133
|
this.#data = signal(ImmutableSet.create(), /* @ts-ignore */
|
|
121
134
|
...(ngDevMode ? [{ debugName: "#data" }] : /* istanbul ignore next */ []));
|
|
122
135
|
this.#filteredExpandedKeys = signal(ImmutableSet.create(), /* @ts-ignore */
|
|
@@ -132,15 +145,12 @@ class TreeService {
|
|
|
132
145
|
this.#navigableNodes = computed(() => {
|
|
133
146
|
const visibleUids = this.#visibleUids();
|
|
134
147
|
const allFlatNodes = _a.flatten(this.nodeSet());
|
|
148
|
+
// flatten() walks the tree depth-first, so its order already is the visual (navigation) order.
|
|
135
149
|
if (visibleUids === null) {
|
|
136
|
-
return allFlatNodes
|
|
137
|
-
.where(n => n.parent === null || !this.isAnyParentCollapsed(n))
|
|
138
|
-
.orderBy(n => n.index)
|
|
139
|
-
.toImmutableSet();
|
|
150
|
+
return allFlatNodes.where(n => n.parent === null || !this.isAnyParentCollapsed(n)).toImmutableSet();
|
|
140
151
|
}
|
|
141
152
|
return allFlatNodes
|
|
142
153
|
.where(n => visibleUids.has(n.uid) && (n.parent === null || !this.isAnyParentCollapsed(n)))
|
|
143
|
-
.orderBy(n => n.index)
|
|
144
154
|
.toImmutableSet();
|
|
145
155
|
}, /* @ts-ignore */
|
|
146
156
|
...(ngDevMode ? [{ debugName: "#navigableNodes" }] : /* istanbul ignore next */ []));
|
|
@@ -158,8 +168,8 @@ class TreeService {
|
|
|
158
168
|
if (!filterText) {
|
|
159
169
|
return null;
|
|
160
170
|
}
|
|
161
|
-
|
|
162
|
-
return
|
|
171
|
+
// Tracked on purpose: moves, lazily loaded children and filter option changes re-run the filter.
|
|
172
|
+
return this.computeVisibleUids(this.nodeSet(), filterText);
|
|
163
173
|
}, /* @ts-ignore */
|
|
164
174
|
...(ngDevMode ? [{ debugName: "#visibleUids" }] : /* istanbul ignore next */ []));
|
|
165
175
|
this.animationTemporarilyDisabled = signal(false, /* @ts-ignore */
|
|
@@ -281,6 +291,10 @@ class TreeService {
|
|
|
281
291
|
.toImmutableSet();
|
|
282
292
|
}, /* @ts-ignore */
|
|
283
293
|
...(ngDevMode ? [{ debugName: "viewNodeSet" }] : /* istanbul ignore next */ []));
|
|
294
|
+
inject(DestroyRef).onDestroy(() => {
|
|
295
|
+
this.#childLoads.forEach(subscription => subscription.unsubscribe());
|
|
296
|
+
this.#childLoads.clear();
|
|
297
|
+
});
|
|
284
298
|
effect(() => {
|
|
285
299
|
const visibleUids = this.#visibleUids();
|
|
286
300
|
untracked(() => {
|
|
@@ -321,6 +335,34 @@ class TreeService {
|
|
|
321
335
|
}
|
|
322
336
|
this.updateNodeIndices();
|
|
323
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* Expands every expandable, enabled sibling of a node (the `*` key of the WAI-ARIA tree pattern).
|
|
340
|
+
*/
|
|
341
|
+
expandSiblings(node) {
|
|
342
|
+
const siblings = node.parent ? node.parent.children() : this.nodeSet();
|
|
343
|
+
for (const sibling of siblings) {
|
|
344
|
+
if (this.hasChildren(sibling)) {
|
|
345
|
+
this.toggleNodeExpand(sibling, true);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Finds the next visible node, after the navigated node and wrapping around, whose text starts with `prefix`
|
|
351
|
+
* (case-insensitive). Used for typeahead.
|
|
352
|
+
*/
|
|
353
|
+
findNodeByText(prefix) {
|
|
354
|
+
const normalizedPrefix = prefix.toLowerCase();
|
|
355
|
+
const nodes = this.#navigableNodes().toArray();
|
|
356
|
+
const navigatedNode = this.navigatedNode();
|
|
357
|
+
const start = navigatedNode ? nodes.indexOf(navigatedNode) : -1;
|
|
358
|
+
for (let offset = 1; offset <= nodes.length; offset++) {
|
|
359
|
+
const node = nodes[(start + offset) % nodes.length];
|
|
360
|
+
if (this.getNodeText(node).toLowerCase().startsWith(normalizedPrefix)) {
|
|
361
|
+
return node;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
324
366
|
getNodeByUid(uid) {
|
|
325
367
|
const nodeDictionary = this.#nodeDictionary();
|
|
326
368
|
return nodeDictionary.get(uid) ?? null;
|
|
@@ -330,13 +372,17 @@ class TreeService {
|
|
|
330
372
|
}
|
|
331
373
|
getNodeText(node) {
|
|
332
374
|
const textField = this.textField();
|
|
375
|
+
let text;
|
|
333
376
|
if (!textField) {
|
|
334
|
-
|
|
377
|
+
text = node.data;
|
|
335
378
|
}
|
|
336
|
-
if (typeof textField === "string") {
|
|
337
|
-
|
|
379
|
+
else if (typeof textField === "string") {
|
|
380
|
+
text = node.data?.[textField];
|
|
338
381
|
}
|
|
339
|
-
|
|
382
|
+
else {
|
|
383
|
+
text = textField(node.data);
|
|
384
|
+
}
|
|
385
|
+
return text == null ? "" : String(text);
|
|
340
386
|
}
|
|
341
387
|
hasChildren(node) {
|
|
342
388
|
if (node === null) {
|
|
@@ -387,17 +433,23 @@ class TreeService {
|
|
|
387
433
|
}
|
|
388
434
|
isDisabled(node) {
|
|
389
435
|
const disableOptions = this.disableOptions();
|
|
390
|
-
const disabledKeys = this.disabledKeys();
|
|
391
436
|
if (!disableOptions.enabled) {
|
|
392
437
|
return false;
|
|
393
438
|
}
|
|
394
|
-
const
|
|
395
|
-
|
|
439
|
+
const disabledKeys = this.disabledKeys();
|
|
440
|
+
if (disabledKeys.contains(this.getDisableKey(node))) {
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
396
443
|
if (!disableOptions.disableChildren) {
|
|
397
|
-
return
|
|
444
|
+
return false;
|
|
398
445
|
}
|
|
399
|
-
|
|
400
|
-
|
|
446
|
+
// A single walk up the ancestors: O(depth) instead of re-evaluating every ancestor's own ancestors.
|
|
447
|
+
for (let parent = node.parent; parent != null; parent = parent.parent) {
|
|
448
|
+
if (disabledKeys.contains(this.getDisableKey(parent))) {
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return false;
|
|
401
453
|
}
|
|
402
454
|
isExpanded(node) {
|
|
403
455
|
const expandableOptions = this.expandableOptions();
|
|
@@ -419,8 +471,7 @@ class TreeService {
|
|
|
419
471
|
if (node.children().isEmpty()) {
|
|
420
472
|
return false;
|
|
421
473
|
}
|
|
422
|
-
const
|
|
423
|
-
const childNodes = hasCheckbox !== null ? node.children().where(n => hasCheckbox(n.data, n.uid)) : node.children();
|
|
474
|
+
const childNodes = this.getCheckStateChildren(node);
|
|
424
475
|
if (childNodes.none()) {
|
|
425
476
|
return false;
|
|
426
477
|
}
|
|
@@ -452,18 +503,25 @@ class TreeService {
|
|
|
452
503
|
}
|
|
453
504
|
return visibleUids.has(node.uid);
|
|
454
505
|
}
|
|
455
|
-
|
|
506
|
+
/**
|
|
507
|
+
* Loads the children of a node from an asynchronous children selector.
|
|
508
|
+
* Children are loaded once and cached; pass `reload` to request them again. A new load cancels a pending one.
|
|
509
|
+
*/
|
|
510
|
+
loadNodeChildren(node, reload = false) {
|
|
456
511
|
const childrenSelector = this.children();
|
|
457
512
|
if (typeof childrenSelector !== "function") {
|
|
458
513
|
return;
|
|
459
514
|
}
|
|
515
|
+
if (!reload && (node.childrenLoaded || node.loading())) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
460
518
|
const children = childrenSelector(node.data);
|
|
461
519
|
if (!(children instanceof Observable)) {
|
|
462
520
|
return;
|
|
463
521
|
}
|
|
464
|
-
|
|
522
|
+
this.#childLoads.get(node.uid)?.unsubscribe();
|
|
465
523
|
node.loading.set(true);
|
|
466
|
-
children.pipe(take(1)).subscribe({
|
|
524
|
+
const subscription = children.pipe(take(1)).subscribe({
|
|
467
525
|
next: c => {
|
|
468
526
|
const childNodes = new List();
|
|
469
527
|
for (const child of c) {
|
|
@@ -473,15 +531,35 @@ class TreeService {
|
|
|
473
531
|
}
|
|
474
532
|
this.loadCheckedKeysForChildren(node, childNodes);
|
|
475
533
|
node.children.update(list => list.clear().addAll(childNodes));
|
|
534
|
+
this.updateNodeIndices();
|
|
535
|
+
node.childrenLoaded = true;
|
|
476
536
|
node.loading.set(false);
|
|
537
|
+
this.#childLoads.delete(node.uid);
|
|
477
538
|
this.restoreExpandedChildren(childNodes);
|
|
478
539
|
},
|
|
479
540
|
error: () => {
|
|
480
541
|
node.loading.set(false);
|
|
542
|
+
this.#childLoads.delete(node.uid);
|
|
543
|
+
},
|
|
544
|
+
complete: () => {
|
|
545
|
+
if (node.loading()) {
|
|
546
|
+
node.loading.set(false);
|
|
547
|
+
}
|
|
548
|
+
this.#childLoads.delete(node.uid);
|
|
481
549
|
}
|
|
482
550
|
});
|
|
551
|
+
if (node.loading()) {
|
|
552
|
+
this.#childLoads.set(node.uid, subscription);
|
|
553
|
+
}
|
|
483
554
|
}
|
|
555
|
+
/**
|
|
556
|
+
* Moves a node relative to a target node. Moving a node onto itself or into its own subtree is ignored,
|
|
557
|
+
* because it would detach the subtree and create a parent cycle.
|
|
558
|
+
*/
|
|
484
559
|
moveNode(sourceNode, targetNode, position) {
|
|
560
|
+
if (sourceNode === targetNode || targetNode.isDescendantOf(sourceNode)) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
485
563
|
const oldParent = sourceNode.parent;
|
|
486
564
|
switch (position) {
|
|
487
565
|
case "inside":
|
|
@@ -552,20 +630,24 @@ class TreeService {
|
|
|
552
630
|
if (sequenceEqual(checkedKeys, orderedKeys)) {
|
|
553
631
|
return;
|
|
554
632
|
}
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
.where(n => contains(keys, this.getCheckKey(n)))
|
|
558
|
-
.toEnumerableSet();
|
|
633
|
+
const keyList = Array.from(keys);
|
|
634
|
+
const keySet = new Set(keyList);
|
|
559
635
|
this.#nodeDictionary().forEach(n => {
|
|
560
|
-
n.value.checked.set(
|
|
636
|
+
n.value.checked.set(keySet.has(this.getCheckKey(n.value)));
|
|
561
637
|
});
|
|
562
|
-
this.#checkedKeys.set(ImmutableSet.create(
|
|
638
|
+
this.#checkedKeys.set(ImmutableSet.create(keyList));
|
|
639
|
+
this.applyCheckedKeysToNodes();
|
|
563
640
|
}
|
|
564
641
|
setData(data) {
|
|
565
642
|
this.#data.set(ImmutableSet.create(data));
|
|
566
643
|
this.nodeSet.set(this.createNodes(data));
|
|
567
644
|
this.updateNodeIndices();
|
|
568
645
|
this.applyCheckedKeysToNodes();
|
|
646
|
+
const navigatedNode = this.navigatedNode();
|
|
647
|
+
if (navigatedNode && this.#nodeDictionary().get(navigatedNode.uid) == null) {
|
|
648
|
+
// The node was removed: drop it so aria-activedescendant never points at a missing element.
|
|
649
|
+
this.navigatedNode.set(null);
|
|
650
|
+
}
|
|
569
651
|
}
|
|
570
652
|
setDataStructure(structure) {
|
|
571
653
|
this.#structure.set(structure);
|
|
@@ -666,11 +748,12 @@ class TreeService {
|
|
|
666
748
|
});
|
|
667
749
|
}
|
|
668
750
|
if (mode !== "single" && checkParents && !childrenOnly) {
|
|
669
|
-
const hasCheckbox = this.#hasCheckboxPredicate();
|
|
670
751
|
const parentNodes = this.getParentNodes(node);
|
|
671
752
|
parentNodes.forEach(n => {
|
|
672
|
-
const
|
|
673
|
-
|
|
753
|
+
const allChecked = this.deriveParentChecked(n);
|
|
754
|
+
if (allChecked === null) {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
674
757
|
const parentKey = this.getCheckKey(n);
|
|
675
758
|
if (allChecked) {
|
|
676
759
|
checkedKeys.add(parentKey);
|
|
@@ -749,6 +832,22 @@ class TreeService {
|
|
|
749
832
|
setTextField(selector) {
|
|
750
833
|
this.textField.set(selector);
|
|
751
834
|
}
|
|
835
|
+
/**
|
|
836
|
+
* Expands or collapses a node the way user interaction does: disabled nodes are left alone, children of an
|
|
837
|
+
* asynchronous children selector are loaded on first expand, and `nodeExpand$` is notified.
|
|
838
|
+
* @returns whether the expand state changed.
|
|
839
|
+
*/
|
|
840
|
+
toggleNodeExpand(node, expanded) {
|
|
841
|
+
if (this.isDisabled(node) || this.isExpanded(node) === expanded) {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
if (expanded) {
|
|
845
|
+
this.loadNodeChildren(node);
|
|
846
|
+
}
|
|
847
|
+
this.setNodeExpand(node, expanded);
|
|
848
|
+
this.nodeExpand$.next({ node, expanded });
|
|
849
|
+
return true;
|
|
850
|
+
}
|
|
752
851
|
getCheckKey(node) {
|
|
753
852
|
const checkBy = this.checkBy();
|
|
754
853
|
if (!checkBy) {
|
|
@@ -784,24 +883,15 @@ class TreeService {
|
|
|
784
883
|
const allNodes = _a.flatten(this.nodeSet());
|
|
785
884
|
const updatedCheckedKeys = this.#checkedKeys().toEnumerableSet();
|
|
786
885
|
let changed = false;
|
|
886
|
+
// Children before parents, so derived parents feed their own ancestors. Parents are only ever added:
|
|
887
|
+
// a parent key supplied by the consumer is kept even when its children are not all checked.
|
|
787
888
|
allNodes.reverse().forEach(node => {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
const allChildrenChecked = childNodes.all(c => c.checked());
|
|
791
|
-
const key = this.getCheckKey(node);
|
|
792
|
-
if (allChildrenChecked !== node.checked()) {
|
|
793
|
-
node.checked.set(allChildrenChecked);
|
|
794
|
-
changed = true;
|
|
795
|
-
}
|
|
796
|
-
if (allChildrenChecked && !updatedCheckedKeys.contains(key)) {
|
|
797
|
-
updatedCheckedKeys.add(key);
|
|
798
|
-
changed = true;
|
|
799
|
-
}
|
|
800
|
-
else if (!allChildrenChecked && updatedCheckedKeys.contains(key)) {
|
|
801
|
-
updatedCheckedKeys.remove(key);
|
|
802
|
-
changed = true;
|
|
803
|
-
}
|
|
889
|
+
if (this.deriveParentChecked(node) !== true || node.checked()) {
|
|
890
|
+
return;
|
|
804
891
|
}
|
|
892
|
+
node.checked.set(true);
|
|
893
|
+
updatedCheckedKeys.add(this.getCheckKey(node));
|
|
894
|
+
changed = true;
|
|
805
895
|
});
|
|
806
896
|
if (changed) {
|
|
807
897
|
this.#checkedKeys.set(updatedCheckedKeys.toImmutableSet());
|
|
@@ -847,35 +937,33 @@ class TreeService {
|
|
|
847
937
|
};
|
|
848
938
|
return buildNodes(parentId, null);
|
|
849
939
|
}
|
|
850
|
-
|
|
940
|
+
/**
|
|
941
|
+
* Builds tree nodes for hierarchical data, reusing the existing node of each data item so node state survives
|
|
942
|
+
* a data refresh. A data object that appears more than once gets a node of its own for every extra occurrence.
|
|
943
|
+
*/
|
|
944
|
+
createHierarchicalNodes(root, parent, existingNodes, usedNodes) {
|
|
851
945
|
const rootList = new List(root);
|
|
852
946
|
const children = this.children();
|
|
853
|
-
if (rootList.isEmpty()
|
|
947
|
+
if (rootList.isEmpty()) {
|
|
854
948
|
return null;
|
|
855
949
|
}
|
|
856
950
|
const nodes = new EnumerableSet();
|
|
857
951
|
for (const dataItem of rootList) {
|
|
858
|
-
const existingNode =
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
node.parent = parent;
|
|
863
|
-
}
|
|
864
|
-
else {
|
|
865
|
-
node = new TreeNode(dataItem);
|
|
866
|
-
node.parent = parent;
|
|
867
|
-
}
|
|
952
|
+
const existingNode = existingNodes.get(dataItem);
|
|
953
|
+
const node = existingNode && !usedNodes.has(existingNode) ? existingNode : new TreeNode(dataItem);
|
|
954
|
+
node.parent = parent;
|
|
955
|
+
usedNodes.add(node);
|
|
868
956
|
if (typeof children === "string") {
|
|
869
957
|
const subNodes = dataItem[children];
|
|
870
958
|
if (subNodes) {
|
|
871
|
-
const childNodes = this.createHierarchicalNodes(subNodes, node,
|
|
959
|
+
const childNodes = this.createHierarchicalNodes(subNodes, node, existingNodes, usedNodes);
|
|
872
960
|
node.children.update(list => list.clear().addAll(childNodes ?? []));
|
|
873
961
|
}
|
|
874
962
|
}
|
|
875
963
|
else {
|
|
876
964
|
const result = children(dataItem);
|
|
877
965
|
if (!(result instanceof Observable)) {
|
|
878
|
-
const childNodes = this.createHierarchicalNodes(result, node,
|
|
966
|
+
const childNodes = this.createHierarchicalNodes(result, node, existingNodes, usedNodes);
|
|
879
967
|
node.children.update(list => list.clear().addAll(childNodes ?? []));
|
|
880
968
|
}
|
|
881
969
|
}
|
|
@@ -892,8 +980,33 @@ class TreeService {
|
|
|
892
980
|
if (structure === "flat") {
|
|
893
981
|
return this.createFlatNodes(data, null).toImmutableSet();
|
|
894
982
|
}
|
|
895
|
-
const
|
|
896
|
-
|
|
983
|
+
const existingNodes = new Map();
|
|
984
|
+
for (const node of this.#nodeDictionary().values()) {
|
|
985
|
+
if (!existingNodes.has(node.data)) {
|
|
986
|
+
existingNodes.set(node.data, node);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return this.createHierarchicalNodes(data, null, existingNodes, new Set()) ?? ImmutableSet.create();
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* The derived check state of a parent: `true` when all of its check-state children are checked,
|
|
993
|
+
* or `null` when it has none (its own state then stands).
|
|
994
|
+
*/
|
|
995
|
+
deriveParentChecked(node) {
|
|
996
|
+
const children = this.getCheckStateChildren(node);
|
|
997
|
+
return children.none() ? null : children.all(c => c.checked());
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Children that determine a parent's derived check state: those with a checkbox and, unless
|
|
1001
|
+
* `checkDisabledChildren` is set, only enabled ones, because the user cannot change disabled children.
|
|
1002
|
+
*/
|
|
1003
|
+
getCheckStateChildren(node) {
|
|
1004
|
+
const hasCheckbox = this.#hasCheckboxPredicate();
|
|
1005
|
+
const checkDisabledChildren = this.checkableOptions().checkDisabledChildren;
|
|
1006
|
+
return node
|
|
1007
|
+
.children()
|
|
1008
|
+
.where(c => (hasCheckbox === null || hasCheckbox(c.data, c.uid)) &&
|
|
1009
|
+
(checkDisabledChildren || !this.isDisabled(c)));
|
|
897
1010
|
}
|
|
898
1011
|
getExpandKey(node) {
|
|
899
1012
|
const expandBy = this.expandBy();
|
|
@@ -944,12 +1057,6 @@ class TreeService {
|
|
|
944
1057
|
}
|
|
945
1058
|
return false;
|
|
946
1059
|
}
|
|
947
|
-
isAnyParentDisabled(node) {
|
|
948
|
-
if (node.parent) {
|
|
949
|
-
return this.isDisabled(node.parent) || this.isAnyParentDisabled(node.parent);
|
|
950
|
-
}
|
|
951
|
-
return false;
|
|
952
|
-
}
|
|
953
1060
|
isFiltered(nodeText, filterText) {
|
|
954
1061
|
const text = this.filterableOptions().caseSensitive ? nodeText : nodeText.toLowerCase();
|
|
955
1062
|
const filter = this.filterableOptions().caseSensitive ? filterText : filterText.toLowerCase();
|
|
@@ -1088,7 +1195,7 @@ class TreeService {
|
|
|
1088
1195
|
const navigableNodes = this.#navigableNodes();
|
|
1089
1196
|
const navigatedNode = this.navigatedNode();
|
|
1090
1197
|
const firstNode = navigableNodes.first();
|
|
1091
|
-
|
|
1198
|
+
const nextNode = navigableNodes
|
|
1092
1199
|
.skipWhile(n => n !== navigatedNode)
|
|
1093
1200
|
.skip(1)
|
|
1094
1201
|
.firstOrDefault();
|
|
@@ -1104,7 +1211,7 @@ class TreeService {
|
|
|
1104
1211
|
navigateToPreviousNode() {
|
|
1105
1212
|
const navigableNodes = this.#navigableNodes();
|
|
1106
1213
|
const navigatedNode = this.navigatedNode();
|
|
1107
|
-
|
|
1214
|
+
const previousNode = navigableNodes.takeWhile(n => n !== navigatedNode).lastOrDefault();
|
|
1108
1215
|
if (previousNode === null) {
|
|
1109
1216
|
this.navigatedNode.set(navigableNodes.last());
|
|
1110
1217
|
return navigableNodes.last();
|
|
@@ -1124,12 +1231,11 @@ class TreeService {
|
|
|
1124
1231
|
const recalculateNodeChain = (start) => {
|
|
1125
1232
|
let current = start;
|
|
1126
1233
|
while (current !== null) {
|
|
1127
|
-
const
|
|
1128
|
-
if (
|
|
1234
|
+
const allChecked = this.deriveParentChecked(current);
|
|
1235
|
+
if (allChecked === null) {
|
|
1129
1236
|
current = current.parent;
|
|
1130
1237
|
continue;
|
|
1131
1238
|
}
|
|
1132
|
-
const allChecked = childNodes.all(c => c.checked());
|
|
1133
1239
|
const key = this.getCheckKey(current);
|
|
1134
1240
|
if (allChecked !== current.checked()) {
|
|
1135
1241
|
current.checked.set(allChecked);
|
|
@@ -1620,12 +1726,7 @@ class SubTreeComponent {
|
|
|
1620
1726
|
#i18n;
|
|
1621
1727
|
#zone;
|
|
1622
1728
|
onExpandStateChange(node) {
|
|
1623
|
-
|
|
1624
|
-
if (!expanded) {
|
|
1625
|
-
this.treeService.loadNodeChildren(node);
|
|
1626
|
-
}
|
|
1627
|
-
this.treeService.setNodeExpand(node, !expanded);
|
|
1628
|
-
this.treeService.nodeExpand$.next({ node, expanded: !expanded });
|
|
1729
|
+
this.treeService.toggleNodeExpand(node, !this.treeService.isExpanded(node));
|
|
1629
1730
|
}
|
|
1630
1731
|
onNodeDragEnd(event) {
|
|
1631
1732
|
const nodeDragEndEvent = new NodeDragEndEvent(event.source.data, event.event);
|
|
@@ -1703,14 +1804,14 @@ class SubTreeComponent {
|
|
|
1703
1804
|
this.treeService.navigatedNode.set(node);
|
|
1704
1805
|
const element = this.#document
|
|
1705
1806
|
.querySelector(`li[data-uid="${node.uid}"]`)
|
|
1706
|
-
?.closest("
|
|
1807
|
+
?.closest("[role=tree]");
|
|
1707
1808
|
if (element == null) {
|
|
1708
1809
|
return;
|
|
1709
1810
|
}
|
|
1710
1811
|
element.focus();
|
|
1711
1812
|
}
|
|
1712
1813
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SubTreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1713
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: SubTreeComponent, isStandalone: true, selector: "mona-sub-tree", inputs: { depth: { classPropertyName: "depth", publicName: "depth", isSignal: true, isRequired: true, transformFunction: null }, nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: true, transformFunction: null }, parent: { classPropertyName: "parent", publicName: "parent", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<ul\n [class]=\"listClass()\"\n [attr.role]=\"parent() ? 'group' :
|
|
1814
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: SubTreeComponent, isStandalone: true, selector: "mona-sub-tree", inputs: { depth: { classPropertyName: "depth", publicName: "depth", isSignal: true, isRequired: true, transformFunction: null }, nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: true, transformFunction: null }, parent: { classPropertyName: "parent", publicName: "parent", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "<ul\n [class]=\"listClass()\"\n [attr.role]=\"parent() ? 'group' : 'none'\"\n cdkDropList\n [cdkDropListSortingDisabled]=\"true\"\n (cdkDropListDropped)=\"onNodeDrop($event)\">\n @for (node of visibleNodes(); track node.uid) {\n <li\n [attr.id]=\"node.uid | treeNodeElementId\"\n [attr.data-uid]=\"node.uid\"\n [class]=\"listItemClass()\"\n [tabindex]=\"-1\"\n role=\"treeitem\"\n [attr.aria-level]=\"depth() + 1\"\n [attr.aria-setsize]=\"visibleNodes().length\"\n [attr.aria-posinset]=\"$index + 1\"\n [attr.aria-expanded]=\"nodeComponent.expandable() ? nodeComponent.expanded() : null\"\n [attr.aria-selected]=\"treeService.selectableOptions().enabled ? nodeComponent.selected() : null\"\n [attr.aria-disabled]=\"nodeComponent.disabled() ? true : null\"\n [attr.aria-checked]=\"\n nodeComponent.checkable() ? (nodeComponent.indeterminate() ? 'mixed' : nodeComponent.checked()) : null\n \"\n (focus)=\"treeService.navigatedNode.set(node)\"\n cdkDrag\n [cdkDragData]=\"node\"\n [cdkDragDisabled]=\"!treeService.draggableOptions().enabled\"\n (cdkDragStarted)=\"onNodeDragStart($event)\"\n (cdkDragEnded)=\"onNodeDragEnd($event)\"\n (cdkDragMoved)=\"onNodeDragMove($event, node)\">\n <div [class]=\"nodeContainerClass()\">\n @if (treeService.expandableOptions().enabled && nodeComponent.expandable()) {\n <button\n type=\"button\"\n tabindex=\"-1\"\n [class]=\"nodeExpanderClass()\"\n [class.mona-disabled]=\"nodeComponent.disabled()\"\n [disabled]=\"nodeComponent.disabled()\"\n [attr.aria-label]=\"\n (nodeComponent.expanded() ? messages().collapse : messages().expand) +\n ' ' +\n nodeComponent.nodeText()\n \"\n (click)=\"onExpandStateChange(node)\">\n @if (node.loading()) {\n <svg lucideLoaderCircle [size]=\"16\" aria-hidden=\"true\" class=\"animate-spin\"></svg>\n } @else if (nodeComponent.expanded()) {\n <svg lucideChevronDown [size]=\"16\" aria-hidden=\"true\"></svg>\n } @else {\n <svg lucideChevronRight [size]=\"16\" aria-hidden=\"true\" class=\"rtl:rotate-180\"></svg>\n }\n </button>\n }\n @if (nodeComponent.checkable()) {\n <mona-check-box\n aria-hidden=\"true\"\n [tabIndex]=\"-1\"\n [disabled]=\"nodeComponent.disabled()\"\n [indeterminate]=\"nodeComponent.indeterminate()\"\n [ngModel]=\"nodeComponent.checked()\"\n (click)=\"nodeComponent.checkboxClick$.next($event)\"\n (ngModelChange)=\"nodeComponent.checkboxCheck$.next($event)\"></mona-check-box>\n }\n <mona-tree-node [node]=\"node\" [depth]=\"depth()\" #nodeComponent></mona-tree-node>\n </div>\n @if (node.children().length > 0 && nodeComponent.expanded()) {\n <div\n class=\"grid overflow-hidden\"\n [animate.enter]=\"nodeExpandEnterClass()\"\n [animate.leave]=\"nodeExpandLeaveClass()\">\n <div class=\"min-h-0 overflow-hidden\">\n <mona-sub-tree [nodes]=\"node.children()\" [depth]=\"depth() + 1\" [parent]=\"node\"></mona-sub-tree>\n </div>\n </div>\n }\n @if (!nodeComponent.disabled()) {\n <div *cdkDragPreview [class]=\"nodeDraggingClass()\">\n <span>\n @switch (treeService.dropPositionSignal()?.position) {\n @case (\"before\") {\n @if (treeService.dropAllowed()) {\n <svg lucideArrowUp [size]=\"16\" aria-hidden=\"true\"></svg>\n } @else {\n <svg lucideBan [size]=\"16\" aria-hidden=\"true\"></svg>\n }\n }\n @case (\"after\") {\n @if (treeService.dropAllowed()) {\n <svg lucideArrowDown [size]=\"16\" aria-hidden=\"true\"></svg>\n } @else {\n <svg lucideBan [size]=\"16\" aria-hidden=\"true\"></svg>\n }\n }\n @case (\"inside\") {\n <svg lucidePlus [size]=\"16\" aria-hidden=\"true\"></svg>\n }\n @default {\n <svg lucideBan [size]=\"16\" aria-hidden=\"true\"></svg>\n }\n }\n </span>\n <span>{{ nodeComponent.nodeText() }}</span>\n </div>\n } @else {\n <div *cdkDragPreview [class]=\"nodeDraggingClass()\"></div>\n }\n </li>\n }\n</ul>\n", styles: [".mona-tree-node-expand-enter{animation:mona-tree-node-expand-in .15s ease-out}.mona-tree-node-expand-leave{animation:mona-tree-node-expand-out .15s ease-out}@keyframes mona-tree-node-expand-in{0%{grid-template-rows:0fr;opacity:0}to{grid-template-rows:1fr;opacity:1}}@keyframes mona-tree-node-expand-out{0%{grid-template-rows:1fr;opacity:1}to{grid-template-rows:0fr;opacity:0}}@media(prefers-reduced-motion:reduce){.mona-tree-node-expand-enter,.mona-tree-node-expand-leave{animation-duration:1ms}}\n"], dependencies: [{ kind: "component", type: SubTreeComponent, selector: "mona-sub-tree", inputs: ["depth", "nodes", "parent"] }, { kind: "component", type: LucideArrowDown, selector: "svg[lucideArrowDown]" }, { kind: "component", type: LucideArrowUp, selector: "svg[lucideArrowUp]" }, { kind: "component", type: LucideBan, selector: "svg[lucideBan]" }, { kind: "component", type: LucideChevronDown, selector: "svg[lucideChevronDown]" }, { kind: "component", type: LucideChevronRight, selector: "svg[lucideChevronRight]" }, { kind: "component", type: LucideLoaderCircle, selector: "svg[lucideLoaderCircle], svg[lucideLoader2]" }, { kind: "component", type: LucidePlus, selector: "svg[lucidePlus]" }, { kind: "component", type: TreeNodeComponent, selector: "mona-tree-node", inputs: ["depth", "node"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: CdkDragPreview, selector: "ng-template[cdkDragPreview]", inputs: ["data", "matchSize"] }, { kind: "component", type: CheckBoxComponent, selector: "mona-check-box", inputs: ["checked", "disabled", "indeterminate", "invalid", "label", "labelPosition", "labelSize", "required", "rounded", "tabIndex", "touched", "class"], outputs: ["checkedChange", "inputBlur", "inputChange", "inputFocus", "touch"] }, { kind: "pipe", type: TreeNodeElementIdPipe, name: "treeNodeElementId" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1714
1815
|
}
|
|
1715
1816
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SubTreeComponent, decorators: [{
|
|
1716
1817
|
type: Component,
|
|
@@ -1729,7 +1830,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
|
|
|
1729
1830
|
CdkDragPreview,
|
|
1730
1831
|
CheckBoxComponent,
|
|
1731
1832
|
TreeNodeElementIdPipe
|
|
1732
|
-
], template: "<ul\n [class]=\"listClass()\"\n [attr.role]=\"parent() ? 'group' :
|
|
1833
|
+
], template: "<ul\n [class]=\"listClass()\"\n [attr.role]=\"parent() ? 'group' : 'none'\"\n cdkDropList\n [cdkDropListSortingDisabled]=\"true\"\n (cdkDropListDropped)=\"onNodeDrop($event)\">\n @for (node of visibleNodes(); track node.uid) {\n <li\n [attr.id]=\"node.uid | treeNodeElementId\"\n [attr.data-uid]=\"node.uid\"\n [class]=\"listItemClass()\"\n [tabindex]=\"-1\"\n role=\"treeitem\"\n [attr.aria-level]=\"depth() + 1\"\n [attr.aria-setsize]=\"visibleNodes().length\"\n [attr.aria-posinset]=\"$index + 1\"\n [attr.aria-expanded]=\"nodeComponent.expandable() ? nodeComponent.expanded() : null\"\n [attr.aria-selected]=\"treeService.selectableOptions().enabled ? nodeComponent.selected() : null\"\n [attr.aria-disabled]=\"nodeComponent.disabled() ? true : null\"\n [attr.aria-checked]=\"\n nodeComponent.checkable() ? (nodeComponent.indeterminate() ? 'mixed' : nodeComponent.checked()) : null\n \"\n (focus)=\"treeService.navigatedNode.set(node)\"\n cdkDrag\n [cdkDragData]=\"node\"\n [cdkDragDisabled]=\"!treeService.draggableOptions().enabled\"\n (cdkDragStarted)=\"onNodeDragStart($event)\"\n (cdkDragEnded)=\"onNodeDragEnd($event)\"\n (cdkDragMoved)=\"onNodeDragMove($event, node)\">\n <div [class]=\"nodeContainerClass()\">\n @if (treeService.expandableOptions().enabled && nodeComponent.expandable()) {\n <button\n type=\"button\"\n tabindex=\"-1\"\n [class]=\"nodeExpanderClass()\"\n [class.mona-disabled]=\"nodeComponent.disabled()\"\n [disabled]=\"nodeComponent.disabled()\"\n [attr.aria-label]=\"\n (nodeComponent.expanded() ? messages().collapse : messages().expand) +\n ' ' +\n nodeComponent.nodeText()\n \"\n (click)=\"onExpandStateChange(node)\">\n @if (node.loading()) {\n <svg lucideLoaderCircle [size]=\"16\" aria-hidden=\"true\" class=\"animate-spin\"></svg>\n } @else if (nodeComponent.expanded()) {\n <svg lucideChevronDown [size]=\"16\" aria-hidden=\"true\"></svg>\n } @else {\n <svg lucideChevronRight [size]=\"16\" aria-hidden=\"true\" class=\"rtl:rotate-180\"></svg>\n }\n </button>\n }\n @if (nodeComponent.checkable()) {\n <mona-check-box\n aria-hidden=\"true\"\n [tabIndex]=\"-1\"\n [disabled]=\"nodeComponent.disabled()\"\n [indeterminate]=\"nodeComponent.indeterminate()\"\n [ngModel]=\"nodeComponent.checked()\"\n (click)=\"nodeComponent.checkboxClick$.next($event)\"\n (ngModelChange)=\"nodeComponent.checkboxCheck$.next($event)\"></mona-check-box>\n }\n <mona-tree-node [node]=\"node\" [depth]=\"depth()\" #nodeComponent></mona-tree-node>\n </div>\n @if (node.children().length > 0 && nodeComponent.expanded()) {\n <div\n class=\"grid overflow-hidden\"\n [animate.enter]=\"nodeExpandEnterClass()\"\n [animate.leave]=\"nodeExpandLeaveClass()\">\n <div class=\"min-h-0 overflow-hidden\">\n <mona-sub-tree [nodes]=\"node.children()\" [depth]=\"depth() + 1\" [parent]=\"node\"></mona-sub-tree>\n </div>\n </div>\n }\n @if (!nodeComponent.disabled()) {\n <div *cdkDragPreview [class]=\"nodeDraggingClass()\">\n <span>\n @switch (treeService.dropPositionSignal()?.position) {\n @case (\"before\") {\n @if (treeService.dropAllowed()) {\n <svg lucideArrowUp [size]=\"16\" aria-hidden=\"true\"></svg>\n } @else {\n <svg lucideBan [size]=\"16\" aria-hidden=\"true\"></svg>\n }\n }\n @case (\"after\") {\n @if (treeService.dropAllowed()) {\n <svg lucideArrowDown [size]=\"16\" aria-hidden=\"true\"></svg>\n } @else {\n <svg lucideBan [size]=\"16\" aria-hidden=\"true\"></svg>\n }\n }\n @case (\"inside\") {\n <svg lucidePlus [size]=\"16\" aria-hidden=\"true\"></svg>\n }\n @default {\n <svg lucideBan [size]=\"16\" aria-hidden=\"true\"></svg>\n }\n }\n </span>\n <span>{{ nodeComponent.nodeText() }}</span>\n </div>\n } @else {\n <div *cdkDragPreview [class]=\"nodeDraggingClass()\"></div>\n }\n </li>\n }\n</ul>\n", styles: [".mona-tree-node-expand-enter{animation:mona-tree-node-expand-in .15s ease-out}.mona-tree-node-expand-leave{animation:mona-tree-node-expand-out .15s ease-out}@keyframes mona-tree-node-expand-in{0%{grid-template-rows:0fr;opacity:0}to{grid-template-rows:1fr;opacity:1}}@keyframes mona-tree-node-expand-out{0%{grid-template-rows:1fr;opacity:1}to{grid-template-rows:0fr;opacity:0}}@media(prefers-reduced-motion:reduce){.mona-tree-node-expand-enter,.mona-tree-node-expand-leave{animation-duration:1ms}}\n"] }]
|
|
1733
1834
|
}], propDecorators: { depth: [{ type: i0.Input, args: [{ isSignal: true, alias: "depth", required: true }] }], nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: true }] }], parent: [{ type: i0.Input, args: [{ isSignal: true, alias: "parent", required: true }] }] } });
|
|
1734
1835
|
|
|
1735
1836
|
class TreeDropHintComponent {
|
|
@@ -1811,6 +1912,7 @@ class TreeComponent {
|
|
|
1811
1912
|
#focusMonitor;
|
|
1812
1913
|
#hostElementRef;
|
|
1813
1914
|
#i18n;
|
|
1915
|
+
#typeaheadKey$;
|
|
1814
1916
|
#zone;
|
|
1815
1917
|
#lastNavigatedNode;
|
|
1816
1918
|
constructor() {
|
|
@@ -1819,6 +1921,7 @@ class TreeComponent {
|
|
|
1819
1921
|
this.#focusMonitor = inject(FocusMonitor);
|
|
1820
1922
|
this.#hostElementRef = inject(ElementRef);
|
|
1821
1923
|
this.#i18n = inject(MonaI18nService);
|
|
1924
|
+
this.#typeaheadKey$ = new Subject();
|
|
1822
1925
|
this.#zone = inject(NgZone);
|
|
1823
1926
|
this.#lastNavigatedNode = null;
|
|
1824
1927
|
this.isRtl = computed(() => this.#direction() === "rtl", /* @ts-ignore */
|
|
@@ -1968,28 +2071,42 @@ class TreeComponent {
|
|
|
1968
2071
|
this.treeService.navigate("next");
|
|
1969
2072
|
}
|
|
1970
2073
|
else if (event.key === collapseKey) {
|
|
2074
|
+
// WAI-ARIA tree: collapse an open node, otherwise move to the parent.
|
|
1971
2075
|
event.preventDefault();
|
|
1972
|
-
if (!navigatedNode
|
|
2076
|
+
if (!navigatedNode) {
|
|
1973
2077
|
return;
|
|
1974
2078
|
}
|
|
1975
|
-
const
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2079
|
+
const collapsible = this.treeService.hasChildren(navigatedNode) && this.treeService.isExpanded(navigatedNode);
|
|
2080
|
+
if (collapsible && this.treeService.toggleNodeExpand(navigatedNode, false)) {
|
|
2081
|
+
return;
|
|
2082
|
+
}
|
|
2083
|
+
if (!collapsible && navigatedNode.parent) {
|
|
2084
|
+
this.treeService.navigatedNode.set(navigatedNode.parent);
|
|
1979
2085
|
}
|
|
1980
|
-
this.treeService.navigatedNode.set(navigatedNode);
|
|
1981
2086
|
}
|
|
1982
2087
|
else if (event.key === expandKey) {
|
|
2088
|
+
// WAI-ARIA tree: open a closed node, otherwise move to its first child.
|
|
1983
2089
|
event.preventDefault();
|
|
1984
|
-
if (!navigatedNode || !
|
|
2090
|
+
if (!navigatedNode || !this.treeService.hasChildren(navigatedNode)) {
|
|
1985
2091
|
return;
|
|
1986
2092
|
}
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
2093
|
+
if (!this.treeService.isExpanded(navigatedNode)) {
|
|
2094
|
+
this.treeService.toggleNodeExpand(navigatedNode, true);
|
|
2095
|
+
}
|
|
2096
|
+
else if (!navigatedNode.children().isEmpty()) {
|
|
2097
|
+
this.treeService.navigate("next");
|
|
1991
2098
|
}
|
|
1992
2099
|
}
|
|
2100
|
+
else if (event.key === "*") {
|
|
2101
|
+
event.preventDefault();
|
|
2102
|
+
if (navigatedNode) {
|
|
2103
|
+
this.treeService.expandSiblings(navigatedNode);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
else if (event.key !== " " && isTypeaheadKey(event.key, event)) {
|
|
2107
|
+
event.preventDefault();
|
|
2108
|
+
this.#typeaheadKey$.next(event.key);
|
|
2109
|
+
}
|
|
1993
2110
|
else if (event.key === " ") {
|
|
1994
2111
|
event.preventDefault();
|
|
1995
2112
|
if (!navigatedNode || this.treeService.isDisabled(navigatedNode)) {
|
|
@@ -2056,6 +2173,17 @@ class TreeComponent {
|
|
|
2056
2173
|
this.setFocusSubscription();
|
|
2057
2174
|
this.setKeydownSubscription();
|
|
2058
2175
|
this.setNodeDragHandlerSubscription();
|
|
2176
|
+
this.setTypeaheadSubscription();
|
|
2177
|
+
}
|
|
2178
|
+
setTypeaheadSubscription() {
|
|
2179
|
+
setupTypeahead(this.#typeaheadKey$)
|
|
2180
|
+
.pipe(takeUntilDestroyed(this.#destroyRef))
|
|
2181
|
+
.subscribe(buffer => {
|
|
2182
|
+
const node = this.treeService.findNodeByText(buffer);
|
|
2183
|
+
if (node) {
|
|
2184
|
+
this.treeService.navigatedNode.set(node);
|
|
2185
|
+
}
|
|
2186
|
+
});
|
|
2059
2187
|
}
|
|
2060
2188
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: TreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2061
2189
|
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: TreeComponent, isStandalone: true, selector: "mona-tree", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "baseClass()", "attr.role": "'tree'", "attr.tabindex": "0", "attr.aria-label": "ariaLabel() || null", "attr.aria-multiselectable": "treeService.selectableOptions().mode === 'multiple' || null", "attr.aria-activedescendant": "activeDescendantId()" } }, queries: [{ propertyName: "nodeTemplate", first: true, predicate: TreeNodeTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }], ngImport: i0, template: "<mona-sub-tree [nodes]=\"treeService.viewNodeSet()\" [parent]=\"null\" [depth]=\"0\"></mona-sub-tree>\n@if (treeService.dragging() && treeService.dropAllowed()) {\n <mona-tree-drop-hint></mona-tree-drop-hint>\n}\n", dependencies: [{ kind: "component", type: SubTreeComponent, selector: "mona-sub-tree", inputs: ["depth", "nodes", "parent"] }, { kind: "component", type: TreeDropHintComponent, selector: "mona-tree-drop-hint" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
@@ -2079,15 +2207,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
|
|
|
2079
2207
|
* @param idKey - The property key used to uniquely identify each node (e.g., 'id').
|
|
2080
2208
|
* @param parentIdKey - The property key holding the parent node's id, or null for roots (e.g., 'parentId').
|
|
2081
2209
|
* @returns A new array representing the updated flat tree, with the moved node's parentIdKey updated.
|
|
2210
|
+
* The array is returned unchanged when the position is "outside" or the target is the dragged node or one of its
|
|
2211
|
+
* descendants, which would otherwise create a parent cycle.
|
|
2082
2212
|
*/
|
|
2083
2213
|
function moveFlatTreeNode(treeData, event, idKey, parentIdKey) {
|
|
2084
2214
|
const workingArray = [...treeData];
|
|
2085
2215
|
const draggedId = event.nodeItem.data[idKey];
|
|
2086
2216
|
const targetId = event.targetNode.data[idKey];
|
|
2087
2217
|
const position = event.position;
|
|
2088
|
-
if (position === "outside") {
|
|
2218
|
+
if (position === "outside" || draggedId === targetId) {
|
|
2089
2219
|
return workingArray;
|
|
2090
2220
|
}
|
|
2221
|
+
const itemsById = new Map(workingArray.map(item => [item[idKey], item]));
|
|
2222
|
+
const visited = new Set();
|
|
2223
|
+
for (let current = itemsById.get(targetId); current != null; current = itemsById.get(current[parentIdKey])) {
|
|
2224
|
+
if (current[idKey] === draggedId) {
|
|
2225
|
+
return workingArray;
|
|
2226
|
+
}
|
|
2227
|
+
if (visited.has(current[idKey])) {
|
|
2228
|
+
break;
|
|
2229
|
+
}
|
|
2230
|
+
visited.add(current[idKey]);
|
|
2231
|
+
}
|
|
2091
2232
|
const draggedIndex = workingArray.findIndex(item => item[idKey] === draggedId);
|
|
2092
2233
|
if (draggedIndex === -1) {
|
|
2093
2234
|
return workingArray;
|
|
@@ -2133,7 +2274,10 @@ function moveFlatTreeNode(treeData, event, idKey, parentIdKey) {
|
|
|
2133
2274
|
* @param event - The drag-and-drop event payload.
|
|
2134
2275
|
* @param idKey - The property key used to identify nodes (e.g., 'id').
|
|
2135
2276
|
* @param childrenKey - The property key containing the nested items (e.g., 'items').
|
|
2136
|
-
* @returns A new array representing the updated tree structure.
|
|
2277
|
+
* @returns A new top-level array representing the updated tree structure.
|
|
2278
|
+
* Nested children arrays are updated in place, so untouched data items keep their identity (and their tree state).
|
|
2279
|
+
* The move is skipped, returning the tree unchanged, when the position is "outside", the target is missing,
|
|
2280
|
+
* or the target is the dragged node or one of its descendants, so a node is never lost or made its own ancestor.
|
|
2137
2281
|
*/
|
|
2138
2282
|
function moveTreeNode(treeData, event, idKey, childrenKey) {
|
|
2139
2283
|
const updatedTree = [...treeData];
|
|
@@ -2141,6 +2285,29 @@ function moveTreeNode(treeData, event, idKey, childrenKey) {
|
|
|
2141
2285
|
const targetId = event.targetNode.data[idKey];
|
|
2142
2286
|
const position = event.position;
|
|
2143
2287
|
let draggedNode = null;
|
|
2288
|
+
if (position === "outside" || draggedId === targetId) {
|
|
2289
|
+
return updatedTree;
|
|
2290
|
+
}
|
|
2291
|
+
function findNode(nodes, id) {
|
|
2292
|
+
for (const node of nodes) {
|
|
2293
|
+
if (node[idKey] === id) {
|
|
2294
|
+
return node;
|
|
2295
|
+
}
|
|
2296
|
+
const children = node[childrenKey];
|
|
2297
|
+
const found = Array.isArray(children) ? findNode(children, id) : null;
|
|
2298
|
+
if (found) {
|
|
2299
|
+
return found;
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
return null;
|
|
2303
|
+
}
|
|
2304
|
+
const draggedSource = findNode(updatedTree, draggedId);
|
|
2305
|
+
const draggedChildren = draggedSource?.[childrenKey];
|
|
2306
|
+
if (draggedSource == null ||
|
|
2307
|
+
findNode(updatedTree, targetId) == null ||
|
|
2308
|
+
(Array.isArray(draggedChildren) && findNode(draggedChildren, targetId) != null)) {
|
|
2309
|
+
return updatedTree;
|
|
2310
|
+
}
|
|
2144
2311
|
function extractNode(nodes) {
|
|
2145
2312
|
for (let i = 0; i < nodes.length; i++) {
|
|
2146
2313
|
if (nodes[i][idKey] === draggedId) {
|