forty-cdk 0.2.0 → 0.3.0

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 (220) hide show
  1. package/accordion/README.md +122 -0
  2. package/aspect-ratio/README.md +76 -0
  3. package/avatar/README.md +100 -0
  4. package/breadcrumbs/README.md +49 -0
  5. package/breakpoints/README.md +81 -0
  6. package/button/README.md +49 -0
  7. package/calendar/README.md +458 -0
  8. package/carousel/README.md +358 -0
  9. package/checkbox/README.md +146 -0
  10. package/combobox/README.md +535 -0
  11. package/context-menu/README.md +139 -0
  12. package/date-field/README.md +184 -0
  13. package/date-picker/README.md +338 -0
  14. package/dialog/README.md +388 -0
  15. package/disclosure/README.md +114 -0
  16. package/drag-drop/README.md +359 -0
  17. package/drawer/README.md +560 -0
  18. package/dropdown-menu/README.md +176 -0
  19. package/fesm2022/forty-cdk-accordion.mjs +348 -0
  20. package/fesm2022/forty-cdk-accordion.mjs.map +1 -0
  21. package/fesm2022/forty-cdk-aspect-ratio.mjs +74 -0
  22. package/fesm2022/forty-cdk-aspect-ratio.mjs.map +1 -0
  23. package/fesm2022/forty-cdk-avatar.mjs +308 -0
  24. package/fesm2022/forty-cdk-avatar.mjs.map +1 -0
  25. package/fesm2022/forty-cdk-breadcrumbs.mjs +125 -0
  26. package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -0
  27. package/fesm2022/forty-cdk-breakpoints.mjs +117 -0
  28. package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -0
  29. package/fesm2022/forty-cdk-button.mjs +134 -0
  30. package/fesm2022/forty-cdk-button.mjs.map +1 -0
  31. package/fesm2022/forty-cdk-calendar.mjs +2034 -0
  32. package/fesm2022/forty-cdk-calendar.mjs.map +1 -0
  33. package/fesm2022/forty-cdk-carousel.mjs +968 -0
  34. package/fesm2022/forty-cdk-carousel.mjs.map +1 -0
  35. package/fesm2022/forty-cdk-checkbox.mjs +226 -0
  36. package/fesm2022/forty-cdk-checkbox.mjs.map +1 -0
  37. package/fesm2022/forty-cdk-combobox.mjs +2596 -0
  38. package/fesm2022/forty-cdk-combobox.mjs.map +1 -0
  39. package/fesm2022/forty-cdk-context-menu.mjs +413 -0
  40. package/fesm2022/forty-cdk-context-menu.mjs.map +1 -0
  41. package/fesm2022/forty-cdk-core.mjs +9022 -0
  42. package/fesm2022/forty-cdk-core.mjs.map +1 -0
  43. package/fesm2022/forty-cdk-date-field.mjs +744 -0
  44. package/fesm2022/forty-cdk-date-field.mjs.map +1 -0
  45. package/fesm2022/forty-cdk-date-picker.mjs +1011 -0
  46. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -0
  47. package/fesm2022/forty-cdk-dialog.mjs +707 -0
  48. package/fesm2022/forty-cdk-dialog.mjs.map +1 -0
  49. package/fesm2022/forty-cdk-disclosure.mjs +190 -0
  50. package/fesm2022/forty-cdk-disclosure.mjs.map +1 -0
  51. package/fesm2022/forty-cdk-drag-drop.mjs +1180 -0
  52. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -0
  53. package/fesm2022/forty-cdk-drawer.mjs +1641 -0
  54. package/fesm2022/forty-cdk-drawer.mjs.map +1 -0
  55. package/fesm2022/forty-cdk-dropdown-menu.mjs +350 -0
  56. package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -0
  57. package/fesm2022/forty-cdk-field.mjs +425 -0
  58. package/fesm2022/forty-cdk-field.mjs.map +1 -0
  59. package/fesm2022/forty-cdk-fieldset.mjs +164 -0
  60. package/fesm2022/forty-cdk-fieldset.mjs.map +1 -0
  61. package/fesm2022/forty-cdk-file-upload.mjs +221 -0
  62. package/fesm2022/forty-cdk-file-upload.mjs.map +1 -0
  63. package/fesm2022/forty-cdk-hover-card.mjs +496 -0
  64. package/fesm2022/forty-cdk-hover-card.mjs.map +1 -0
  65. package/fesm2022/forty-cdk-input.mjs +274 -0
  66. package/fesm2022/forty-cdk-input.mjs.map +1 -0
  67. package/fesm2022/forty-cdk-internationalized-date.mjs +1 -1
  68. package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
  69. package/fesm2022/forty-cdk-listbox.mjs +1279 -0
  70. package/fesm2022/forty-cdk-listbox.mjs.map +1 -0
  71. package/fesm2022/forty-cdk-menu.mjs +1439 -0
  72. package/fesm2022/forty-cdk-menu.mjs.map +1 -0
  73. package/fesm2022/forty-cdk-menubar.mjs +787 -0
  74. package/fesm2022/forty-cdk-menubar.mjs.map +1 -0
  75. package/fesm2022/forty-cdk-meter.mjs +211 -0
  76. package/fesm2022/forty-cdk-meter.mjs.map +1 -0
  77. package/fesm2022/forty-cdk-navigation-menu.mjs +1145 -0
  78. package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -0
  79. package/fesm2022/forty-cdk-number-input.mjs +559 -0
  80. package/fesm2022/forty-cdk-number-input.mjs.map +1 -0
  81. package/fesm2022/forty-cdk-otp-input.mjs +527 -0
  82. package/fesm2022/forty-cdk-otp-input.mjs.map +1 -0
  83. package/fesm2022/forty-cdk-pagination.mjs +323 -0
  84. package/fesm2022/forty-cdk-pagination.mjs.map +1 -0
  85. package/fesm2022/forty-cdk-pane-resizer.mjs +297 -0
  86. package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -0
  87. package/fesm2022/forty-cdk-popover.mjs +698 -0
  88. package/fesm2022/forty-cdk-popover.mjs.map +1 -0
  89. package/fesm2022/forty-cdk-progress.mjs +226 -0
  90. package/fesm2022/forty-cdk-progress.mjs.map +1 -0
  91. package/fesm2022/forty-cdk-radio-group.mjs +378 -0
  92. package/fesm2022/forty-cdk-radio-group.mjs.map +1 -0
  93. package/fesm2022/forty-cdk-scroll-area.mjs +640 -0
  94. package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -0
  95. package/fesm2022/forty-cdk-search.mjs +205 -0
  96. package/fesm2022/forty-cdk-search.mjs.map +1 -0
  97. package/fesm2022/forty-cdk-select.mjs +1661 -0
  98. package/fesm2022/forty-cdk-select.mjs.map +1 -0
  99. package/fesm2022/forty-cdk-separator.mjs +82 -0
  100. package/fesm2022/forty-cdk-separator.mjs.map +1 -0
  101. package/fesm2022/forty-cdk-signal-forms.mjs +97 -0
  102. package/fesm2022/forty-cdk-signal-forms.mjs.map +1 -0
  103. package/fesm2022/forty-cdk-slider.mjs +803 -0
  104. package/fesm2022/forty-cdk-slider.mjs.map +1 -0
  105. package/fesm2022/forty-cdk-stepper.mjs +886 -0
  106. package/fesm2022/forty-cdk-stepper.mjs.map +1 -0
  107. package/fesm2022/forty-cdk-switch.mjs +137 -0
  108. package/fesm2022/forty-cdk-switch.mjs.map +1 -0
  109. package/fesm2022/forty-cdk-table.mjs +1518 -0
  110. package/fesm2022/forty-cdk-table.mjs.map +1 -0
  111. package/fesm2022/forty-cdk-tabs.mjs +400 -0
  112. package/fesm2022/forty-cdk-tabs.mjs.map +1 -0
  113. package/fesm2022/forty-cdk-time-field.mjs +593 -0
  114. package/fesm2022/forty-cdk-time-field.mjs.map +1 -0
  115. package/fesm2022/forty-cdk-time-picker.mjs +1013 -0
  116. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -0
  117. package/fesm2022/forty-cdk-toast.mjs +1153 -0
  118. package/fesm2022/forty-cdk-toast.mjs.map +1 -0
  119. package/fesm2022/forty-cdk-toggle.mjs +516 -0
  120. package/fesm2022/forty-cdk-toggle.mjs.map +1 -0
  121. package/fesm2022/forty-cdk-toolbar.mjs +374 -0
  122. package/fesm2022/forty-cdk-toolbar.mjs.map +1 -0
  123. package/fesm2022/forty-cdk-tooltip.mjs +672 -0
  124. package/fesm2022/forty-cdk-tooltip.mjs.map +1 -0
  125. package/fesm2022/forty-cdk-tree.mjs +2007 -0
  126. package/fesm2022/forty-cdk-tree.mjs.map +1 -0
  127. package/fesm2022/forty-cdk-virtualization.mjs +1 -1
  128. package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
  129. package/fesm2022/forty-cdk.mjs +0 -43310
  130. package/fesm2022/forty-cdk.mjs.map +1 -1
  131. package/field/README.md +97 -0
  132. package/fieldset/README.md +86 -0
  133. package/file-upload/README.md +73 -0
  134. package/hover-card/README.md +171 -0
  135. package/input/README.md +156 -0
  136. package/listbox/README.md +424 -0
  137. package/menu/README.md +181 -0
  138. package/menubar/README.md +140 -0
  139. package/meter/README.md +128 -0
  140. package/navigation-menu/README.md +253 -0
  141. package/number-input/README.md +171 -0
  142. package/otp-input/README.md +198 -0
  143. package/package.json +213 -1
  144. package/pagination/README.md +61 -0
  145. package/pane-resizer/README.md +136 -0
  146. package/popover/README.md +262 -0
  147. package/progress/README.md +115 -0
  148. package/radio-group/README.md +129 -0
  149. package/scroll-area/README.md +184 -0
  150. package/search/README.md +42 -0
  151. package/select/README.md +488 -0
  152. package/separator/README.md +84 -0
  153. package/signal-forms/README.md +72 -0
  154. package/slider/README.md +152 -0
  155. package/stepper/README.md +292 -0
  156. package/switch/README.md +116 -0
  157. package/table/README.md +769 -0
  158. package/tabs/README.md +130 -0
  159. package/time-field/README.md +157 -0
  160. package/time-picker/README.md +172 -0
  161. package/toast/README.md +398 -0
  162. package/toggle/README.md +224 -0
  163. package/toolbar/README.md +109 -0
  164. package/tooltip/README.md +274 -0
  165. package/tree/README.md +708 -0
  166. package/types/forty-cdk-accordion.d.ts +242 -0
  167. package/types/forty-cdk-aspect-ratio.d.ts +59 -0
  168. package/types/forty-cdk-avatar.d.ts +133 -0
  169. package/types/forty-cdk-breadcrumbs.d.ts +92 -0
  170. package/types/forty-cdk-breakpoints.d.ts +141 -0
  171. package/types/forty-cdk-button.d.ts +80 -0
  172. package/types/forty-cdk-calendar.d.ts +914 -0
  173. package/types/forty-cdk-carousel.d.ts +530 -0
  174. package/types/forty-cdk-checkbox.d.ts +141 -0
  175. package/types/forty-cdk-combobox.d.ts +1259 -0
  176. package/types/forty-cdk-context-menu.d.ts +313 -0
  177. package/types/forty-cdk-core.d.ts +5774 -0
  178. package/types/forty-cdk-date-field.d.ts +307 -0
  179. package/types/forty-cdk-date-picker.d.ts +622 -0
  180. package/types/forty-cdk-dialog.d.ts +546 -0
  181. package/types/forty-cdk-disclosure.d.ts +127 -0
  182. package/types/forty-cdk-drag-drop.d.ts +456 -0
  183. package/types/forty-cdk-drawer.d.ts +871 -0
  184. package/types/forty-cdk-dropdown-menu.d.ts +242 -0
  185. package/types/forty-cdk-field.d.ts +236 -0
  186. package/types/forty-cdk-fieldset.d.ts +119 -0
  187. package/types/forty-cdk-file-upload.d.ts +124 -0
  188. package/types/forty-cdk-hover-card.d.ts +320 -0
  189. package/types/forty-cdk-input.d.ts +169 -0
  190. package/types/forty-cdk-internationalized-date.d.ts +1 -1
  191. package/types/forty-cdk-listbox.d.ts +513 -0
  192. package/types/forty-cdk-menu.d.ts +629 -0
  193. package/types/forty-cdk-menubar.d.ts +451 -0
  194. package/types/forty-cdk-meter.d.ts +122 -0
  195. package/types/forty-cdk-navigation-menu.d.ts +514 -0
  196. package/types/forty-cdk-number-input.d.ts +319 -0
  197. package/types/forty-cdk-otp-input.d.ts +248 -0
  198. package/types/forty-cdk-pagination.d.ts +214 -0
  199. package/types/forty-cdk-pane-resizer.d.ts +145 -0
  200. package/types/forty-cdk-popover.d.ts +509 -0
  201. package/types/forty-cdk-progress.d.ts +143 -0
  202. package/types/forty-cdk-radio-group.d.ts +222 -0
  203. package/types/forty-cdk-scroll-area.d.ts +258 -0
  204. package/types/forty-cdk-search.d.ts +142 -0
  205. package/types/forty-cdk-select.d.ts +899 -0
  206. package/types/forty-cdk-separator.d.ts +59 -0
  207. package/types/forty-cdk-signal-forms.d.ts +58 -0
  208. package/types/forty-cdk-slider.d.ts +379 -0
  209. package/types/forty-cdk-stepper.d.ts +650 -0
  210. package/types/forty-cdk-switch.d.ts +87 -0
  211. package/types/forty-cdk-table.d.ts +723 -0
  212. package/types/forty-cdk-tabs.d.ts +235 -0
  213. package/types/forty-cdk-time-field.d.ts +307 -0
  214. package/types/forty-cdk-time-picker.d.ts +578 -0
  215. package/types/forty-cdk-toast.d.ts +598 -0
  216. package/types/forty-cdk-toggle.d.ts +310 -0
  217. package/types/forty-cdk-toolbar.d.ts +217 -0
  218. package/types/forty-cdk-tooltip.d.ts +436 -0
  219. package/types/forty-cdk-tree.d.ts +688 -0
  220. package/types/forty-cdk.d.ts +1 -19743
@@ -0,0 +1,2007 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, ElementRef, model, input, booleanAttribute, numberAttribute, output, computed, signal, effect, Directive, DOCUMENT, PLATFORM_ID, DestroyRef } from '@angular/core';
3
+ import { moveIndex, firstEnabledHost, VirtualizedNavigator, createDefaults, injectTextDirection, RovingTabindex, injectTypeahead, Collection, resolveListNavigation, resolveTreeExpandCollapse, resolveLiftedDragControl, LiveAnnouncer, createKeyboardDragMediator, createPointerDragSession, isDragLiftKey, gapFromPointerY, levelFromPointerX, resolveTreeDrop, PreviewController, resolveDropIndicator, hostId, registerHandle } from 'forty-cdk/core';
4
+ import { isPlatformBrowser } from '@angular/common';
5
+
6
+ /**
7
+ * Focus engine for the standard (non-virtualized) tree: DOM focus rides the
8
+ * `treeitem`, tracked through {@link RovingTabindex}. The current node is the
9
+ * roving-active host; navigation walks the flattened visible-node list.
10
+ *
11
+ * Internal — not re-exported from `tree/index.ts` or `public-api.ts`.
12
+ */
13
+ class RovingFocusModel {
14
+ #deps;
15
+ constructor(deps) {
16
+ this.#deps = deps;
17
+ }
18
+ focusTarget(handle) {
19
+ this.#deps.roving.focusActive(handle.host);
20
+ }
21
+ current() {
22
+ const entry = this.#currentNode();
23
+ if (!entry) {
24
+ return null;
25
+ }
26
+ const handle = entry.handle;
27
+ return { value: handle.value(), expandable: handle.expandable(), disabled: handle.disabled() };
28
+ }
29
+ navigate(action) {
30
+ const active = this.#deps.roving.active();
31
+ const items = this.#deps.visibleHandles();
32
+ if (items.length === 0) {
33
+ return;
34
+ }
35
+ const currentIndex = items.findIndex((item) => item.host === active);
36
+ const next = moveIndex(currentIndex < 0 ? 0 : currentIndex, items.length, action, {
37
+ loop: false,
38
+ isDisabled: (i) => items[i].disabled(),
39
+ });
40
+ if (next === null) {
41
+ return;
42
+ }
43
+ const target = items[next];
44
+ if (!target) {
45
+ return;
46
+ }
47
+ this.#deps.roving.focusActive(target.host);
48
+ this.#deps.selectOnFocus(target.value());
49
+ }
50
+ enterChild() {
51
+ const entry = this.#currentNode();
52
+ if (!entry) {
53
+ return;
54
+ }
55
+ const child = entry.handle.childContainer();
56
+ const firstChild = child ? firstEnabledHost(child.items()) : null;
57
+ if (firstChild) {
58
+ this.#deps.roving.focusActive(firstChild);
59
+ }
60
+ }
61
+ moveToParent() {
62
+ const entry = this.#currentNode();
63
+ if (entry?.parentHost) {
64
+ this.#deps.roving.focusActive(entry.parentHost);
65
+ }
66
+ }
67
+ typeaheadTo(handle) {
68
+ this.#deps.roving.focusActive(handle.host);
69
+ }
70
+ #currentNode() {
71
+ const active = this.#deps.roving.active();
72
+ if (active === null) {
73
+ return null;
74
+ }
75
+ return this.#deps.visibleNodes().find((entry) => entry.handle.host === active) ?? null;
76
+ }
77
+ }
78
+ /**
79
+ * Focus engine for the virtualized tree: DOM focus stays on the container and
80
+ * an `aria-activedescendant` pointer tracks the active node. Owns the shared
81
+ * `_internal/virtualized-navigator` engine directly — a tree never wraps, so it
82
+ * pins `loop` to `false`, and its snapshot entry carries `level` / `expandable`
83
+ * / `value` so the tree-specific enter-child / go-to-parent moves can resolve
84
+ * levels outside the rendered window. Selection never follows focus here.
85
+ *
86
+ * Internal — not re-exported from `tree/index.ts` or `public-api.ts`.
87
+ */
88
+ class ActiveDescendantFocusModel {
89
+ #deps;
90
+ #core;
91
+ constructor(deps) {
92
+ this.#deps = deps;
93
+ this.#core = new VirtualizedNavigator({ ...deps, loop: () => false }, {
94
+ posOf: (n) => n.itemIndex(),
95
+ idOf: (n) => n.id(),
96
+ hostOf: (n) => n.host,
97
+ readEntry: (n) => ({
98
+ id: n.id(),
99
+ disabled: n.disabled(),
100
+ level: n.level(),
101
+ expandable: n.expandable(),
102
+ value: n.value(),
103
+ }),
104
+ });
105
+ }
106
+ /** @see VirtualizedNavigator.prime */
107
+ prime() {
108
+ this.#core.prime();
109
+ }
110
+ /** @see VirtualizedNavigator.tryResolvePending */
111
+ tryResolvePending() {
112
+ return this.#core.tryResolvePending();
113
+ }
114
+ focusTarget(handle) {
115
+ this.#deps.setActiveId(handle.id());
116
+ }
117
+ current() {
118
+ const cur = this.#currentEntry();
119
+ if (!cur) {
120
+ return null;
121
+ }
122
+ return { value: cur.value, expandable: cur.expandable, disabled: cur.disabled };
123
+ }
124
+ navigate(action) {
125
+ this.#core.navigate(action);
126
+ }
127
+ /**
128
+ * Move activedescendant to the first child of the current node (the node
129
+ * immediately after it in pre-order flat space). No-op when there is no
130
+ * active node or when the active node is the last in the list.
131
+ */
132
+ enterChild() {
133
+ const cur = this.#currentEntry();
134
+ if (!cur)
135
+ return;
136
+ const target = cur.pos + 1;
137
+ const total = this.#deps.totalCount();
138
+ if (total !== undefined && target < total) {
139
+ this.#core.seedActive(target);
140
+ }
141
+ }
142
+ /**
143
+ * Move activedescendant to the nearest preceding node at a shallower level
144
+ * (the parent). No-op when the active node has no visible parent in the
145
+ * snapshot — an accepted edge case when the parent has never been rendered.
146
+ */
147
+ moveToParent() {
148
+ const cur = this.#currentEntry();
149
+ if (!cur)
150
+ return;
151
+ const indexed = this.#core.snapshotByPos();
152
+ for (let p = cur.pos - 1; p >= 0; p--) {
153
+ const e = indexed.get(p);
154
+ if (e && e.level < cur.level) {
155
+ this.#core.seedActive(p);
156
+ return;
157
+ }
158
+ }
159
+ }
160
+ typeaheadTo(handle) {
161
+ this.#deps.setActiveId(handle.id());
162
+ handle.host.scrollIntoView?.({ block: 'nearest' });
163
+ }
164
+ /**
165
+ * Resolve the active node's position entry from live items first, then the
166
+ * snapshot. Returns `{ pos, value, level, expandable, disabled }` or `null`
167
+ * when nothing is active.
168
+ */
169
+ #currentEntry() {
170
+ const currentId = this.#deps.getActiveId();
171
+ if (currentId === null) {
172
+ return null;
173
+ }
174
+ const live = this.#deps.items().find((o) => o.id() === currentId);
175
+ if (live) {
176
+ const pos = live.itemIndex();
177
+ if (pos !== null) {
178
+ return {
179
+ pos,
180
+ value: live.value(),
181
+ level: live.level(),
182
+ expandable: live.expandable(),
183
+ disabled: live.disabled(),
184
+ };
185
+ }
186
+ }
187
+ const indexed = this.#core.snapshotByPos();
188
+ for (const [pos, entry] of indexed) {
189
+ if (entry.id === currentId) {
190
+ return {
191
+ pos,
192
+ value: entry.value,
193
+ level: entry.level,
194
+ expandable: entry.expandable,
195
+ disabled: entry.disabled,
196
+ };
197
+ }
198
+ }
199
+ return null;
200
+ }
201
+ }
202
+
203
+ const FOR_TREE_CONTEXT = new InjectionToken('FOR_TREE_CONTEXT');
204
+ const FOR_TREE_CONTAINER_CONTEXT = new InjectionToken('FOR_TREE_CONTAINER_CONTEXT');
205
+ const FOR_TREE_ITEM_CONTEXT = new InjectionToken('FOR_TREE_ITEM_CONTEXT');
206
+ /** Injects the nearest {@link ForTreeContext}, throwing a prefixed error if absent. */
207
+ function injectTreeContext(piece) {
208
+ const ctx = inject(FOR_TREE_CONTEXT, { optional: true });
209
+ if (!ctx) {
210
+ throw new Error(`[forty-cdk/tree] ${piece} must be used inside a [forTree] element.`);
211
+ }
212
+ return ctx;
213
+ }
214
+ /** Injects the nearest {@link ForTreeContainerContext} (the root or a group). */
215
+ function injectTreeContainerContext(piece) {
216
+ const ctx = inject(FOR_TREE_CONTAINER_CONTEXT, { optional: true });
217
+ if (!ctx) {
218
+ throw new Error(`[forty-cdk/tree] ${piece} must be used inside a [forTree] or [forTreeGroup] element.`);
219
+ }
220
+ return ctx;
221
+ }
222
+ /** Injects the nearest enclosing {@link ForTreeItemContext}. */
223
+ function injectTreeItemContext(piece) {
224
+ const ctx = inject(FOR_TREE_ITEM_CONTEXT, { optional: true });
225
+ if (!ctx) {
226
+ throw new Error(`[forty-cdk/tree] ${piece} must be used inside a [forTreeItem] element.`);
227
+ }
228
+ return ctx;
229
+ }
230
+
231
+ /**
232
+ * Library fallback for tree defaults, read at the root injector when no
233
+ * consumer has called `provideForTreeDefaults`. Exported for the shared
234
+ * defaults contract spec; not re-exported from the primitive's public entry.
235
+ */
236
+ const FOR_TREE_FALLBACK_DEFAULTS = {
237
+ selectionFollowsFocus: false,
238
+ };
239
+ const { token, provideDefaults } = createDefaults('FOR_TREE_DEFAULTS', FOR_TREE_FALLBACK_DEFAULTS);
240
+ /** Token holding the resolved tree defaults for the current scope. */
241
+ const FOR_TREE_DEFAULTS = token;
242
+ /**
243
+ * Configures forty-cdk tree defaults for this injector scope. Partial
244
+ * overrides inherit unspecified keys from the parent scope (or library
245
+ * defaults at the root).
246
+ */
247
+ function provideForTreeDefaults(defaults = {}) {
248
+ return provideDefaults(defaults);
249
+ }
250
+
251
+ /**
252
+ * Selection + checkbox-cascade engine for `ForTree`, extracted from the root so
253
+ * the directive keeps only its reactive wiring and ARIA. Owns single / multi /
254
+ * checkbox selection, cascade tri-state derivation, range selection, select-all,
255
+ * and the `*`-key sibling expansion — every write goes back through the deps'
256
+ * model setters so `ForTree` stays the single source of truth.
257
+ *
258
+ * Internal — not re-exported from `tree/index.ts` or `public-api.ts`.
259
+ */
260
+ class TreeSelection {
261
+ #deps;
262
+ constructor(deps) {
263
+ this.#deps = deps;
264
+ }
265
+ checkState(value) {
266
+ const current = this.#deps.value();
267
+ if (this.#deps.selectionMode() !== 'checkbox' || !this.#deps.cascade()) {
268
+ return current.includes(value) ? 'true' : 'false';
269
+ }
270
+ const descendants = this.#resolveDescendants(value);
271
+ if (descendants.length === 0) {
272
+ return current.includes(value) ? 'true' : 'false';
273
+ }
274
+ const selected = new Set(current);
275
+ let checked = 0;
276
+ for (const d of descendants) {
277
+ if (selected.has(d)) {
278
+ checked += 1;
279
+ }
280
+ }
281
+ if (checked === 0) {
282
+ return 'false';
283
+ }
284
+ return checked === descendants.length ? 'true' : 'mixed';
285
+ }
286
+ select(value) {
287
+ if (this.#deps.disabled()) {
288
+ return;
289
+ }
290
+ if (this.#deps.selectionMode() === 'checkbox' && this.#deps.cascade()) {
291
+ const group = new Set([value, ...this.#resolveDescendants(value)]);
292
+ const current = this.#deps.value();
293
+ const allChecked = [...group].every((v) => current.includes(v));
294
+ this.#deps.setValue(allChecked ? current.filter((v) => !group.has(v)) : [...new Set([...current, ...group])]);
295
+ }
296
+ else if (this.#deps.multiple() || this.#deps.selectionMode() === 'checkbox') {
297
+ const current = this.#deps.value();
298
+ this.#deps.setValue(current.includes(value) ? current.filter((v) => v !== value) : [...current, value]);
299
+ }
300
+ else {
301
+ this.#deps.setValue([value]);
302
+ }
303
+ this.#deps.setAnchorValue(value);
304
+ }
305
+ expandSiblings(currentItem) {
306
+ if (this.#deps.disabled()) {
307
+ return;
308
+ }
309
+ const entries = this.#deps.visibleNodes();
310
+ const current = entries.find((e) => e.handle.host === currentItem);
311
+ if (!current) {
312
+ return;
313
+ }
314
+ const next = [...this.#deps.expanded()];
315
+ for (const entry of entries) {
316
+ if (entry.parentHost === current.parentHost &&
317
+ entry.handle.expandable() &&
318
+ !next.includes(entry.handle.value())) {
319
+ next.push(entry.handle.value());
320
+ }
321
+ }
322
+ this.#deps.setExpanded(next);
323
+ }
324
+ extendByArrow(currentItem, action) {
325
+ if (this.#deps.disabled() || !this.#deps.multiple()) {
326
+ return;
327
+ }
328
+ const items = this.#deps.visibleHandles();
329
+ if (items.length === 0) {
330
+ return;
331
+ }
332
+ const currentIndex = items.findIndex((item) => item.host === currentItem);
333
+ const next = moveIndex(currentIndex < 0 ? 0 : currentIndex, items.length, action, {
334
+ loop: false,
335
+ isDisabled: (i) => items[i].disabled(),
336
+ });
337
+ if (next === null) {
338
+ return;
339
+ }
340
+ const target = items[next];
341
+ if (!target) {
342
+ return;
343
+ }
344
+ // Establish the range anchor at the origin of the shift-extend run so a
345
+ // following Shift+Space ranges from where the user started extending, not
346
+ // from a stale (or absent) anchor. A pre-existing anchor (e.g. from a prior
347
+ // click) is left in place, matching the listbox range contract.
348
+ if (this.#deps.anchorValue() === null && currentIndex >= 0) {
349
+ this.#deps.setAnchorValue(items[currentIndex].value());
350
+ }
351
+ this.#deps.roving.focusActive(target.host);
352
+ const value = target.value();
353
+ const current = this.#deps.value();
354
+ this.#deps.setValue(current.includes(value) ? current.filter((v) => v !== value) : [...current, value]);
355
+ }
356
+ selectRangeToFocused(currentItem) {
357
+ if (this.#deps.disabled() || !this.#deps.multiple()) {
358
+ return;
359
+ }
360
+ const items = this.#deps.visibleHandles();
361
+ const currentIndex = items.findIndex((item) => item.host === currentItem);
362
+ if (currentIndex < 0) {
363
+ return;
364
+ }
365
+ const anchorValue = this.#deps.anchorValue();
366
+ const anchorIndex = anchorValue === null ? currentIndex : items.findIndex((i) => i.value() === anchorValue);
367
+ const start = anchorIndex < 0 ? currentIndex : anchorIndex;
368
+ const [lo, hi] = start <= currentIndex ? [start, currentIndex] : [currentIndex, start];
369
+ const next = [...this.#deps.value()];
370
+ for (let i = lo; i <= hi; i++) {
371
+ const item = items[i];
372
+ if (!item || item.disabled()) {
373
+ continue;
374
+ }
375
+ const value = item.value();
376
+ if (!next.includes(value)) {
377
+ next.push(value);
378
+ }
379
+ }
380
+ this.#deps.setValue(next);
381
+ }
382
+ selectAll() {
383
+ if (this.#deps.disabled() || !this.#deps.multiple()) {
384
+ return;
385
+ }
386
+ const values = this.#deps
387
+ .visibleNodes()
388
+ .map((entry) => entry.handle)
389
+ .filter((handle) => !handle.disabled())
390
+ .map((handle) => handle.value());
391
+ if (values.length === 0) {
392
+ return;
393
+ }
394
+ const current = this.#deps.value();
395
+ const allSelected = values.every((v) => current.includes(v));
396
+ this.#deps.setValue(allSelected ? [] : [...new Set([...current, ...values])]);
397
+ }
398
+ #resolveDescendants(value) {
399
+ const fn = this.#deps.descendantsOf();
400
+ if (!fn) {
401
+ throw new Error('[forty-cdk/tree] `cascade` requires a `descendantsOf` descriptor returning the descendant values of a node.');
402
+ }
403
+ return fn(value);
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Headless implementation of the
409
+ * [WAI-ARIA Tree View pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/).
410
+ *
411
+ * A nested tree (`role="tree"` → `treeitem` → `group` → `treeitem`) with
412
+ * `@if`-driven expansion, roving-tabindex focus management (APG Approach A —
413
+ * DOM focus rides the `treeitem`), typeahead, RTL arrow mirroring, and full
414
+ * `aria-level` / `aria-setsize` / `aria-posinset` wiring.
415
+ *
416
+ * Two orthogonal models:
417
+ * - `value` — selected node values; single mode (default) keeps 0 or 1
418
+ * element, multi mode accumulates.
419
+ * - `expanded` — open parent node values; always multi (no single mode).
420
+ *
421
+ * Single-select consumers read the sole value through {@link ForTree.selected}
422
+ * instead of unwrapping `value()[0]`.
423
+ *
424
+ * @example
425
+ * ```html
426
+ * <ul forTree [(value)]="selected" [(expanded)]="expanded" aria-label="Files">
427
+ * <ng-container [ngTemplateOutlet]="node" [ngTemplateOutletContext]="{ $implicit: root }" />
428
+ * </ul>
429
+ * ```
430
+ */
431
+ class ForTree {
432
+ #defaults = inject(FOR_TREE_DEFAULTS);
433
+ #host = inject(ElementRef);
434
+ /**
435
+ * Two-way bindable. Selected node values. Single mode keeps the array at
436
+ * length <= 1. The `model()` change emitter (`(valueChange)`) fires only on
437
+ * internal selection changes (node activation or `selectionFollowsFocus`
438
+ * navigation), never on consumer writes via `[(value)]`.
439
+ */
440
+ value = model([], /* @ts-ignore */
441
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
442
+ /**
443
+ * Two-way bindable. Open (expanded) parent node values. Always multi — any
444
+ * number of nodes can be open. The `model()` change emitter
445
+ * (`(expandedChange)`) fires only on internal expand / collapse, never on
446
+ * consumer writes via `[(expanded)]`.
447
+ */
448
+ expanded = model([], /* @ts-ignore */
449
+ ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
450
+ /** When true, multiple nodes can be selected. Single mode (default) replaces. */
451
+ multiple = input(false, { ...(ngDevMode ? { debugName: "multiple" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
452
+ /** Disables the whole tree: nodes are not selectable and report `aria-disabled`. */
453
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
454
+ /** Navigation axis. `'vertical'` (default) uses ArrowUp/Down for movement. */
455
+ orientation = input('vertical', /* @ts-ignore */
456
+ ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
457
+ /**
458
+ * Selection presentation. `'highlight'` (default) keeps the `aria-selected`
459
+ * contract; `'checkbox'` switches each `treeitem` to `aria-checked` and is
460
+ * inherently multi-select (each node toggles independently).
461
+ */
462
+ selectionMode = input('highlight', /* @ts-ignore */
463
+ ...(ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []));
464
+ /**
465
+ * Enables cascade selection in `selectionMode="checkbox"`: checking or
466
+ * unchecking a node propagates to all its descendants, and a parent derives
467
+ * `aria-checked="mixed"` when only some descendants are checked. Ignored in
468
+ * `'highlight'` mode. Requires {@link ForTree.descendantsOf}. Default `false`.
469
+ */
470
+ cascade = input(false, { ...(ngDevMode ? { debugName: "cascade" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
471
+ /**
472
+ * Returns the selectable descendant values of a node (excluding the node
473
+ * itself), used to cascade selection and derive `'mixed'` across collapsed —
474
+ * possibly unmounted — subtrees. **Required** when {@link ForTree.cascade} is
475
+ * `true`; the tree throws a `[forty-cdk/tree]` error otherwise.
476
+ */
477
+ descendantsOf = input(/* @ts-ignore */
478
+ ...(ngDevMode ? [undefined, { debugName: "descendantsOf" }] : /* istanbul ignore next */ []));
479
+ /**
480
+ * Total number of nodes in the flattened visible-node list. When set, enables
481
+ * the virtualized activedescendant focus model. Leave unset (default
482
+ * `undefined`) for the standard roving-tabindex model.
483
+ */
484
+ totalCount = input(undefined, { ...(ngDevMode ? { debugName: "totalCount" } : /* istanbul ignore next */ {}), transform: (v) => (v == null ? undefined : numberAttribute(v)) });
485
+ /**
486
+ * Inclusive-exclusive `[start, end)` index range of the currently rendered
487
+ * nodes. The virtualizer provides this; the tree uses it to decide whether a
488
+ * navigation target is in the visible window.
489
+ */
490
+ visibleRange = input(undefined, /* @ts-ignore */
491
+ ...(ngDevMode ? [{ debugName: "visibleRange" }] : /* istanbul ignore next */ []));
492
+ /**
493
+ * Emitted when keyboard navigation reaches a node outside the rendered
494
+ * window. The consumer passes this index to `injectVirtualizer`'s
495
+ * `scrollToIndex` so the correct node mounts.
496
+ */
497
+ scrollToIndex = output();
498
+ /**
499
+ * Manual `aria-label` for the tree. Use this when no visible label element
500
+ * exists; otherwise prefer pointing `aria-labelledby` at one. A `null`
501
+ * (default) or empty value emits no attribute.
502
+ */
503
+ ariaLabel = input(null, /* @ts-ignore */
504
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
505
+ /**
506
+ * Writing direction. When unset (default `null`), the inherited ambient
507
+ * direction is resolved from the nearest ancestor carrying a `dir` attribute
508
+ * (or `<html dir>`), defaulting to `'ltr'`. An explicit `[dir]` always wins.
509
+ * The resolved value is reflected to the host `dir` attribute and swaps the
510
+ * expand / collapse arrow semantics in RTL.
511
+ */
512
+ _dirInput = input(null, { ...(ngDevMode ? { debugName: "_dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
513
+ dir = injectTextDirection(this._dirInput);
514
+ /**
515
+ * Single-mode only: when true, arrow navigation also selects the focused
516
+ * node. The default is read from `provideForTreeDefaults` for the
517
+ * surrounding scope.
518
+ */
519
+ selectionFollowsFocus = input(this.#defaults.selectionFollowsFocus, { ...(ngDevMode ? { debugName: "selectionFollowsFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
520
+ /**
521
+ * Read-only single-select convenience view of {@link value}. Returns the
522
+ * sole selected value when exactly one node is selected, otherwise `null`
523
+ * (empty selection, or multiple selections in `multiple` mode).
524
+ */
525
+ selected = computed(() => {
526
+ const v = this.value();
527
+ return v.length === 1 ? v[0] : null;
528
+ }, /* @ts-ignore */
529
+ ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
530
+ /** Root container hosts level-1 items. */
531
+ level = signal(1, /* @ts-ignore */
532
+ ...(ngDevMode ? [{ debugName: "level" }] : /* istanbul ignore next */ []));
533
+ roving = new RovingTabindex();
534
+ #typeahead = injectTypeahead();
535
+ #items = new Collection();
536
+ #anchorValue = signal(null, /* @ts-ignore */
537
+ ...(ngDevMode ? [{ debugName: "#anchorValue" }] : /* istanbul ignore next */ []));
538
+ items = this.#items.items;
539
+ #visibleEntries = computed(() => {
540
+ const expanded = this.expanded();
541
+ const result = [];
542
+ const walk = (container, parentHost) => {
543
+ for (const handle of container.items()) {
544
+ result.push({ handle, parentHost });
545
+ if (expanded.includes(handle.value())) {
546
+ const child = handle.childContainer();
547
+ if (child) {
548
+ walk(child, handle.host);
549
+ }
550
+ }
551
+ }
552
+ };
553
+ walk(this, null);
554
+ return result;
555
+ }, /* @ts-ignore */
556
+ ...(ngDevMode ? [{ debugName: "#visibleEntries" }] : /* istanbul ignore next */ []));
557
+ /**
558
+ * Flattened currently-visible nodes in DOM order, each with its resolved parent host. Exposed for
559
+ * drag-drop composition (`[forTreeNodeDrag]`). Reflects expansion: collapsed subtrees are absent.
560
+ */
561
+ visibleNodes = this.#visibleEntries;
562
+ #visibleHandles = computed(() => this.#visibleEntries().map((entry) => entry.handle), /* @ts-ignore */
563
+ ...(ngDevMode ? [{ debugName: "#visibleHandles" }] : /* istanbul ignore next */ []));
564
+ #firstEnabledRoot = computed(() => firstEnabledHost(this.#items.items()), /* @ts-ignore */
565
+ ...(ngDevMode ? [{ debugName: "#firstEnabledRoot" }] : /* istanbul ignore next */ []));
566
+ #virtualized = computed(() => this.totalCount() !== undefined, /* @ts-ignore */
567
+ ...(ngDevMode ? [{ debugName: "#virtualized" }] : /* istanbul ignore next */ []));
568
+ #activeId = signal(null, /* @ts-ignore */
569
+ ...(ngDevMode ? [{ debugName: "#activeId" }] : /* istanbul ignore next */ []));
570
+ /**
571
+ * The active node's `id` when using the activedescendant focus model,
572
+ * `null` in the roving-tabindex path. The host reflects this as
573
+ * `aria-activedescendant`; items read it to compute `data-highlighted`.
574
+ */
575
+ activeDescendantId = computed(() => this.#virtualized() ? this.#activeId() : null, /* @ts-ignore */
576
+ ...(ngDevMode ? [{ debugName: "activeDescendantId" }] : /* istanbul ignore next */ []));
577
+ /**
578
+ * Tabindex for the tree host. In the virtualized path the host is always the
579
+ * single tab stop. In the roving path the host carries no tabindex (items own
580
+ * their own tab stop). A disabled tree is never tabbable.
581
+ */
582
+ hostTabindex = computed(() => {
583
+ if (this.disabled())
584
+ return null;
585
+ return this.#virtualized() ? '0' : null;
586
+ }, /* @ts-ignore */
587
+ ...(ngDevMode ? [{ debugName: "hostTabindex" }] : /* istanbul ignore next */ []));
588
+ #selection = new TreeSelection({
589
+ value: this.value,
590
+ expanded: this.expanded,
591
+ multiple: this.multiple,
592
+ disabled: this.disabled,
593
+ selectionMode: this.selectionMode,
594
+ cascade: this.cascade,
595
+ descendantsOf: this.descendantsOf,
596
+ visibleNodes: this.#visibleEntries,
597
+ visibleHandles: this.#visibleHandles,
598
+ roving: this.roving,
599
+ setValue: (next) => this.value.set(next),
600
+ setExpanded: (next) => this.expanded.set(next),
601
+ anchorValue: () => this.#anchorValue(),
602
+ setAnchorValue: (value) => this.#anchorValue.set(value),
603
+ });
604
+ #rovingModel = null;
605
+ #activeDescendantModel = null;
606
+ #requireActiveDescendantModel() {
607
+ return (this.#activeDescendantModel ??= new ActiveDescendantFocusModel({
608
+ items: this.#items.items,
609
+ totalCount: this.totalCount,
610
+ visibleRange: this.visibleRange,
611
+ getActiveId: () => this.#activeId(),
612
+ setActiveId: (id) => this.#activeId.set(id),
613
+ emitScrollToIndex: (idx) => this.scrollToIndex.emit(idx),
614
+ }));
615
+ }
616
+ #focusModel() {
617
+ if (this.#virtualized()) {
618
+ return this.#requireActiveDescendantModel();
619
+ }
620
+ return (this.#rovingModel ??= new RovingFocusModel({
621
+ roving: this.roving,
622
+ visibleNodes: this.#visibleEntries,
623
+ visibleHandles: this.#visibleHandles,
624
+ selectOnFocus: (value) => {
625
+ if (!this.multiple() && this.selectionFollowsFocus()) {
626
+ this.value.set([value]);
627
+ this.#anchorValue.set(value);
628
+ }
629
+ },
630
+ }));
631
+ }
632
+ constructor() {
633
+ effect(() => {
634
+ if (this.#virtualized())
635
+ return;
636
+ const active = this.roving.active();
637
+ if (active === null) {
638
+ return;
639
+ }
640
+ const visible = this.#visibleEntries();
641
+ const entry = visible.find((e) => e.handle.host === active);
642
+ if (entry && !entry.handle.disabled() && active.isConnected) {
643
+ return;
644
+ }
645
+ const fallback = visible.find((e) => !e.handle.disabled());
646
+ this.roving.setActive(fallback?.handle.host ?? null);
647
+ });
648
+ effect(() => {
649
+ this.#items.items();
650
+ if (!this.#virtualized())
651
+ return;
652
+ const model = this.#requireActiveDescendantModel();
653
+ model.prime();
654
+ model.tryResolvePending();
655
+ });
656
+ }
657
+ isExpanded(value) {
658
+ return this.expanded().includes(value);
659
+ }
660
+ isSelected(value) {
661
+ return this.value().includes(value);
662
+ }
663
+ /**
664
+ * Tri-state check status of a node in `selectionMode="checkbox"`. Without
665
+ * cascade (or in `'highlight'` mode) returns `'true'` / `'false'` by direct
666
+ * membership. With cascade a parent returns `'true'` when all its descendants
667
+ * are checked, `'false'` when none are, and `'mixed'` otherwise.
668
+ */
669
+ checkState(value) {
670
+ return this.#selection.checkState(value);
671
+ }
672
+ setExpanded(value, open) {
673
+ const current = this.expanded();
674
+ const has = current.includes(value);
675
+ if (open && !has) {
676
+ this.expanded.set([...current, value]);
677
+ }
678
+ else if (!open && has) {
679
+ this.#relocateActiveOnCollapse(value);
680
+ this.expanded.set(current.filter((v) => v !== value));
681
+ }
682
+ }
683
+ select(value) {
684
+ this.#selection.select(value);
685
+ }
686
+ #relocateActiveOnCollapse(value) {
687
+ const active = this.roving.active();
688
+ if (active === null) {
689
+ return;
690
+ }
691
+ const visible = this.#visibleEntries();
692
+ const collapsing = visible.find((e) => e.handle.value() === value);
693
+ if (!collapsing) {
694
+ return;
695
+ }
696
+ const collapsingHost = collapsing.handle.host;
697
+ if (collapsingHost !== active && collapsingHost.contains(active)) {
698
+ this.roving.focusActive(collapsingHost);
699
+ }
700
+ }
701
+ navigate(_currentItem, action) {
702
+ if (this.disabled()) {
703
+ return;
704
+ }
705
+ this.#focusModel().navigate(action);
706
+ }
707
+ expandOrEnter(_currentItem) {
708
+ if (this.disabled()) {
709
+ return;
710
+ }
711
+ const model = this.#focusModel();
712
+ const cur = model.current();
713
+ if (!cur || cur.disabled || !cur.expandable) {
714
+ return;
715
+ }
716
+ if (!this.isExpanded(cur.value)) {
717
+ this.setExpanded(cur.value, true);
718
+ return;
719
+ }
720
+ model.enterChild();
721
+ }
722
+ collapseOrLeave(_currentItem) {
723
+ if (this.disabled()) {
724
+ return;
725
+ }
726
+ const model = this.#focusModel();
727
+ const cur = model.current();
728
+ if (!cur) {
729
+ return;
730
+ }
731
+ if (cur.expandable && this.isExpanded(cur.value)) {
732
+ this.setExpanded(cur.value, false);
733
+ return;
734
+ }
735
+ model.moveToParent();
736
+ }
737
+ expandSiblings(currentItem) {
738
+ this.#selection.expandSiblings(currentItem);
739
+ }
740
+ extendByArrow(currentItem, action) {
741
+ this.#selection.extendByArrow(currentItem, action);
742
+ }
743
+ selectRangeToFocused(currentItem) {
744
+ this.#selection.selectRangeToFocused(currentItem);
745
+ }
746
+ selectAll() {
747
+ this.#selection.selectAll();
748
+ }
749
+ handleTypeahead(event) {
750
+ if (!this.#typeahead.handle(event)) {
751
+ return false;
752
+ }
753
+ const buffer = this.#typeahead.buffer().toLowerCase();
754
+ if (!buffer) {
755
+ return true;
756
+ }
757
+ const source = this.#virtualized()
758
+ ? this.#items.items()
759
+ : this.#visibleEntries().map((entry) => entry.handle);
760
+ const match = source.find((handle) => {
761
+ if (handle.disabled()) {
762
+ return false;
763
+ }
764
+ const text = (handle.textValue() || handle.labelEl()?.textContent || '').trim().toLowerCase();
765
+ return text.startsWith(buffer);
766
+ });
767
+ if (match) {
768
+ this.#focusModel().typeaheadTo(match);
769
+ }
770
+ return true;
771
+ }
772
+ isFirstFocusableItem(el) {
773
+ return this.#firstEnabledRoot() === el;
774
+ }
775
+ onHostKeyDown(event) {
776
+ if (!this.#virtualized() || this.disabled())
777
+ return;
778
+ const host = this.#host.nativeElement;
779
+ if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {
780
+ event.preventDefault();
781
+ this.#activateActiveDescendant();
782
+ return;
783
+ }
784
+ const action = resolveListNavigation(event, {
785
+ orientation: this.orientation(),
786
+ dir: this.dir(),
787
+ });
788
+ if (action === 'next' || action === 'prev' || action === 'first' || action === 'last') {
789
+ event.preventDefault();
790
+ this.navigate(host, action);
791
+ return;
792
+ }
793
+ const intent = resolveTreeExpandCollapse(event, {
794
+ orientation: this.orientation(),
795
+ dir: this.dir(),
796
+ });
797
+ if (intent === 'expand') {
798
+ event.preventDefault();
799
+ this.expandOrEnter(host);
800
+ return;
801
+ }
802
+ if (intent === 'collapse') {
803
+ event.preventDefault();
804
+ this.collapseOrLeave(host);
805
+ return;
806
+ }
807
+ this.handleTypeahead(event);
808
+ }
809
+ onHostFocusIn() {
810
+ if (!this.#virtualized() || this.disabled())
811
+ return;
812
+ if (this.#activeId() !== null)
813
+ return;
814
+ const items = this.#items.items();
815
+ if (items.length === 0)
816
+ return;
817
+ const ordered = [...items].sort((a, b) => (a.itemIndex() ?? 0) - (b.itemIndex() ?? 0));
818
+ const value = this.value();
819
+ const selectedFirst = ordered.find((h) => !h.disabled() && value.includes(h.value()));
820
+ const target = selectedFirst ?? ordered.find((h) => !h.disabled());
821
+ if (target)
822
+ this.#activeId.set(target.id());
823
+ }
824
+ #activateActiveDescendant() {
825
+ const id = this.#activeId();
826
+ if (id === null)
827
+ return;
828
+ const handle = this.#items.items().find((o) => o.id() === id);
829
+ if (!handle || handle.disabled())
830
+ return;
831
+ this.select(handle.value());
832
+ }
833
+ registerItem(handle) {
834
+ this.#items.register(handle);
835
+ }
836
+ notifyItemClick(itemId) {
837
+ if (!this.#virtualized())
838
+ return;
839
+ this.#activeId.set(itemId);
840
+ this.#host.nativeElement.focus();
841
+ }
842
+ unregisterItem(handle) {
843
+ this.#items.unregister(handle);
844
+ this.roving.unregister(handle.host);
845
+ if (this.#virtualized() && this.#activeId() === handle.id()) {
846
+ this.#activeId.set(null);
847
+ }
848
+ }
849
+ indexOfHost(el) {
850
+ return this.#items.indexOfHost(el);
851
+ }
852
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForTree, deps: [], target: i0.ɵɵFactoryTarget.Directive });
853
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForTree, isStandalone: true, selector: "[forTree]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, expanded: { classPropertyName: "expanded", publicName: "expanded", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, cascade: { classPropertyName: "cascade", publicName: "cascade", isSignal: true, isRequired: false, transformFunction: null }, descendantsOf: { classPropertyName: "descendantsOf", publicName: "descendantsOf", isSignal: true, isRequired: false, transformFunction: null }, totalCount: { classPropertyName: "totalCount", publicName: "totalCount", isSignal: true, isRequired: false, transformFunction: null }, visibleRange: { classPropertyName: "visibleRange", publicName: "visibleRange", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, _dirInput: { classPropertyName: "_dirInput", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null }, selectionFollowsFocus: { classPropertyName: "selectionFollowsFocus", publicName: "selectionFollowsFocus", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", expanded: "expandedChange", scrollToIndex: "scrollToIndex" }, host: { attributes: { "role": "tree" }, listeners: { "keydown": "onHostKeyDown($event)", "focusin": "onHostFocusIn()" }, properties: { "attr.aria-label": "ariaLabel() || null", "attr.aria-multiselectable": "multiple() ? \"true\" : null", "attr.aria-orientation": "orientation()", "attr.aria-disabled": "disabled() ? \"true\" : null", "attr.data-orientation": "orientation()", "attr.data-disabled": "disabled() ? \"\" : null", "attr.dir": "dir()", "attr.aria-activedescendant": "activeDescendantId()", "attr.tabindex": "hostTabindex()" } }, providers: [
854
+ { provide: FOR_TREE_CONTEXT, useExisting: ForTree },
855
+ { provide: FOR_TREE_CONTAINER_CONTEXT, useExisting: ForTree },
856
+ ], exportAs: ["forTree"], ngImport: i0 });
857
+ }
858
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForTree, decorators: [{
859
+ type: Directive,
860
+ args: [{
861
+ selector: '[forTree]',
862
+ exportAs: 'forTree',
863
+ host: {
864
+ role: 'tree',
865
+ '[attr.aria-label]': 'ariaLabel() || null',
866
+ '[attr.aria-multiselectable]': 'multiple() ? "true" : null',
867
+ '[attr.aria-orientation]': 'orientation()',
868
+ '[attr.aria-disabled]': 'disabled() ? "true" : null',
869
+ '[attr.data-orientation]': 'orientation()',
870
+ '[attr.data-disabled]': 'disabled() ? "" : null',
871
+ '[attr.dir]': 'dir()',
872
+ '[attr.aria-activedescendant]': 'activeDescendantId()',
873
+ '[attr.tabindex]': 'hostTabindex()',
874
+ '(keydown)': 'onHostKeyDown($event)',
875
+ '(focusin)': 'onHostFocusIn()',
876
+ },
877
+ providers: [
878
+ { provide: FOR_TREE_CONTEXT, useExisting: ForTree },
879
+ { provide: FOR_TREE_CONTAINER_CONTEXT, useExisting: ForTree },
880
+ ],
881
+ }]
882
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], expanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "expanded", required: false }] }, { type: i0.Output, args: ["expandedChange"] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], cascade: [{ type: i0.Input, args: [{ isSignal: true, alias: "cascade", required: false }] }], descendantsOf: [{ type: i0.Input, args: [{ isSignal: true, alias: "descendantsOf", required: false }] }], totalCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalCount", required: false }] }], visibleRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "visibleRange", required: false }] }], scrollToIndex: [{ type: i0.Output, args: ["scrollToIndex"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], _dirInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }], selectionFollowsFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionFollowsFocus", required: false }] }] } });
883
+
884
+ /**
885
+ * Screen-reader announcement strings for `ForTreeNodeDrag`. Kept framework-free and
886
+ * separate from the directive so the live-region copy lives in one place and can be unit
887
+ * tested without rendering the tree. Not part of the public API.
888
+ */
889
+ /** Spoken when a node is picked up (assertive). */
890
+ function announceTreeLift(label) {
891
+ return `Picked up ${label}. Use arrow keys to move, Space to drop, Escape to cancel.`;
892
+ }
893
+ /** Spoken on each intermediate move while a node is lifted (polite). */
894
+ function announceTreeMove(label, parentLabel, position, total) {
895
+ const parentPart = parentLabel ? `under ${parentLabel}, ` : 'at root, ';
896
+ return `${label}: ${parentPart}position ${position} of ${total}.`;
897
+ }
898
+ /** Spoken when a node is committed to its new position (assertive). */
899
+ function announceTreeDrop(label, parentLabel, position, total) {
900
+ const parentPart = parentLabel ? `under ${parentLabel}, ` : 'at root, ';
901
+ return `Dropped ${label} ${parentPart}position ${position} of ${total}.`;
902
+ }
903
+ /** Spoken when a lift is cancelled and the node returns to its origin (assertive). */
904
+ function announceTreeCancel(label) {
905
+ return `Cancelled. ${label} returned to its original position.`;
906
+ }
907
+ /** Spoken when a `canDrop` veto rejects the attempted drop (assertive). */
908
+ function announceTreeInvalid(label) {
909
+ return `Cannot drop ${label} here.`;
910
+ }
911
+
912
+ /**
913
+ * Resolves the action for a key press while a node is lifted, honoring writing direction for
914
+ * the depth keys (ArrowRight deepens in LTR, ArrowLeft deepens in RTL). Cancel / commit are
915
+ * delegated to the shared {@link resolveLiftedDragControl}. Returns `null` for keys with no
916
+ * lifted-drag meaning.
917
+ *
918
+ * @param event The keydown event.
919
+ * @param dir Resolved writing direction.
920
+ */
921
+ function resolveTreeDragLiftedAction(event, dir) {
922
+ const control = resolveLiftedDragControl(event);
923
+ if (control) {
924
+ return control;
925
+ }
926
+ const key = event.key;
927
+ if (key === 'ArrowDown') {
928
+ return 'down';
929
+ }
930
+ if (key === 'ArrowUp') {
931
+ return 'up';
932
+ }
933
+ const isRtl = dir === 'rtl';
934
+ if (key === (isRtl ? 'ArrowLeft' : 'ArrowRight')) {
935
+ return 'deepen';
936
+ }
937
+ if (key === (isRtl ? 'ArrowRight' : 'ArrowLeft')) {
938
+ return 'shallow';
939
+ }
940
+ return null;
941
+ }
942
+
943
+ /**
944
+ * Builds the flat, DOM-ordered rows the drop resolver works on, EXCLUDING the lifted node
945
+ * (whose subtree is collapsed during the drag). Reads each row's live viewport rect.
946
+ *
947
+ * @param visible The tree's currently visible nodes, in DOM order.
948
+ * @param liftedValue The value of the node being dragged, filtered out of the result.
949
+ */
950
+ function buildTreeDropRows(visible, liftedValue) {
951
+ return visible
952
+ .filter((e) => e.handle.value() !== liftedValue)
953
+ .map((e) => {
954
+ const rect = e.handle.host.getBoundingClientRect();
955
+ return {
956
+ value: e.handle.value(),
957
+ level: e.handle.level(),
958
+ left: rect.left,
959
+ top: rect.top,
960
+ bottom: rect.bottom,
961
+ };
962
+ });
963
+ }
964
+ /** The visible row's trimmed accessible label (its label element, else its text content). */
965
+ function treeNodeLabel(entry) {
966
+ const labelEl = entry.handle.labelEl();
967
+ return (labelEl?.textContent ?? entry.handle.host.textContent ?? '').trim();
968
+ }
969
+ /**
970
+ * Whether a pointerdown inside `itemHost` may start a drag, honoring drag handles: when the
971
+ * item contains one or more registered handles, the press must land inside one; an item with
972
+ * no handle is grabbable anywhere.
973
+ *
974
+ * @param itemHost The `[forTreeItem]` host the press landed in.
975
+ * @param target The pointerdown event target.
976
+ * @param handles All registered drag-handle elements (across the whole tree).
977
+ */
978
+ function isInsideGrabArea(itemHost, target, handles) {
979
+ const itemHandles = [...handles].filter((h) => itemHost.contains(h));
980
+ if (itemHandles.length === 0) {
981
+ return true;
982
+ }
983
+ return itemHandles.some((h) => h === target || h.contains(target));
984
+ }
985
+ /** The trimmed label for `parentValue`, or `null` when it has no row (root drop). */
986
+ function treeParentLabel(visible, parentValue) {
987
+ if (parentValue === null) {
988
+ return null;
989
+ }
990
+ const entry = visible.find((e) => e.handle.value() === parentValue);
991
+ return entry ? treeNodeLabel(entry) : null;
992
+ }
993
+ /**
994
+ * Captures the lifted node's origin (value, parent, sibling index) from its position in the
995
+ * visible list, before any collapse-on-lift mutates the tree.
996
+ *
997
+ * @param visible The visible nodes at lift time, in DOM order.
998
+ * @param visibleIdx The lifted node's index within `visible`.
999
+ */
1000
+ function resolveTreeLiftContext(visible, visibleIdx) {
1001
+ const entry = visible[visibleIdx];
1002
+ if (!entry) {
1003
+ return null;
1004
+ }
1005
+ const parentHost = entry.parentHost;
1006
+ const parentEntry = parentHost ? visible.find((e) => e.handle.host === parentHost) : null;
1007
+ const previousIndex = visible.filter((e) => e.parentHost === parentHost && visible.indexOf(e) < visibleIdx).length;
1008
+ return {
1009
+ value: entry.handle.value(),
1010
+ parentValue: parentEntry ? parentEntry.handle.value() : null,
1011
+ previousIndex,
1012
+ };
1013
+ }
1014
+ /**
1015
+ * Resolves the initial insertion gap after a collapse-on-lift re-renders the visible list.
1016
+ * Pointer lifts clamp the original `visibleIdx` to the row count; keyboard lifts map it to the
1017
+ * first row whose post-collapse visible index is at or past `visibleIdx` (`rows.length` if
1018
+ * none), keeping the cursor where the focused node sat.
1019
+ *
1020
+ * @param rows The post-collapse drop rows (lifted node already excluded).
1021
+ * @param visibleAfter The visible nodes after the collapse.
1022
+ * @param visibleIdx The lifted node's index before the collapse.
1023
+ * @param mode Whether the lift was started by pointer or keyboard.
1024
+ */
1025
+ function resolveLiftGap(rows, visibleAfter, visibleIdx, mode) {
1026
+ if (mode === 'pointer') {
1027
+ return Math.min(visibleIdx, rows.length);
1028
+ }
1029
+ const gap = rows.findIndex((r) => {
1030
+ const nextEntry = visibleAfter.find((e) => e.handle.value() === r.value);
1031
+ return nextEntry && visibleAfter.indexOf(nextEntry) >= visibleIdx;
1032
+ });
1033
+ return gap < 0 ? rows.length : gap;
1034
+ }
1035
+
1036
+ const POINTER_ARM_THRESHOLD_PX = 5;
1037
+ /** InjectionToken for the `[forTreeNodeDrag]` coordinator. */
1038
+ const FOR_TREE_NODE_DRAG_CONTEXT = new InjectionToken('FOR_TREE_NODE_DRAG_CONTEXT');
1039
+ /**
1040
+ * Root-level drag-drop coordinator for `ForTree`. Apply on the same element as `[forTree]` to
1041
+ * enable reordering and re-parenting of tree nodes by pointer and keyboard.
1042
+ *
1043
+ * Keyboard: focus a node, then press Ctrl+Space (or Cmd+Space) to lift. While lifted, ArrowUp/Down
1044
+ * move the sibling position, ArrowRight/Left change depth, Space/Enter drops, Escape cancels.
1045
+ *
1046
+ * Pointer: drag any enabled item to a new position; an optional `[forTreeNodeDragHandle]` on an
1047
+ * item constrains the grab area.
1048
+ *
1049
+ * The `(nodeDrop)` output fires once per committed move. Apply `moveTreeNode` in the handler to
1050
+ * update the consumer's data. Provide a `[canDrop]` function to veto specific moves.
1051
+ */
1052
+ class ForTreeNodeDrag {
1053
+ #ctx = injectTreeContext('ForTreeNodeDrag');
1054
+ #hostEl = inject(ElementRef).nativeElement;
1055
+ #document = inject(DOCUMENT);
1056
+ #isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
1057
+ #announcer = inject(LiveAnnouncer);
1058
+ #destroyRef = inject(DestroyRef);
1059
+ /** Disables all drag interactions on this tree. */
1060
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1061
+ /**
1062
+ * Optional veto callback. Return `false` to reject a specific drop — the node is returned to its
1063
+ * original position and an announcement is made. When omitted, all drops are accepted.
1064
+ */
1065
+ canDrop = input(undefined, /* @ts-ignore */
1066
+ ...(ngDevMode ? [{ debugName: "canDrop" }] : /* istanbul ignore next */ []));
1067
+ /** Emitted once per committed move. Apply `moveTreeNode` in the handler to update your data. */
1068
+ nodeDrop = output();
1069
+ _dragging = signal(false, /* @ts-ignore */
1070
+ ...(ngDevMode ? [{ debugName: "_dragging" }] : /* istanbul ignore next */ []));
1071
+ _dropTargetValid = signal(false, /* @ts-ignore */
1072
+ ...(ngDevMode ? [{ debugName: "_dropTargetValid" }] : /* istanbul ignore next */ []));
1073
+ _dropLevel = signal(null, /* @ts-ignore */
1074
+ ...(ngDevMode ? [{ debugName: "_dropLevel" }] : /* istanbul ignore next */ []));
1075
+ #dropIndicator = signal(null, /* @ts-ignore */
1076
+ ...(ngDevMode ? [{ debugName: "#dropIndicator" }] : /* istanbul ignore next */ []));
1077
+ /** Resolved drop indicator while a drag is live; `null` when idle. */
1078
+ dropIndicator = this.#dropIndicator.asReadonly();
1079
+ #handles = new Set();
1080
+ #mode = 'idle';
1081
+ #liftedValue = null;
1082
+ #previousParent = null;
1083
+ #previousIndex = 0;
1084
+ #gapIndex = 0;
1085
+ #desiredLevel = 1;
1086
+ #previewController = null;
1087
+ #wasExpanded = false;
1088
+ #liftedHost = null;
1089
+ #label = '';
1090
+ #pointerSession = null;
1091
+ constructor() {
1092
+ if (!this.#isBrowser) {
1093
+ return;
1094
+ }
1095
+ createKeyboardDragMediator({
1096
+ host: this.#hostEl,
1097
+ isBrowser: this.#isBrowser,
1098
+ destroyRef: this.#destroyRef,
1099
+ isLifted: () => this.#mode === 'keyboard',
1100
+ onIdleKeydown: (event) => this.#onIdleKeydown(event),
1101
+ onLiftedKeydown: (event) => this.#handleLiftedKeydown(event),
1102
+ onFocusOut: (event) => this.#onFocusout(event),
1103
+ });
1104
+ this.#pointerSession = createPointerDragSession({
1105
+ host: this.#hostEl,
1106
+ document: this.#document,
1107
+ armThreshold: POINTER_ARM_THRESHOLD_PX,
1108
+ canStart: (event) => this.#canStartPointer(event),
1109
+ onLift: (event) => this.#onPointerLift(event),
1110
+ onMove: (event) => this.#onPointerMove(event),
1111
+ onCommit: (event) => this.#onPointerCommit(event),
1112
+ onCancel: () => this.#cancelSession(true),
1113
+ });
1114
+ this.#destroyRef.onDestroy(() => {
1115
+ this.#pointerSession?.destroy();
1116
+ if (this.#mode !== 'idle') {
1117
+ this.#cancelSession(false);
1118
+ }
1119
+ });
1120
+ }
1121
+ registerHandle(el) {
1122
+ this.#handles.add(el);
1123
+ }
1124
+ unregisterHandle(el) {
1125
+ this.#handles.delete(el);
1126
+ }
1127
+ #onIdleKeydown(event) {
1128
+ if (this.#mode !== 'idle' || !isDragLiftKey(event)) {
1129
+ return;
1130
+ }
1131
+ if (this.disabled() || this.#ctx.disabled()) {
1132
+ return;
1133
+ }
1134
+ const target = event.target;
1135
+ const visible = this.#ctx.visibleNodes();
1136
+ const entry = visible.find((e) => e.handle.host === target);
1137
+ if (!entry || entry.handle.disabled()) {
1138
+ return;
1139
+ }
1140
+ event.preventDefault();
1141
+ event.stopPropagation();
1142
+ this.#lift(entry.handle.host, visible.indexOf(entry), visible, 'keyboard');
1143
+ }
1144
+ #handleLiftedKeydown(event) {
1145
+ const action = resolveTreeDragLiftedAction(event, this.#ctx.dir());
1146
+ if (action === null) {
1147
+ return;
1148
+ }
1149
+ event.preventDefault();
1150
+ event.stopPropagation();
1151
+ if (action === 'cancel') {
1152
+ this.#cancelSession(true);
1153
+ return;
1154
+ }
1155
+ if (action === 'commit') {
1156
+ this.#commitSession();
1157
+ return;
1158
+ }
1159
+ const visible = this.#ctx.visibleNodes();
1160
+ if (action === 'down') {
1161
+ this.#gapIndex = Math.min(this.#gapIndex + 1, buildTreeDropRows(visible, this.#liftedValue).length);
1162
+ }
1163
+ else if (action === 'up') {
1164
+ this.#gapIndex = Math.max(this.#gapIndex - 1, 0);
1165
+ }
1166
+ else if (action === 'deepen') {
1167
+ this.#desiredLevel++;
1168
+ }
1169
+ else {
1170
+ this.#desiredLevel = Math.max(1, this.#desiredLevel - 1);
1171
+ }
1172
+ this.#resolveAndAnnounceMove(visible);
1173
+ }
1174
+ #onFocusout(event) {
1175
+ if (this.#mode !== 'keyboard') {
1176
+ return;
1177
+ }
1178
+ const related = event.relatedTarget;
1179
+ if (related && this.#hostEl.contains(related)) {
1180
+ return;
1181
+ }
1182
+ this.#cancelSession(true);
1183
+ }
1184
+ #canStartPointer(event) {
1185
+ if (this.disabled() || this.#ctx.disabled() || event.button !== 0) {
1186
+ return false;
1187
+ }
1188
+ const target = event.target;
1189
+ const itemHost = target.closest('[forTreeItem]');
1190
+ if (!itemHost) {
1191
+ return false;
1192
+ }
1193
+ const entry = this.#ctx.visibleNodes().find((e) => e.handle.host === itemHost);
1194
+ if (!entry || entry.handle.disabled()) {
1195
+ return false;
1196
+ }
1197
+ if (!isInsideGrabArea(itemHost, target, this.#handles)) {
1198
+ return false;
1199
+ }
1200
+ this.#liftedHost = itemHost;
1201
+ return true;
1202
+ }
1203
+ #onPointerLift(event) {
1204
+ if (!this.#liftedHost) {
1205
+ return false;
1206
+ }
1207
+ const visible = this.#ctx.visibleNodes();
1208
+ const idx = visible.findIndex((e) => e.handle.host === this.#liftedHost);
1209
+ if (idx < 0) {
1210
+ this.#liftedHost = null;
1211
+ return false;
1212
+ }
1213
+ this.#lift(this.#liftedHost, idx, visible, 'pointer', {
1214
+ x: event.clientX,
1215
+ y: event.clientY,
1216
+ });
1217
+ return true;
1218
+ }
1219
+ #applyPointerPosition(event) {
1220
+ const rows = buildTreeDropRows(this.#ctx.visibleNodes(), this.#liftedValue);
1221
+ this.#gapIndex = gapFromPointerY(rows, event.clientY);
1222
+ this.#desiredLevel = levelFromPointerX(rows, this.#gapIndex, event.clientX);
1223
+ return rows;
1224
+ }
1225
+ #onPointerMove(event) {
1226
+ if (this.#mode !== 'pointer') {
1227
+ return;
1228
+ }
1229
+ const rows = this.#applyPointerPosition(event);
1230
+ const target = resolveTreeDrop(rows, this.#gapIndex, this.#desiredLevel);
1231
+ this.#publishDropTarget(rows, target.level);
1232
+ this.#previewController?.moveTo({ x: event.clientX, y: event.clientY });
1233
+ }
1234
+ #onPointerCommit(event) {
1235
+ if (this.#mode !== 'pointer') {
1236
+ return;
1237
+ }
1238
+ this.#applyPointerPosition(event);
1239
+ this.#commitSession();
1240
+ }
1241
+ #lift(host, visibleIdx, visible, mode, point) {
1242
+ const entry = visible[visibleIdx];
1243
+ const origin = resolveTreeLiftContext(visible, visibleIdx);
1244
+ if (!entry || !origin) {
1245
+ return;
1246
+ }
1247
+ this.#liftedValue = origin.value;
1248
+ this.#previousParent = origin.parentValue;
1249
+ this.#previousIndex = origin.previousIndex;
1250
+ this.#wasExpanded = this.#ctx.isExpanded(origin.value);
1251
+ this.#label = treeNodeLabel(entry);
1252
+ this.#mode = mode;
1253
+ this.#liftedHost = host;
1254
+ if (this.#wasExpanded) {
1255
+ this.#ctx.setExpanded(origin.value, false);
1256
+ }
1257
+ if (mode === 'pointer' && point) {
1258
+ this.#previewController = new PreviewController({
1259
+ source: host,
1260
+ point,
1261
+ preview: null,
1262
+ doc: this.#document,
1263
+ boundary: null,
1264
+ lockAxis: () => null,
1265
+ });
1266
+ }
1267
+ const visibleAfter = this.#ctx.visibleNodes();
1268
+ const rows = buildTreeDropRows(visibleAfter, this.#liftedValue);
1269
+ this.#gapIndex = resolveLiftGap(rows, visibleAfter, visibleIdx, mode);
1270
+ this.#desiredLevel = entry.handle.level();
1271
+ this._dragging.set(true);
1272
+ this.#publishDropTarget(rows, this.#desiredLevel);
1273
+ this.#announcer.announce(announceTreeLift(this.#label), 'assertive');
1274
+ }
1275
+ #publishDropTarget(rows, level) {
1276
+ this._dropTargetValid.set(true);
1277
+ this._dropLevel.set(level);
1278
+ this.#dropIndicator.set(resolveDropIndicator(rows, this.#gapIndex, level));
1279
+ }
1280
+ #commitSession() {
1281
+ if (this.#mode === 'idle' || this.#liftedValue === null) {
1282
+ return;
1283
+ }
1284
+ const visible = this.#ctx.visibleNodes();
1285
+ const rows = buildTreeDropRows(visible, this.#liftedValue);
1286
+ const target = resolveTreeDrop(rows, this.#gapIndex, this.#desiredLevel);
1287
+ const parentLabel = treeParentLabel(visible, target.parentValue);
1288
+ const event = {
1289
+ node: this.#liftedValue,
1290
+ previousParent: this.#previousParent,
1291
+ newParent: target.parentValue,
1292
+ previousIndex: this.#previousIndex,
1293
+ currentIndex: target.index,
1294
+ };
1295
+ const veto = this.canDrop();
1296
+ if (veto !== undefined && !veto(event)) {
1297
+ this.#announcer.announce(announceTreeInvalid(this.#label), 'assertive');
1298
+ this.#cancelSession(true);
1299
+ return;
1300
+ }
1301
+ this.#restoreExpansion();
1302
+ this.nodeDrop.emit(event);
1303
+ this.#announcer.announce(announceTreeDrop(this.#label, parentLabel, target.index + 1, target.siblingCount + 1), 'assertive');
1304
+ this.#clearSession();
1305
+ }
1306
+ #cancelSession(restore) {
1307
+ if (restore) {
1308
+ this.#restoreExpansion();
1309
+ this.#announcer.announce(announceTreeCancel(this.#label), 'assertive');
1310
+ }
1311
+ this.#clearSession();
1312
+ }
1313
+ #restoreExpansion() {
1314
+ if (this.#wasExpanded && this.#liftedValue !== null) {
1315
+ this.#ctx.setExpanded(this.#liftedValue, true);
1316
+ }
1317
+ }
1318
+ #clearSession() {
1319
+ this.#mode = 'idle';
1320
+ this.#liftedValue = null;
1321
+ this.#previousParent = null;
1322
+ this.#previousIndex = 0;
1323
+ this.#gapIndex = 0;
1324
+ this.#desiredLevel = 1;
1325
+ this.#wasExpanded = false;
1326
+ this.#liftedHost = null;
1327
+ this.#label = '';
1328
+ if (this.#previewController) {
1329
+ this.#previewController.destroy();
1330
+ this.#previewController = null;
1331
+ }
1332
+ this._dragging.set(false);
1333
+ this._dropTargetValid.set(false);
1334
+ this._dropLevel.set(null);
1335
+ this.#dropIndicator.set(null);
1336
+ }
1337
+ #resolveAndAnnounceMove(visible) {
1338
+ const rows = buildTreeDropRows(visible, this.#liftedValue);
1339
+ const target = resolveTreeDrop(rows, this.#gapIndex, this.#desiredLevel);
1340
+ this.#desiredLevel = target.level;
1341
+ this.#publishDropTarget(rows, target.level);
1342
+ const parentLabel = treeParentLabel(visible, target.parentValue);
1343
+ this.#announcer.announce(announceTreeMove(this.#label, parentLabel, target.index + 1, target.siblingCount + 1), 'polite');
1344
+ }
1345
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForTreeNodeDrag, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1346
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForTreeNodeDrag, isStandalone: true, selector: "[forTreeNodeDrag]", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, canDrop: { classPropertyName: "canDrop", publicName: "canDrop", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeDrop: "nodeDrop" }, host: { properties: { "attr.data-dragging": "_dragging() ? \"\" : null", "attr.data-drop-target": "_dropTargetValid() ? \"\" : null", "style.--for-tree-drop-level": "_dropLevel()" } }, providers: [{ provide: FOR_TREE_NODE_DRAG_CONTEXT, useExisting: ForTreeNodeDrag }], exportAs: ["forTreeNodeDrag"], ngImport: i0 });
1347
+ }
1348
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForTreeNodeDrag, decorators: [{
1349
+ type: Directive,
1350
+ args: [{
1351
+ selector: '[forTreeNodeDrag]',
1352
+ exportAs: 'forTreeNodeDrag',
1353
+ providers: [{ provide: FOR_TREE_NODE_DRAG_CONTEXT, useExisting: ForTreeNodeDrag }],
1354
+ host: {
1355
+ '[attr.data-dragging]': '_dragging() ? "" : null',
1356
+ '[attr.data-drop-target]': '_dropTargetValid() ? "" : null',
1357
+ '[style.--for-tree-drop-level]': '_dropLevel()',
1358
+ },
1359
+ }]
1360
+ }], ctorParameters: () => [], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], canDrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "canDrop", required: false }] }], nodeDrop: [{ type: i0.Output, args: ["nodeDrop"] }] } });
1361
+
1362
+ /**
1363
+ * A single node in a `ForTree`. Carries the `role="treeitem"`, its ARIA state
1364
+ * (`aria-expanded` only when a `[forTreeItemToggle]` is registered, plus
1365
+ * `aria-selected` / `aria-level` / `aria-setsize` / `aria-posinset`), the
1366
+ * roving tab stop, and the full keyboard interaction.
1367
+ *
1368
+ * Apply on the structural element (typically `<li forTreeItem>`); place a
1369
+ * `[forTreeItemLabel]` inside as the pointer target and a `[forTreeGroup]`
1370
+ * (behind `@if`) for children.
1371
+ */
1372
+ class ForTreeItem {
1373
+ #tree = injectTreeContext('ForTreeItem');
1374
+ #container = injectTreeContainerContext('ForTreeItem');
1375
+ #host = inject(ElementRef);
1376
+ #drag = inject(FOR_TREE_NODE_DRAG_CONTEXT, { optional: true });
1377
+ /** Stable identifier for this node, mirrored into `[(value)]` / `[(expanded)]`. */
1378
+ value = input.required(/* @ts-ignore */
1379
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1380
+ /** Disables this node: not selectable, skipped by keyboard navigation. */
1381
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1382
+ /**
1383
+ * Typeahead text source override. Falls back to the `[forTreeItemLabel]`
1384
+ * element's text content when empty (default).
1385
+ */
1386
+ textValue = input('', /* @ts-ignore */
1387
+ ...(ngDevMode ? [{ debugName: "textValue" }] : /* istanbul ignore next */ []));
1388
+ /**
1389
+ * Virtualized path: zero-based absolute index in the flattened visible-node list.
1390
+ * Leave unset (default `null`) outside the virtualized path.
1391
+ */
1392
+ itemIndex = input(null, /* @ts-ignore */
1393
+ ...(ngDevMode ? [{ debugName: "itemIndex" }] : /* istanbul ignore next */ []));
1394
+ /**
1395
+ * Virtualized path: tree depth of this node (1-based, matching `aria-level`).
1396
+ * When set, overrides the container-derived level in the virtualized path.
1397
+ * Leave unset outside the virtualized path.
1398
+ */
1399
+ _levelInput = input(null, { ...(ngDevMode ? { debugName: "_levelInput" } : /* istanbul ignore next */ {}), alias: 'level' });
1400
+ /**
1401
+ * Virtualized path: total number of siblings at this node's level (matching
1402
+ * `aria-setsize`). When set, overrides the container-derived setsize in the
1403
+ * virtualized path. Leave unset outside the virtualized path.
1404
+ */
1405
+ _setSizeInput = input(null, { ...(ngDevMode ? { debugName: "_setSizeInput" } : /* istanbul ignore next */ {}), alias: 'setSize' });
1406
+ /**
1407
+ * Virtualized path: 1-based position among siblings at this node's level
1408
+ * (matching `aria-posinset`). When set, overrides the container-derived
1409
+ * posinset in the virtualized path. Leave unset outside the virtualized path.
1410
+ */
1411
+ _posInSetInput = input(null, { ...(ngDevMode ? { debugName: "_posInSetInput" } : /* istanbul ignore next */ {}), alias: 'posInSet' });
1412
+ #toggleCount = signal(0, /* @ts-ignore */
1413
+ ...(ngDevMode ? [{ debugName: "#toggleCount" }] : /* istanbul ignore next */ []));
1414
+ #childContainer = signal(null, /* @ts-ignore */
1415
+ ...(ngDevMode ? [{ debugName: "#childContainer" }] : /* istanbul ignore next */ []));
1416
+ #labelEl = signal(null, /* @ts-ignore */
1417
+ ...(ngDevMode ? [{ debugName: "#labelEl" }] : /* istanbul ignore next */ []));
1418
+ id = hostId('for-tree-item');
1419
+ #virtualized = computed(() => this.#tree.totalCount() !== undefined, /* @ts-ignore */
1420
+ ...(ngDevMode ? [{ debugName: "#virtualized" }] : /* istanbul ignore next */ []));
1421
+ /** True once a `[forTreeItemToggle]` registers, marking the node a parent (D4). */
1422
+ expandable = computed(() => this.#toggleCount() > 0, /* @ts-ignore */
1423
+ ...(ngDevMode ? [{ debugName: "expandable" }] : /* istanbul ignore next */ []));
1424
+ expanded = computed(() => this.#tree.isExpanded(this.value()), /* @ts-ignore */
1425
+ ...(ngDevMode ? [{ debugName: "expanded" }] : /* istanbul ignore next */ []));
1426
+ selected = computed(() => this.#tree.isSelected(this.value()), /* @ts-ignore */
1427
+ ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
1428
+ /** True when the root tree is in `'checkbox'` selection mode. */
1429
+ checkboxMode = computed(() => this.#tree.selectionMode() === 'checkbox', /* @ts-ignore */
1430
+ ...(ngDevMode ? [{ debugName: "checkboxMode" }] : /* istanbul ignore next */ []));
1431
+ /**
1432
+ * Tri-state checkbox status of this node — `'true'` / `'false'`, or `'mixed'`
1433
+ * when cascade is on and only some descendants are checked. Drives the
1434
+ * checkbox anatomy; meaningful only in `selectionMode="checkbox"`.
1435
+ */
1436
+ checkState = computed(() => this.#tree.checkState(this.value()), /* @ts-ignore */
1437
+ ...(ngDevMode ? [{ debugName: "checkState" }] : /* istanbul ignore next */ []));
1438
+ /**
1439
+ * True when this node is the current keyboard-focused / active candidate.
1440
+ * In the roving-tabindex path tracks DOM focus; in the virtualized
1441
+ * activedescendant path tracks `aria-activedescendant`. Reflected as
1442
+ * `data-highlighted`.
1443
+ */
1444
+ highlighted = computed(() => {
1445
+ const activeId = this.#tree.activeDescendantId();
1446
+ if (activeId !== null)
1447
+ return activeId === this.id();
1448
+ return this.#tree.roving.active() === this.#host.nativeElement;
1449
+ }, /* @ts-ignore */
1450
+ ...(ngDevMode ? [{ debugName: "highlighted" }] : /* istanbul ignore next */ []));
1451
+ effectiveDisabled = computed(() => this.disabled() || this.#tree.disabled(), /* @ts-ignore */
1452
+ ...(ngDevMode ? [{ debugName: "effectiveDisabled" }] : /* istanbul ignore next */ []));
1453
+ /**
1454
+ * Drop-indicator hook for `[forTreeNodeDrag]`: `'before'` / `'after'` when a live drag would
1455
+ * land adjacent to this row, `null` otherwise (and always `null` without a drag coordinator).
1456
+ * Reflected as `data-drop-position`.
1457
+ */
1458
+ _dropPosition = computed(() => {
1459
+ const indicator = this.#drag?.dropIndicator();
1460
+ if (!indicator || indicator.anchor !== this.value()) {
1461
+ return null;
1462
+ }
1463
+ return indicator.position;
1464
+ }, /* @ts-ignore */
1465
+ ...(ngDevMode ? [{ debugName: "_dropPosition" }] : /* istanbul ignore next */ []));
1466
+ level = computed(() => this.#virtualized() ? (this._levelInput() ?? this.#container.level()) : this.#container.level(), /* @ts-ignore */
1467
+ ...(ngDevMode ? [{ debugName: "level" }] : /* istanbul ignore next */ []));
1468
+ posinset = computed(() => this.#virtualized()
1469
+ ? (this._posInSetInput() ?? this.#container.indexOfHost(this.#host.nativeElement) + 1)
1470
+ : this.#container.indexOfHost(this.#host.nativeElement) + 1, /* @ts-ignore */
1471
+ ...(ngDevMode ? [{ debugName: "posinset" }] : /* istanbul ignore next */ []));
1472
+ setsize = computed(() => this.#virtualized()
1473
+ ? (this._setSizeInput() ?? this.#container.items().length)
1474
+ : this.#container.items().length, /* @ts-ignore */
1475
+ ...(ngDevMode ? [{ debugName: "setsize" }] : /* istanbul ignore next */ []));
1476
+ tabindex = computed(() => {
1477
+ if (this.effectiveDisabled()) {
1478
+ return -1;
1479
+ }
1480
+ if (this.#virtualized()) {
1481
+ return -1;
1482
+ }
1483
+ if (this.#tree.roving.hasActive()) {
1484
+ return this.#tree.roving.tabindexFor(this.#host.nativeElement);
1485
+ }
1486
+ if (this.selected()) {
1487
+ return 0;
1488
+ }
1489
+ if (this.#tree.value().length > 0) {
1490
+ return -1;
1491
+ }
1492
+ return this.#tree.isFirstFocusableItem(this.#host.nativeElement) ? 0 : -1;
1493
+ }, /* @ts-ignore */
1494
+ ...(ngDevMode ? [{ debugName: "tabindex" }] : /* istanbul ignore next */ []));
1495
+ constructor() {
1496
+ const handle = {
1497
+ host: this.#host.nativeElement,
1498
+ value: this.value,
1499
+ disabled: this.effectiveDisabled,
1500
+ expandable: this.expandable,
1501
+ childContainer: this.#childContainer.asReadonly(),
1502
+ textValue: this.textValue,
1503
+ labelEl: this.#labelEl.asReadonly(),
1504
+ id: this.id,
1505
+ itemIndex: this.itemIndex,
1506
+ level: this.level,
1507
+ };
1508
+ registerHandle(handle, (h) => this.#container.registerItem(h), (h) => this.#container.unregisterItem(h), 'afterNextRender');
1509
+ }
1510
+ registerToggle() {
1511
+ this.#toggleCount.update((n) => n + 1);
1512
+ return () => this.#toggleCount.update((n) => n - 1);
1513
+ }
1514
+ setChildContainer(container) {
1515
+ this.#childContainer.set(container);
1516
+ }
1517
+ setLabel(el) {
1518
+ this.#labelEl.set(el);
1519
+ }
1520
+ toggle() {
1521
+ if (!this.expandable() || this.effectiveDisabled()) {
1522
+ return;
1523
+ }
1524
+ this.#tree.setExpanded(this.value(), !this.expanded());
1525
+ }
1526
+ select() {
1527
+ if (this.effectiveDisabled()) {
1528
+ return;
1529
+ }
1530
+ this.#tree.select(this.value());
1531
+ }
1532
+ focusItem() {
1533
+ if (this.effectiveDisabled()) {
1534
+ return;
1535
+ }
1536
+ if (this.#virtualized()) {
1537
+ this.#tree.notifyItemClick(this.id());
1538
+ return;
1539
+ }
1540
+ this.#tree.roving.focusActive(this.#host.nativeElement);
1541
+ }
1542
+ onFocus() {
1543
+ if (this.effectiveDisabled() || this.#virtualized()) {
1544
+ return;
1545
+ }
1546
+ this.#tree.roving.setActive(this.#host.nativeElement);
1547
+ }
1548
+ onPointerDown(event) {
1549
+ if (!this.#virtualized())
1550
+ return;
1551
+ event.preventDefault();
1552
+ }
1553
+ onKeyDown(event) {
1554
+ const host = this.#host.nativeElement;
1555
+ // Tree items nest, so a keydown on a descendant bubbles through every
1556
+ // ancestor treeitem. Only the focused item (the event target) acts.
1557
+ if (event.target !== host || this.effectiveDisabled()) {
1558
+ return;
1559
+ }
1560
+ const tree = this.#tree;
1561
+ if (tree.multiple()) {
1562
+ if ((event.ctrlKey || event.metaKey) &&
1563
+ !event.shiftKey &&
1564
+ !event.altKey &&
1565
+ (event.key === 'a' || event.key === 'A')) {
1566
+ event.preventDefault();
1567
+ tree.selectAll();
1568
+ return;
1569
+ }
1570
+ if (event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
1571
+ const action = resolveListNavigation(event, {
1572
+ orientation: tree.orientation(),
1573
+ dir: tree.dir(),
1574
+ });
1575
+ if (action === 'next' || action === 'prev') {
1576
+ event.preventDefault();
1577
+ tree.extendByArrow(host, action);
1578
+ return;
1579
+ }
1580
+ if (event.key === ' ' || event.key === 'Spacebar') {
1581
+ event.preventDefault();
1582
+ tree.selectRangeToFocused(host);
1583
+ return;
1584
+ }
1585
+ }
1586
+ }
1587
+ if (event.key === 'Enter') {
1588
+ event.preventDefault();
1589
+ tree.select(this.value());
1590
+ return;
1591
+ }
1592
+ if (event.key === ' ' || event.key === 'Spacebar') {
1593
+ event.preventDefault();
1594
+ tree.select(this.value());
1595
+ return;
1596
+ }
1597
+ if (event.key === '*') {
1598
+ event.preventDefault();
1599
+ tree.expandSiblings(host);
1600
+ return;
1601
+ }
1602
+ const action = resolveListNavigation(event, {
1603
+ orientation: tree.orientation(),
1604
+ dir: tree.dir(),
1605
+ });
1606
+ if (action) {
1607
+ event.preventDefault();
1608
+ tree.navigate(host, action);
1609
+ return;
1610
+ }
1611
+ const intent = resolveTreeExpandCollapse(event, {
1612
+ orientation: tree.orientation(),
1613
+ dir: tree.dir(),
1614
+ });
1615
+ if (intent === 'expand') {
1616
+ event.preventDefault();
1617
+ tree.expandOrEnter(host);
1618
+ return;
1619
+ }
1620
+ if (intent === 'collapse') {
1621
+ event.preventDefault();
1622
+ tree.collapseOrLeave(host);
1623
+ return;
1624
+ }
1625
+ tree.handleTypeahead(event);
1626
+ }
1627
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForTreeItem, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1628
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForTreeItem, isStandalone: true, selector: "[forTreeItem]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, textValue: { classPropertyName: "textValue", publicName: "textValue", isSignal: true, isRequired: false, transformFunction: null }, itemIndex: { classPropertyName: "itemIndex", publicName: "itemIndex", isSignal: true, isRequired: false, transformFunction: null }, _levelInput: { classPropertyName: "_levelInput", publicName: "level", isSignal: true, isRequired: false, transformFunction: null }, _setSizeInput: { classPropertyName: "_setSizeInput", publicName: "setSize", isSignal: true, isRequired: false, transformFunction: null }, _posInSetInput: { classPropertyName: "_posInSetInput", publicName: "posInSet", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "treeitem" }, listeners: { "keydown": "onKeyDown($event)", "focus": "onFocus()", "pointerdown": "onPointerDown($event)" }, properties: { "id": "id()", "attr.aria-expanded": "expandable() ? (expanded() ? \"true\" : \"false\") : null", "attr.aria-checked": "checkboxMode() ? checkState() : null", "attr.aria-selected": "checkboxMode() ? null : (selected() ? \"true\" : \"false\")", "attr.aria-level": "level()", "attr.aria-setsize": "setsize()", "attr.aria-posinset": "posinset()", "attr.aria-disabled": "effectiveDisabled() ? \"true\" : null", "attr.tabindex": "tabindex()", "attr.data-state": "expandable() ? (expanded() ? \"open\" : \"closed\") : null", "attr.data-selected": "selected() ? \"\" : null", "attr.data-highlighted": "highlighted() ? \"\" : null", "attr.data-disabled": "effectiveDisabled() ? \"\" : null", "attr.data-checked": "checkboxMode() ? checkState() : null", "attr.data-drop-position": "_dropPosition()" } }, providers: [{ provide: FOR_TREE_ITEM_CONTEXT, useExisting: ForTreeItem }], exportAs: ["forTreeItem"], ngImport: i0 });
1629
+ }
1630
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForTreeItem, decorators: [{
1631
+ type: Directive,
1632
+ args: [{
1633
+ selector: '[forTreeItem]',
1634
+ exportAs: 'forTreeItem',
1635
+ host: {
1636
+ role: 'treeitem',
1637
+ '[id]': 'id()',
1638
+ '[attr.aria-expanded]': 'expandable() ? (expanded() ? "true" : "false") : null',
1639
+ '[attr.aria-checked]': 'checkboxMode() ? checkState() : null',
1640
+ '[attr.aria-selected]': 'checkboxMode() ? null : (selected() ? "true" : "false")',
1641
+ '[attr.aria-level]': 'level()',
1642
+ '[attr.aria-setsize]': 'setsize()',
1643
+ '[attr.aria-posinset]': 'posinset()',
1644
+ '[attr.aria-disabled]': 'effectiveDisabled() ? "true" : null',
1645
+ '[attr.tabindex]': 'tabindex()',
1646
+ '[attr.data-state]': 'expandable() ? (expanded() ? "open" : "closed") : null',
1647
+ '[attr.data-selected]': 'selected() ? "" : null',
1648
+ '[attr.data-highlighted]': 'highlighted() ? "" : null',
1649
+ '[attr.data-disabled]': 'effectiveDisabled() ? "" : null',
1650
+ '[attr.data-checked]': 'checkboxMode() ? checkState() : null',
1651
+ '[attr.data-drop-position]': '_dropPosition()',
1652
+ '(keydown)': 'onKeyDown($event)',
1653
+ '(focus)': 'onFocus()',
1654
+ '(pointerdown)': 'onPointerDown($event)',
1655
+ },
1656
+ providers: [{ provide: FOR_TREE_ITEM_CONTEXT, useExisting: ForTreeItem }],
1657
+ }]
1658
+ }], ctorParameters: () => [], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], textValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "textValue", required: false }] }], itemIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemIndex", required: false }] }], _levelInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "level", required: false }] }], _setSizeInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "setSize", required: false }] }], _posInSetInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "posInSet", required: false }] }] } });
1659
+
1660
+ /**
1661
+ * Pointer target for a `ForTreeItem` and the default typeahead text source.
1662
+ * Clicking it selects the node and moves roving focus to the `treeitem`
1663
+ * (focus stays on the item, never on the label). Place the
1664
+ * `[forTreeItemToggle]` and the node's visible text inside it.
1665
+ */
1666
+ class ForTreeItemLabel {
1667
+ #item = injectTreeItemContext('ForTreeItemLabel');
1668
+ #host = inject(ElementRef);
1669
+ constructor() {
1670
+ this.#item.setLabel(this.#host.nativeElement);
1671
+ inject(DestroyRef).onDestroy(() => this.#item.setLabel(null));
1672
+ }
1673
+ onClick() {
1674
+ this.#item.select();
1675
+ this.#item.focusItem();
1676
+ }
1677
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForTreeItemLabel, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1678
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForTreeItemLabel, isStandalone: true, selector: "[forTreeItemLabel]", host: { listeners: { "click": "onClick()" } }, exportAs: ["forTreeItemLabel"], ngImport: i0 });
1679
+ }
1680
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForTreeItemLabel, decorators: [{
1681
+ type: Directive,
1682
+ args: [{
1683
+ selector: '[forTreeItemLabel]',
1684
+ exportAs: 'forTreeItemLabel',
1685
+ host: {
1686
+ '(click)': 'onClick()',
1687
+ },
1688
+ }]
1689
+ }], ctorParameters: () => [] });
1690
+
1691
+ /**
1692
+ * Optional expand / collapse control inside a `ForTreeItem`. Its mere presence
1693
+ * marks the item as a parent (D4): a `treeitem` emits `aria-expanded` /
1694
+ * `data-state` only when a toggle is registered, so leaves (no toggle) emit
1695
+ * neither — matching the APG "end nodes lack `aria-expanded`" rule.
1696
+ *
1697
+ * Decorative: the enclosing `treeitem` owns `aria-expanded`, so the toggle is
1698
+ * `aria-hidden` and not separately focusable. Clicking it toggles expansion
1699
+ * without selecting the node.
1700
+ */
1701
+ class ForTreeItemToggle {
1702
+ item = injectTreeItemContext('ForTreeItemToggle');
1703
+ constructor() {
1704
+ const unregister = this.item.registerToggle();
1705
+ inject(DestroyRef).onDestroy(unregister);
1706
+ }
1707
+ onClick(event) {
1708
+ event.stopPropagation();
1709
+ this.item.toggle();
1710
+ }
1711
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForTreeItemToggle, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1712
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForTreeItemToggle, isStandalone: true, selector: "[forTreeItemToggle]", host: { attributes: { "type": "button", "tabindex": "-1", "aria-hidden": "true" }, listeners: { "click": "onClick($event)" }, properties: { "attr.data-state": "item.expanded() ? \"open\" : \"closed\"" } }, exportAs: ["forTreeItemToggle"], ngImport: i0 });
1713
+ }
1714
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForTreeItemToggle, decorators: [{
1715
+ type: Directive,
1716
+ args: [{
1717
+ selector: '[forTreeItemToggle]',
1718
+ exportAs: 'forTreeItemToggle',
1719
+ host: {
1720
+ type: 'button',
1721
+ tabindex: '-1',
1722
+ 'aria-hidden': 'true',
1723
+ '[attr.data-state]': 'item.expanded() ? "open" : "closed"',
1724
+ '(click)': 'onClick($event)',
1725
+ },
1726
+ }]
1727
+ }], ctorParameters: () => [] });
1728
+
1729
+ /**
1730
+ * Nested container (`role="group"`) holding a parent node's child
1731
+ * `ForTreeItem`s. Rendered behind `@if` so a collapsed parent drops its group
1732
+ * entirely. Its `level` is one deeper than the enclosing item, and it
1733
+ * registers itself as that item's child container so the root can flatten the
1734
+ * visible nodes.
1735
+ *
1736
+ * @example
1737
+ * ```html
1738
+ * @if (n.children?.length && isExpanded(n.id)) {
1739
+ * <ul forTreeGroup>
1740
+ * @for (child of n.children; track child.id) { ... }
1741
+ * </ul>
1742
+ * }
1743
+ * ```
1744
+ */
1745
+ class ForTreeGroup {
1746
+ #parentItem = injectTreeItemContext('ForTreeGroup');
1747
+ #items = new Collection();
1748
+ items = this.#items.items;
1749
+ level = computed(() => this.#parentItem.level() + 1, /* @ts-ignore */
1750
+ ...(ngDevMode ? [{ debugName: "level" }] : /* istanbul ignore next */ []));
1751
+ constructor() {
1752
+ this.#parentItem.setChildContainer(this);
1753
+ inject(DestroyRef).onDestroy(() => this.#parentItem.setChildContainer(null));
1754
+ }
1755
+ registerItem(handle) {
1756
+ this.#items.register(handle);
1757
+ }
1758
+ unregisterItem(handle) {
1759
+ this.#items.unregister(handle);
1760
+ }
1761
+ indexOfHost(el) {
1762
+ return this.#items.indexOfHost(el);
1763
+ }
1764
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForTreeGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1765
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForTreeGroup, isStandalone: true, selector: "[forTreeGroup]", host: { attributes: { "role": "group" } }, providers: [{ provide: FOR_TREE_CONTAINER_CONTEXT, useExisting: ForTreeGroup }], exportAs: ["forTreeGroup"], ngImport: i0 });
1766
+ }
1767
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForTreeGroup, decorators: [{
1768
+ type: Directive,
1769
+ args: [{
1770
+ selector: '[forTreeGroup]',
1771
+ exportAs: 'forTreeGroup',
1772
+ host: {
1773
+ role: 'group',
1774
+ },
1775
+ providers: [{ provide: FOR_TREE_CONTAINER_CONTEXT, useExisting: ForTreeGroup }],
1776
+ }]
1777
+ }], ctorParameters: () => [] });
1778
+
1779
+ /**
1780
+ * Visible checkbox surface inside a `ForTreeItem`, used in the tree's
1781
+ * `selectionMode="checkbox"` anatomy. Decorative for assistive tech — the
1782
+ * enclosing `treeitem` owns `aria-checked`, so this element is `aria-hidden`
1783
+ * and not separately focusable. Reflects `data-state="checked" | "unchecked" |
1784
+ * "indeterminate"` for styling. Clicking it toggles the node's selection and
1785
+ * moves roving focus to the node; place a `[forTreeItemCheckboxIndicator]`
1786
+ * inside for the glyph.
1787
+ */
1788
+ class ForTreeItemCheckbox {
1789
+ item = injectTreeItemContext('ForTreeItemCheckbox');
1790
+ dataState = computed(() => {
1791
+ const state = this.item.checkState();
1792
+ return state === 'true' ? 'checked' : state === 'mixed' ? 'indeterminate' : 'unchecked';
1793
+ }, /* @ts-ignore */
1794
+ ...(ngDevMode ? [{ debugName: "dataState" }] : /* istanbul ignore next */ []));
1795
+ onClick(event) {
1796
+ event.stopPropagation();
1797
+ this.item.select();
1798
+ this.item.focusItem();
1799
+ }
1800
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForTreeItemCheckbox, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1801
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForTreeItemCheckbox, isStandalone: true, selector: "[forTreeItemCheckbox]", host: { attributes: { "aria-hidden": "true" }, listeners: { "click": "onClick($event)" }, properties: { "attr.data-state": "dataState()" } }, exportAs: ["forTreeItemCheckbox"], ngImport: i0 });
1802
+ }
1803
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForTreeItemCheckbox, decorators: [{
1804
+ type: Directive,
1805
+ args: [{
1806
+ selector: '[forTreeItemCheckbox]',
1807
+ exportAs: 'forTreeItemCheckbox',
1808
+ host: {
1809
+ 'aria-hidden': 'true',
1810
+ '[attr.data-state]': 'dataState()',
1811
+ '(click)': 'onClick($event)',
1812
+ },
1813
+ }]
1814
+ }] });
1815
+
1816
+ /**
1817
+ * Optional glyph slot inside a `[forTreeItemCheckbox]`. Shows while the node
1818
+ * is checked or indeterminate (`data-state="checked"` or `"indeterminate"`);
1819
+ * self-hides only when fully unchecked. Hidden state is enforced with an inline
1820
+ * `display: none` (which beats any author `display` rule applied via a class)
1821
+ * in addition to the `hidden` attribute that removes it from the a11y tree.
1822
+ * Mirrors `data-state="checked" | "unchecked" | "indeterminate"` from the item.
1823
+ */
1824
+ class ForTreeItemCheckboxIndicator {
1825
+ item = injectTreeItemContext('ForTreeItemCheckboxIndicator');
1826
+ shown = computed(() => this.item.checkState() !== 'false', /* @ts-ignore */
1827
+ ...(ngDevMode ? [{ debugName: "shown" }] : /* istanbul ignore next */ []));
1828
+ dataState = computed(() => {
1829
+ const state = this.item.checkState();
1830
+ return state === 'true' ? 'checked' : state === 'mixed' ? 'indeterminate' : 'unchecked';
1831
+ }, /* @ts-ignore */
1832
+ ...(ngDevMode ? [{ debugName: "dataState" }] : /* istanbul ignore next */ []));
1833
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForTreeItemCheckboxIndicator, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1834
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForTreeItemCheckboxIndicator, isStandalone: true, selector: "[forTreeItemCheckboxIndicator]", host: { properties: { "attr.data-state": "dataState()", "attr.hidden": "shown() ? null : \"\"", "style.display": "shown() ? null : \"none\"" } }, exportAs: ["forTreeItemCheckboxIndicator"], ngImport: i0 });
1835
+ }
1836
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForTreeItemCheckboxIndicator, decorators: [{
1837
+ type: Directive,
1838
+ args: [{
1839
+ selector: '[forTreeItemCheckboxIndicator]',
1840
+ exportAs: 'forTreeItemCheckboxIndicator',
1841
+ host: {
1842
+ '[attr.data-state]': 'dataState()',
1843
+ '[attr.hidden]': 'shown() ? null : ""',
1844
+ '[style.display]': 'shown() ? null : "none"',
1845
+ },
1846
+ }]
1847
+ }] });
1848
+
1849
+ /**
1850
+ * Returns the de-duplicated ancestor values that must be added to a tree's
1851
+ * `expanded` set so every matched node becomes visible.
1852
+ *
1853
+ * Pure and headless: filtering stays consumer-owned — the consumer matches its
1854
+ * own data, re-renders the tree, and feeds its hierarchy through `ancestorsOf`.
1855
+ * Merge the result into `[(expanded)]`:
1856
+ *
1857
+ * ```ts
1858
+ * this.expanded.update((open) => [
1859
+ * ...new Set([...open, ...expandToReveal(matches, this.ancestorsOf)]),
1860
+ * ]);
1861
+ * ```
1862
+ *
1863
+ * The matched nodes themselves are not returned — a node is made visible by
1864
+ * expanding its ancestors, so a root-level match contributes nothing.
1865
+ *
1866
+ * @param matches The values of the nodes that matched the current filter.
1867
+ * @param ancestorsOf Returns a node's ancestor values (the node itself excluded);
1868
+ * order is irrelevant and a root node returns an empty list.
1869
+ * @returns The unique ancestor values to expand. Empty when `matches` is empty
1870
+ * or every match is a root.
1871
+ */
1872
+ function expandToReveal(matches, ancestorsOf) {
1873
+ const reveal = new Set();
1874
+ for (const match of matches) {
1875
+ for (const ancestor of ancestorsOf(match)) {
1876
+ reveal.add(ancestor);
1877
+ }
1878
+ }
1879
+ return [...reveal];
1880
+ }
1881
+
1882
+ /**
1883
+ * Optional drag handle for a tree node. When placed inside a tree item, it constrains the pointer
1884
+ * grab area — only pointer events originating from within this element start a drag for that item.
1885
+ * Has no effect on the keyboard drag path (Ctrl+Space on the focused item always works).
1886
+ *
1887
+ * @example
1888
+ * ```html
1889
+ * <li forTreeItem value="file">
1890
+ * <span forTreeNodeDragHandle aria-hidden="true">⠿</span>
1891
+ * <span forTreeItemLabel>File.txt</span>
1892
+ * </li>
1893
+ * ```
1894
+ */
1895
+ class ForTreeNodeDragHandle {
1896
+ constructor() {
1897
+ const ctx = inject(FOR_TREE_NODE_DRAG_CONTEXT, { optional: true });
1898
+ if (!ctx) {
1899
+ throw new Error('[forty-cdk/tree] [forTreeNodeDragHandle] must be used inside a [forTreeNodeDrag] element.');
1900
+ }
1901
+ const el = inject(ElementRef).nativeElement;
1902
+ ctx.registerHandle(el);
1903
+ inject(DestroyRef).onDestroy(() => ctx.unregisterHandle(el));
1904
+ }
1905
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForTreeNodeDragHandle, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1906
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForTreeNodeDragHandle, isStandalone: true, selector: "[forTreeNodeDragHandle]", exportAs: ["forTreeNodeDragHandle"], ngImport: i0 });
1907
+ }
1908
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForTreeNodeDragHandle, decorators: [{
1909
+ type: Directive,
1910
+ args: [{
1911
+ selector: '[forTreeNodeDragHandle]',
1912
+ exportAs: 'forTreeNodeDragHandle',
1913
+ }]
1914
+ }], ctorParameters: () => [] });
1915
+
1916
+ function collectSubtreeIds(node, options) {
1917
+ const ids = new Set();
1918
+ ids.add(options.trackBy(node));
1919
+ const kids = options.children(node) ?? [];
1920
+ for (const child of kids) {
1921
+ for (const id of collectSubtreeIds(child, options)) {
1922
+ ids.add(id);
1923
+ }
1924
+ }
1925
+ return ids;
1926
+ }
1927
+ function detachNode(roots, targetId, options) {
1928
+ for (let i = 0; i < roots.length; i++) {
1929
+ const node = roots[i];
1930
+ if (options.trackBy(node) === targetId) {
1931
+ const next = [...roots.slice(0, i), ...roots.slice(i + 1)];
1932
+ return { found: node, roots: next };
1933
+ }
1934
+ const kids = options.children(node) ?? [];
1935
+ if (kids.length > 0) {
1936
+ const childResult = detachNode(kids, targetId, options);
1937
+ if (childResult !== null) {
1938
+ const updatedNode = options.withChildren(node, childResult.roots);
1939
+ const next = [...roots.slice(0, i), updatedNode, ...roots.slice(i + 1)];
1940
+ return { found: childResult.found, roots: next };
1941
+ }
1942
+ }
1943
+ }
1944
+ return null;
1945
+ }
1946
+ function insertNode(roots, parentId, index, nodeToInsert, options) {
1947
+ if (parentId === null) {
1948
+ const clamped = Math.max(0, Math.min(index, roots.length));
1949
+ return [...roots.slice(0, clamped), nodeToInsert, ...roots.slice(clamped)];
1950
+ }
1951
+ for (let i = 0; i < roots.length; i++) {
1952
+ const node = roots[i];
1953
+ if (options.trackBy(node) === parentId) {
1954
+ const kids = options.children(node) ?? [];
1955
+ const clamped = Math.max(0, Math.min(index, kids.length));
1956
+ const newKids = [...kids.slice(0, clamped), nodeToInsert, ...kids.slice(clamped)];
1957
+ const updated = options.withChildren(node, newKids);
1958
+ return [...roots.slice(0, i), updated, ...roots.slice(i + 1)];
1959
+ }
1960
+ const kids = options.children(node) ?? [];
1961
+ if (kids.length > 0) {
1962
+ const result = insertNode(kids, parentId, index, nodeToInsert, options);
1963
+ if (result !== null) {
1964
+ const updated = options.withChildren(node, result);
1965
+ return [...roots.slice(0, i), updated, ...roots.slice(i + 1)];
1966
+ }
1967
+ }
1968
+ }
1969
+ return null;
1970
+ }
1971
+ /**
1972
+ * Applies a {@link ForTreeDragDropEvent} to a nested, consumer-owned tree, returning a new roots
1973
+ * array. Pure and immutable — never mutates `roots` or any node. Detaches `event.node` from its
1974
+ * current parent and re-inserts it (with its subtree) under `event.newParent` at
1975
+ * `event.currentIndex` (or among the roots when `newParent` is `null`). Returns a shallow copy of
1976
+ * `roots` unchanged when the move is a no-op or invalid (node not found, or `newParent` is the node
1977
+ * itself or one of its descendants).
1978
+ */
1979
+ function moveTreeNode(roots, options) {
1980
+ const { event } = options;
1981
+ const detachResult = detachNode(roots, event.node, options);
1982
+ if (detachResult === null) {
1983
+ return [...roots];
1984
+ }
1985
+ const { found, roots: rootsAfterDetach } = detachResult;
1986
+ if (event.newParent !== null) {
1987
+ const subtreeIds = collectSubtreeIds(found, options);
1988
+ if (subtreeIds.has(event.newParent)) {
1989
+ return [...roots];
1990
+ }
1991
+ if (event.newParent === event.node) {
1992
+ return [...roots];
1993
+ }
1994
+ }
1995
+ const result = insertNode(rootsAfterDetach, event.newParent, event.currentIndex, found, options);
1996
+ if (result === null) {
1997
+ return [...roots];
1998
+ }
1999
+ return result;
2000
+ }
2001
+
2002
+ /**
2003
+ * Generated bundle index. Do not edit.
2004
+ */
2005
+
2006
+ export { FOR_TREE_CONTAINER_CONTEXT, FOR_TREE_CONTEXT, FOR_TREE_DEFAULTS, FOR_TREE_ITEM_CONTEXT, FOR_TREE_NODE_DRAG_CONTEXT, ForTree, ForTreeGroup, ForTreeItem, ForTreeItemCheckbox, ForTreeItemCheckboxIndicator, ForTreeItemLabel, ForTreeItemToggle, ForTreeNodeDrag, ForTreeNodeDragHandle, expandToReveal, moveTreeNode, provideForTreeDefaults };
2007
+ //# sourceMappingURL=forty-cdk-tree.mjs.map