@nanahoshi/mona-ui 0.23.0 → 0.23.2

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.
Files changed (24) hide show
  1. package/fesm2022/nanahoshi-mona-ui-dropdown-list.mjs +1 -1
  2. 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
  3. package/fesm2022/{nanahoshi-mona-ui-grid-grid-list.component-S2qxwP2M.mjs → nanahoshi-mona-ui-grid-grid-list.component-CpeheGXU.mjs} +4 -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
  5. package/fesm2022/{nanahoshi-mona-ui-grid-grid-virtual-list.component-DArxdAah.mjs → nanahoshi-mona-ui-grid-grid-virtual-list.component-BzKywU5b.mjs} +4 -4
  6. 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
  7. package/fesm2022/nanahoshi-mona-ui-grid.mjs +1 -1
  8. package/fesm2022/nanahoshi-mona-ui-i18n.mjs +150 -90
  9. package/fesm2022/nanahoshi-mona-ui-internal-filter-input.mjs +47 -7
  10. package/fesm2022/nanahoshi-mona-ui-internal-list.mjs +386 -180
  11. package/fesm2022/nanahoshi-mona-ui-internal-tree.mjs +266 -99
  12. package/fesm2022/nanahoshi-mona-ui-internal.mjs +35 -32
  13. package/fesm2022/nanahoshi-mona-ui-locales.mjs +13 -0
  14. package/fesm2022/nanahoshi-mona-ui-numeric-text-box.mjs +27 -1
  15. package/fesm2022/nanahoshi-mona-ui-popup-menu.mjs +1 -1
  16. package/fesm2022/nanahoshi-mona-ui-tree-view.mjs +1 -1
  17. package/package.json +1 -1
  18. package/types/nanahoshi-mona-ui-i18n.d.ts +21 -4
  19. package/types/nanahoshi-mona-ui-internal-filter-input.d.ts +35 -1
  20. package/types/nanahoshi-mona-ui-internal-list.d.ts +82 -16
  21. package/types/nanahoshi-mona-ui-internal-tree.d.ts +50 -3
  22. package/types/nanahoshi-mona-ui-internal.d.ts +18 -4
  23. package/types/nanahoshi-mona-ui-locales.d.ts +13 -0
  24. package/types/nanahoshi-mona-ui-numeric-text-box.d.ts +5 -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, untracked, effect, Injectable, Pipe, inject, DestroyRef, input, ChangeDetectionStrategy, Component, DOCUMENT, NgZone, ElementRef, afterRenderEffect, contentChild, TemplateRef, afterNextRender } from '@angular/core';
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, contains, EnumerableSet, forEach } from '@mirei/ts-collections';
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
- const nodeSet = this.nodeSet();
162
- return untracked(() => this.computeVisibleUids(nodeSet, filterText));
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
- return String(node.data) || "";
377
+ text = node.data;
335
378
  }
336
- if (typeof textField === "string") {
337
- return node.data[textField];
379
+ else if (typeof textField === "string") {
380
+ text = node.data?.[textField];
338
381
  }
339
- return textField(node.data);
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 key = this.getDisableKey(node);
395
- const disabled = disabledKeys.contains(key);
439
+ const disabledKeys = this.disabledKeys();
440
+ if (disabledKeys.contains(this.getDisableKey(node))) {
441
+ return true;
442
+ }
396
443
  if (!disableOptions.disableChildren) {
397
- return disabled;
444
+ return false;
398
445
  }
399
- const anyParentDisabled = this.isAnyParentDisabled(node);
400
- return disabled || (disableOptions.disableChildren && anyParentDisabled);
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 hasCheckbox = this.#hasCheckboxPredicate();
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
- loadNodeChildren(node) {
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
- node.children.set(ImmutableSet.create());
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 checkedKeysSet = this.#nodeDictionary()
556
- .values()
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(checkedKeysSet.contains(n.value));
636
+ n.value.checked.set(keySet.has(this.getCheckKey(n.value)));
561
637
  });
562
- this.#checkedKeys.set(ImmutableSet.create(keys));
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 siblings = hasCheckbox !== null ? n.children().where(c => hasCheckbox(c.data, c.uid)) : n.children();
673
- const allChecked = siblings.all(c => c.checked());
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
- const childNodes = node.children();
789
- if (!childNodes.isEmpty()) {
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
- createHierarchicalNodes(root, parent, nodeDict) {
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() || !children) {
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 = nodeDict.firstOrDefault(n => n.value.data === dataItem);
859
- let node;
860
- if (existingNode) {
861
- node = existingNode.value;
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, nodeDict);
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, nodeDict);
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 nodeDictionary = this.#nodeDictionary();
896
- return this.createHierarchicalNodes(data, null, nodeDictionary) ?? ImmutableSet.create();
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
- let nextNode = navigableNodes
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
- let previousNode = navigableNodes.takeWhile(n => n !== navigatedNode).lastOrDefault();
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 childNodes = current.children();
1128
- if (childNodes.isEmpty()) {
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
- const expanded = this.treeService.isExpanded(node);
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(".mona-tree");
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' : null\"\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 [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 }); }
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' : null\"\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 [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"] }]
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 || !navigatedNode.nodeItem.hasChildren) {
2076
+ if (!navigatedNode) {
1973
2077
  return;
1974
2078
  }
1975
- const expanded = this.treeService.isExpanded(navigatedNode);
1976
- const disabled = this.treeService.isDisabled(navigatedNode);
1977
- if (!disabled && expanded) {
1978
- this.treeService.setNodeExpand(navigatedNode, false);
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 || !navigatedNode.nodeItem.hasChildren) {
2090
+ if (!navigatedNode || !this.treeService.hasChildren(navigatedNode)) {
1985
2091
  return;
1986
2092
  }
1987
- const expanded = this.treeService.isExpanded(navigatedNode);
1988
- const disabled = this.treeService.isDisabled(navigatedNode);
1989
- if (!disabled && !expanded) {
1990
- this.treeService.setNodeExpand(navigatedNode, true);
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) {