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.
- package/accordion/README.md +122 -0
- package/aspect-ratio/README.md +76 -0
- package/avatar/README.md +100 -0
- package/breadcrumbs/README.md +49 -0
- package/breakpoints/README.md +81 -0
- package/button/README.md +49 -0
- package/calendar/README.md +458 -0
- package/carousel/README.md +358 -0
- package/checkbox/README.md +146 -0
- package/combobox/README.md +535 -0
- package/context-menu/README.md +139 -0
- package/date-field/README.md +184 -0
- package/date-picker/README.md +338 -0
- package/dialog/README.md +388 -0
- package/disclosure/README.md +114 -0
- package/drag-drop/README.md +359 -0
- package/drawer/README.md +560 -0
- package/dropdown-menu/README.md +176 -0
- package/fesm2022/forty-cdk-accordion.mjs +348 -0
- package/fesm2022/forty-cdk-accordion.mjs.map +1 -0
- package/fesm2022/forty-cdk-aspect-ratio.mjs +74 -0
- package/fesm2022/forty-cdk-aspect-ratio.mjs.map +1 -0
- package/fesm2022/forty-cdk-avatar.mjs +308 -0
- package/fesm2022/forty-cdk-avatar.mjs.map +1 -0
- package/fesm2022/forty-cdk-breadcrumbs.mjs +125 -0
- package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -0
- package/fesm2022/forty-cdk-breakpoints.mjs +117 -0
- package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -0
- package/fesm2022/forty-cdk-button.mjs +134 -0
- package/fesm2022/forty-cdk-button.mjs.map +1 -0
- package/fesm2022/forty-cdk-calendar.mjs +2034 -0
- package/fesm2022/forty-cdk-calendar.mjs.map +1 -0
- package/fesm2022/forty-cdk-carousel.mjs +968 -0
- package/fesm2022/forty-cdk-carousel.mjs.map +1 -0
- package/fesm2022/forty-cdk-checkbox.mjs +226 -0
- package/fesm2022/forty-cdk-checkbox.mjs.map +1 -0
- package/fesm2022/forty-cdk-combobox.mjs +2596 -0
- package/fesm2022/forty-cdk-combobox.mjs.map +1 -0
- package/fesm2022/forty-cdk-context-menu.mjs +413 -0
- package/fesm2022/forty-cdk-context-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-core.mjs +9022 -0
- package/fesm2022/forty-cdk-core.mjs.map +1 -0
- package/fesm2022/forty-cdk-date-field.mjs +744 -0
- package/fesm2022/forty-cdk-date-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-date-picker.mjs +1011 -0
- package/fesm2022/forty-cdk-date-picker.mjs.map +1 -0
- package/fesm2022/forty-cdk-dialog.mjs +707 -0
- package/fesm2022/forty-cdk-dialog.mjs.map +1 -0
- package/fesm2022/forty-cdk-disclosure.mjs +190 -0
- package/fesm2022/forty-cdk-disclosure.mjs.map +1 -0
- package/fesm2022/forty-cdk-drag-drop.mjs +1180 -0
- package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -0
- package/fesm2022/forty-cdk-drawer.mjs +1641 -0
- package/fesm2022/forty-cdk-drawer.mjs.map +1 -0
- package/fesm2022/forty-cdk-dropdown-menu.mjs +350 -0
- package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-field.mjs +425 -0
- package/fesm2022/forty-cdk-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-fieldset.mjs +164 -0
- package/fesm2022/forty-cdk-fieldset.mjs.map +1 -0
- package/fesm2022/forty-cdk-file-upload.mjs +221 -0
- package/fesm2022/forty-cdk-file-upload.mjs.map +1 -0
- package/fesm2022/forty-cdk-hover-card.mjs +496 -0
- package/fesm2022/forty-cdk-hover-card.mjs.map +1 -0
- package/fesm2022/forty-cdk-input.mjs +274 -0
- package/fesm2022/forty-cdk-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-internationalized-date.mjs +1 -1
- package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
- package/fesm2022/forty-cdk-listbox.mjs +1279 -0
- package/fesm2022/forty-cdk-listbox.mjs.map +1 -0
- package/fesm2022/forty-cdk-menu.mjs +1439 -0
- package/fesm2022/forty-cdk-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-menubar.mjs +787 -0
- package/fesm2022/forty-cdk-menubar.mjs.map +1 -0
- package/fesm2022/forty-cdk-meter.mjs +211 -0
- package/fesm2022/forty-cdk-meter.mjs.map +1 -0
- package/fesm2022/forty-cdk-navigation-menu.mjs +1145 -0
- package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-number-input.mjs +559 -0
- package/fesm2022/forty-cdk-number-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-otp-input.mjs +527 -0
- package/fesm2022/forty-cdk-otp-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-pagination.mjs +323 -0
- package/fesm2022/forty-cdk-pagination.mjs.map +1 -0
- package/fesm2022/forty-cdk-pane-resizer.mjs +297 -0
- package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -0
- package/fesm2022/forty-cdk-popover.mjs +698 -0
- package/fesm2022/forty-cdk-popover.mjs.map +1 -0
- package/fesm2022/forty-cdk-progress.mjs +226 -0
- package/fesm2022/forty-cdk-progress.mjs.map +1 -0
- package/fesm2022/forty-cdk-radio-group.mjs +378 -0
- package/fesm2022/forty-cdk-radio-group.mjs.map +1 -0
- package/fesm2022/forty-cdk-scroll-area.mjs +640 -0
- package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -0
- package/fesm2022/forty-cdk-search.mjs +205 -0
- package/fesm2022/forty-cdk-search.mjs.map +1 -0
- package/fesm2022/forty-cdk-select.mjs +1661 -0
- package/fesm2022/forty-cdk-select.mjs.map +1 -0
- package/fesm2022/forty-cdk-separator.mjs +82 -0
- package/fesm2022/forty-cdk-separator.mjs.map +1 -0
- package/fesm2022/forty-cdk-signal-forms.mjs +97 -0
- package/fesm2022/forty-cdk-signal-forms.mjs.map +1 -0
- package/fesm2022/forty-cdk-slider.mjs +803 -0
- package/fesm2022/forty-cdk-slider.mjs.map +1 -0
- package/fesm2022/forty-cdk-stepper.mjs +886 -0
- package/fesm2022/forty-cdk-stepper.mjs.map +1 -0
- package/fesm2022/forty-cdk-switch.mjs +137 -0
- package/fesm2022/forty-cdk-switch.mjs.map +1 -0
- package/fesm2022/forty-cdk-table.mjs +1518 -0
- package/fesm2022/forty-cdk-table.mjs.map +1 -0
- package/fesm2022/forty-cdk-tabs.mjs +400 -0
- package/fesm2022/forty-cdk-tabs.mjs.map +1 -0
- package/fesm2022/forty-cdk-time-field.mjs +593 -0
- package/fesm2022/forty-cdk-time-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-time-picker.mjs +1013 -0
- package/fesm2022/forty-cdk-time-picker.mjs.map +1 -0
- package/fesm2022/forty-cdk-toast.mjs +1153 -0
- package/fesm2022/forty-cdk-toast.mjs.map +1 -0
- package/fesm2022/forty-cdk-toggle.mjs +516 -0
- package/fesm2022/forty-cdk-toggle.mjs.map +1 -0
- package/fesm2022/forty-cdk-toolbar.mjs +374 -0
- package/fesm2022/forty-cdk-toolbar.mjs.map +1 -0
- package/fesm2022/forty-cdk-tooltip.mjs +672 -0
- package/fesm2022/forty-cdk-tooltip.mjs.map +1 -0
- package/fesm2022/forty-cdk-tree.mjs +2007 -0
- package/fesm2022/forty-cdk-tree.mjs.map +1 -0
- package/fesm2022/forty-cdk-virtualization.mjs +1 -1
- package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
- package/fesm2022/forty-cdk.mjs +0 -43310
- package/fesm2022/forty-cdk.mjs.map +1 -1
- package/field/README.md +97 -0
- package/fieldset/README.md +86 -0
- package/file-upload/README.md +73 -0
- package/hover-card/README.md +171 -0
- package/input/README.md +156 -0
- package/listbox/README.md +424 -0
- package/menu/README.md +181 -0
- package/menubar/README.md +140 -0
- package/meter/README.md +128 -0
- package/navigation-menu/README.md +253 -0
- package/number-input/README.md +171 -0
- package/otp-input/README.md +198 -0
- package/package.json +213 -1
- package/pagination/README.md +61 -0
- package/pane-resizer/README.md +136 -0
- package/popover/README.md +262 -0
- package/progress/README.md +115 -0
- package/radio-group/README.md +129 -0
- package/scroll-area/README.md +184 -0
- package/search/README.md +42 -0
- package/select/README.md +488 -0
- package/separator/README.md +84 -0
- package/signal-forms/README.md +72 -0
- package/slider/README.md +152 -0
- package/stepper/README.md +292 -0
- package/switch/README.md +116 -0
- package/table/README.md +769 -0
- package/tabs/README.md +130 -0
- package/time-field/README.md +157 -0
- package/time-picker/README.md +172 -0
- package/toast/README.md +398 -0
- package/toggle/README.md +224 -0
- package/toolbar/README.md +109 -0
- package/tooltip/README.md +274 -0
- package/tree/README.md +708 -0
- package/types/forty-cdk-accordion.d.ts +242 -0
- package/types/forty-cdk-aspect-ratio.d.ts +59 -0
- package/types/forty-cdk-avatar.d.ts +133 -0
- package/types/forty-cdk-breadcrumbs.d.ts +92 -0
- package/types/forty-cdk-breakpoints.d.ts +141 -0
- package/types/forty-cdk-button.d.ts +80 -0
- package/types/forty-cdk-calendar.d.ts +914 -0
- package/types/forty-cdk-carousel.d.ts +530 -0
- package/types/forty-cdk-checkbox.d.ts +141 -0
- package/types/forty-cdk-combobox.d.ts +1259 -0
- package/types/forty-cdk-context-menu.d.ts +313 -0
- package/types/forty-cdk-core.d.ts +5774 -0
- package/types/forty-cdk-date-field.d.ts +307 -0
- package/types/forty-cdk-date-picker.d.ts +622 -0
- package/types/forty-cdk-dialog.d.ts +546 -0
- package/types/forty-cdk-disclosure.d.ts +127 -0
- package/types/forty-cdk-drag-drop.d.ts +456 -0
- package/types/forty-cdk-drawer.d.ts +871 -0
- package/types/forty-cdk-dropdown-menu.d.ts +242 -0
- package/types/forty-cdk-field.d.ts +236 -0
- package/types/forty-cdk-fieldset.d.ts +119 -0
- package/types/forty-cdk-file-upload.d.ts +124 -0
- package/types/forty-cdk-hover-card.d.ts +320 -0
- package/types/forty-cdk-input.d.ts +169 -0
- package/types/forty-cdk-internationalized-date.d.ts +1 -1
- package/types/forty-cdk-listbox.d.ts +513 -0
- package/types/forty-cdk-menu.d.ts +629 -0
- package/types/forty-cdk-menubar.d.ts +451 -0
- package/types/forty-cdk-meter.d.ts +122 -0
- package/types/forty-cdk-navigation-menu.d.ts +514 -0
- package/types/forty-cdk-number-input.d.ts +319 -0
- package/types/forty-cdk-otp-input.d.ts +248 -0
- package/types/forty-cdk-pagination.d.ts +214 -0
- package/types/forty-cdk-pane-resizer.d.ts +145 -0
- package/types/forty-cdk-popover.d.ts +509 -0
- package/types/forty-cdk-progress.d.ts +143 -0
- package/types/forty-cdk-radio-group.d.ts +222 -0
- package/types/forty-cdk-scroll-area.d.ts +258 -0
- package/types/forty-cdk-search.d.ts +142 -0
- package/types/forty-cdk-select.d.ts +899 -0
- package/types/forty-cdk-separator.d.ts +59 -0
- package/types/forty-cdk-signal-forms.d.ts +58 -0
- package/types/forty-cdk-slider.d.ts +379 -0
- package/types/forty-cdk-stepper.d.ts +650 -0
- package/types/forty-cdk-switch.d.ts +87 -0
- package/types/forty-cdk-table.d.ts +723 -0
- package/types/forty-cdk-tabs.d.ts +235 -0
- package/types/forty-cdk-time-field.d.ts +307 -0
- package/types/forty-cdk-time-picker.d.ts +578 -0
- package/types/forty-cdk-toast.d.ts +598 -0
- package/types/forty-cdk-toggle.d.ts +310 -0
- package/types/forty-cdk-toolbar.d.ts +217 -0
- package/types/forty-cdk-tooltip.d.ts +436 -0
- package/types/forty-cdk-tree.d.ts +688 -0
- 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
|