@anymux/ui-kit 0.1.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/dist/ExplorerLayout-CSIJd7N4.js +105 -0
- package/dist/ExplorerLayout-CSIJd7N4.js.map +1 -0
- package/dist/FileBrowserContext-B6jixa2j.js +11 -0
- package/dist/FileBrowserContext-B6jixa2j.js.map +1 -0
- package/dist/calendar-DSlrbHoj.js +761 -0
- package/dist/calendar-DSlrbHoj.js.map +1 -0
- package/dist/calendar.d.ts +3 -0
- package/dist/calendar.js +3 -0
- package/dist/contacts-DQXTZzHc.js +539 -0
- package/dist/contacts-DQXTZzHc.js.map +1 -0
- package/dist/contacts.d.ts +3 -0
- package/dist/contacts.js +3 -0
- package/dist/file-browser-m5atC3kF.js +6755 -0
- package/dist/file-browser-m5atC3kF.js.map +1 -0
- package/dist/file-browser.d.ts +11 -0
- package/dist/file-browser.js +9 -0
- package/dist/git-B55e6LL-.js +561 -0
- package/dist/git-B55e6LL-.js.map +1 -0
- package/dist/git.d.ts +2 -0
- package/dist/git.js +3 -0
- package/dist/iconMap-V4B8P-Uh.js +206 -0
- package/dist/iconMap-V4B8P-Uh.js.map +1 -0
- package/dist/icons-CIsIOZXR.js +0 -0
- package/dist/icons.d.ts +2 -0
- package/dist/icons.js +4 -0
- package/dist/index-BNmNIWBL.d.ts +71 -0
- package/dist/index-BNmNIWBL.d.ts.map +1 -0
- package/dist/index-Bryv_GCG.d.ts +1481 -0
- package/dist/index-Bryv_GCG.d.ts.map +1 -0
- package/dist/index-CuQIjSXs.d.ts +134 -0
- package/dist/index-CuQIjSXs.d.ts.map +1 -0
- package/dist/index-DSu19mq0.d.ts +153 -0
- package/dist/index-DSu19mq0.d.ts.map +1 -0
- package/dist/index-DmsyeHFr.d.ts +149 -0
- package/dist/index-DmsyeHFr.d.ts.map +1 -0
- package/dist/index-DxnJ8FYM.d.ts +17 -0
- package/dist/index-DxnJ8FYM.d.ts.map +1 -0
- package/dist/index-DzfY1Tok.d.ts +32 -0
- package/dist/index-DzfY1Tok.d.ts.map +1 -0
- package/dist/index-Ml_SgiKa.d.ts +1847 -0
- package/dist/index-Ml_SgiKa.d.ts.map +1 -0
- package/dist/index-kHr9udZD.d.ts +1025 -0
- package/dist/index-kHr9udZD.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +15 -0
- package/dist/layout-Ca_4r8ka.js +89 -0
- package/dist/layout-Ca_4r8ka.js.map +1 -0
- package/dist/layout.d.ts +2 -0
- package/dist/layout.js +5 -0
- package/dist/list-CxfT6hix.js +6831 -0
- package/dist/list-CxfT6hix.js.map +1 -0
- package/dist/list.d.ts +2 -0
- package/dist/list.js +5 -0
- package/dist/media-DZ292aKK.js +557 -0
- package/dist/media-DZ292aKK.js.map +1 -0
- package/dist/media.d.ts +3 -0
- package/dist/media.js +3 -0
- package/dist/tree-Dd9Z0Aso.js +3351 -0
- package/dist/tree-Dd9Z0Aso.js.map +1 -0
- package/dist/tree.d.ts +2 -0
- package/dist/tree.js +6 -0
- package/dist/types-common-CB3kRek8.d.ts +26 -0
- package/dist/types-common-CB3kRek8.d.ts.map +1 -0
- package/dist/utils-B4fdKKsy.js +3 -0
- package/package.json +109 -0
- package/src/calendar/AgendaView.tsx +37 -0
- package/src/calendar/CalendarBrowser.tsx +90 -0
- package/src/calendar/CalendarModel.ts +142 -0
- package/src/calendar/CalendarSidebar.tsx +81 -0
- package/src/calendar/DayView.tsx +76 -0
- package/src/calendar/EventCard.tsx +51 -0
- package/src/calendar/MockCalendarProvider.ts +98 -0
- package/src/calendar/MonthView.tsx +77 -0
- package/src/calendar/WeekView.tsx +129 -0
- package/src/calendar/index.ts +18 -0
- package/src/calendar/types.ts +25 -0
- package/src/contacts/ContactAvatar.tsx +35 -0
- package/src/contacts/ContactBrowser.tsx +56 -0
- package/src/contacts/ContactCard.tsx +37 -0
- package/src/contacts/ContactDetail.tsx +63 -0
- package/src/contacts/ContactGroupSidebar.tsx +40 -0
- package/src/contacts/ContactList.tsx +32 -0
- package/src/contacts/ContactListModel.ts +120 -0
- package/src/contacts/MockContactProvider.ts +77 -0
- package/src/contacts/index.ts +17 -0
- package/src/contacts/types.ts +26 -0
- package/src/demos/CalendarBrowserDemo.tsx +15 -0
- package/src/demos/ContactBrowserDemo.tsx +15 -0
- package/src/demos/MediaBrowserDemo.tsx +15 -0
- package/src/file-browser/adapters/DocumentViewerAdapter.ts +371 -0
- package/src/file-browser/adapters/FileSystemBridge.ts +168 -0
- package/src/file-browser/adapters/GitBrowserAdapter.ts +546 -0
- package/src/file-browser/adapters/README.md +504 -0
- package/src/file-browser/adapters/index.ts +27 -0
- package/src/file-browser/adapters/types.ts +70 -0
- package/src/file-browser/architecture.md +645 -0
- package/src/file-browser/components/CreateItemDialog.tsx +71 -0
- package/src/file-browser/components/DeleteConfirmDialog.tsx +58 -0
- package/src/file-browser/components/FileBrowser.tsx +473 -0
- package/src/file-browser/components/FileBrowserContent.tsx +209 -0
- package/src/file-browser/components/FileBrowserHeader.tsx +151 -0
- package/src/file-browser/components/FileBrowserToolbar.tsx +145 -0
- package/src/file-browser/components/LeftPanel/LeftPanel.tsx +103 -0
- package/src/file-browser/components/LeftPanel/LeftPanelTabs.tsx +70 -0
- package/src/file-browser/components/LeftPanel/TreeNavigationView.tsx +256 -0
- package/src/file-browser/components/PreviewPane.tsx +146 -0
- package/src/file-browser/components/RightPanel/FilePreview.tsx +219 -0
- package/src/file-browser/components/RightPanel/RightPanel.tsx +186 -0
- package/src/file-browser/components/RightPanel/RightPanelToolbar.tsx +113 -0
- package/src/file-browser/components/UploadProgress.tsx +123 -0
- package/src/file-browser/components/ViewerHost.tsx +208 -0
- package/src/file-browser/components/mobile/MobileNavigation.tsx +227 -0
- package/src/file-browser/components/navigation/NavigationButtons.tsx +171 -0
- package/src/file-browser/components/shared/ErrorBoundary.tsx +116 -0
- package/src/file-browser/components/shared/FileBrowserItem.tsx +195 -0
- package/src/file-browser/components/shared/FileIcon.tsx +169 -0
- package/src/file-browser/components/toolbar/ViewModeToggle.tsx +200 -0
- package/src/file-browser/components/views/ListView/ListView.tsx +484 -0
- package/src/file-browser/components/views/ThumbnailView/ThumbnailView.tsx +323 -0
- package/src/file-browser/components/views/TreeView/TreeNode.tsx +186 -0
- package/src/file-browser/components/views/TreeView/TreeNodeList.tsx +191 -0
- package/src/file-browser/components/views/TreeView/TreeView.tsx +200 -0
- package/src/file-browser/components/views/TreemapView/TreemapView.tsx +339 -0
- package/src/file-browser/context/FileBrowserContext.tsx +13 -0
- package/src/file-browser/examples/BasicUsage.tsx +20 -0
- package/src/file-browser/index.ts +98 -0
- package/src/file-browser/models/FileBrowserModel.ts +623 -0
- package/src/file-browser/models/LeftPanelManagerModel.ts +105 -0
- package/src/file-browser/models/NavigationManagerModel.ts +312 -0
- package/src/file-browser/models/ResponsiveLayoutManagerModel.ts +437 -0
- package/src/file-browser/models/RightPanelManagerModel.ts +190 -0
- package/src/file-browser/models/SelectionManagerModel.ts +252 -0
- package/src/file-browser/models/ToolbarManagerModel.ts +144 -0
- package/src/file-browser/models/UploadModel.ts +147 -0
- package/src/file-browser/models/ViewModeManagerModel.ts +185 -0
- package/src/file-browser/models/ViewerHostModel.ts +44 -0
- package/src/file-browser/models/ui/ListViewUIModel.ts +265 -0
- package/src/file-browser/models/ui/PreviewUIModel.ts +297 -0
- package/src/file-browser/models/ui/ThumbnailViewUIModel.ts +254 -0
- package/src/file-browser/models/ui/TreeViewUIModel.ts +128 -0
- package/src/file-browser/models/ui/TreemapViewUIModel.ts +350 -0
- package/src/file-browser/providers/FileSystemListProvider.ts +552 -0
- package/src/file-browser/providers/FileSystemProvider.ts +401 -0
- package/src/file-browser/providers/FileSystemTreeProvider.ts +231 -0
- package/src/file-browser/providers/GitProvider.ts +337 -0
- package/src/file-browser/providers/GitRepositoryProvider.ts +376 -0
- package/src/file-browser/providers/IFileBrowserProvider.ts +56 -0
- package/src/file-browser/providers/MemoryProvider.ts +303 -0
- package/src/file-browser/providers/index.ts +4 -0
- package/src/file-browser/registry/ViewerRegistry.ts +551 -0
- package/src/file-browser/registry/types.ts +144 -0
- package/src/file-browser/scripts/performanceBenchmark.ts +553 -0
- package/src/file-browser/services/ThumbnailCacheService.ts +128 -0
- package/src/file-browser/tasks.md +537 -0
- package/src/file-browser/types/FileBrowserTypes.ts +126 -0
- package/src/file-browser/types/ProviderTypes.ts +155 -0
- package/src/file-browser/types/UITypes.ts +235 -0
- package/src/file-browser/types/ViewModeTypes.ts +150 -0
- package/src/file-browser/utils/gestures.ts +327 -0
- package/src/file-browser/utils/performance.ts +563 -0
- package/src/file-browser/viewers/ImageViewer.tsx +163 -0
- package/src/file-browser/viewers/ImageViewerModel.ts +79 -0
- package/src/file-browser/viewers/TextViewer.tsx +95 -0
- package/src/file-browser/viewers/UnsupportedFileViewer.tsx +57 -0
- package/src/file-browser/viewers/index.ts +61 -0
- package/src/git/BranchList.tsx +128 -0
- package/src/git/CommitGraph.tsx +239 -0
- package/src/git/CommitList.tsx +258 -0
- package/src/git/DiffViewer.tsx +219 -0
- package/src/git/index.ts +4 -0
- package/src/icons/iconMap.ts +146 -0
- package/src/icons/index.ts +9 -0
- package/src/index.ts +13 -0
- package/src/layout/README.md +307 -0
- package/src/layout/components/ExplorerLayout/ExplorerLayout.tsx +178 -0
- package/src/layout/examples/SimpleExample.tsx +60 -0
- package/src/layout/index.ts +6 -0
- package/src/lib/utils.ts +1 -0
- package/src/list/README.md +303 -0
- package/src/list/architecture.md +807 -0
- package/src/list/components/CalculatedGridView.tsx +252 -0
- package/src/list/components/DragPreview.tsx +102 -0
- package/src/list/components/ListContextMenu.tsx +274 -0
- package/src/list/components/ListItem.tsx +761 -0
- package/src/list/components/ListItems.tsx +919 -0
- package/src/list/components/MasonryView.tsx +241 -0
- package/src/list/components/SearchFilter.tsx +44 -0
- package/src/list/components/TreemapView.tsx +709 -0
- package/src/list/components/ViewSizeControls.tsx +205 -0
- package/src/list/components/ViewTypeSelector.tsx +312 -0
- package/src/list/components/VirtualizedDetailsView.tsx +231 -0
- package/src/list/components/VirtualizedGrid.tsx +164 -0
- package/src/list/components/VirtualizedList.tsx +154 -0
- package/src/list/components/VirtualizedMasonryView.tsx +344 -0
- package/src/list/components/shared/EmptyState.tsx +103 -0
- package/src/list/components/shared/ErrorBoundary.tsx +123 -0
- package/src/list/components/shared/ErrorDisplay.tsx +100 -0
- package/src/list/components/shared/ListLoader.tsx +146 -0
- package/src/list/components/shared/LoadingIndicator.tsx +80 -0
- package/src/list/index.ts +92 -0
- package/src/list/models/ListItemsModel.ts +1301 -0
- package/src/list/models/TreemapModel.ts +204 -0
- package/src/list/providers/ListItemsProvider.ts +313 -0
- package/src/list/providers/TestListProvider.ts +604 -0
- package/src/list/tasks.md +937 -0
- package/src/list/types/ListTypes.ts +178 -0
- package/src/list/utils/BenchmarkLogger.ts +243 -0
- package/src/list/utils/DragDropManager.ts +320 -0
- package/src/list/utils/GridLayoutCalculator.ts +290 -0
- package/src/list/utils/ListAccessibility.ts +367 -0
- package/src/list/utils/ListKeyboard.ts +414 -0
- package/src/list/utils/MasonryLayoutCalculator.ts +302 -0
- package/src/list/utils/MasonryLayoutEngine.ts +401 -0
- package/src/list/utils/__tests__/MasonryLayoutEngine.test.ts +157 -0
- package/src/list/utils/__tests__/VirtualizedMasonryView.test.tsx +251 -0
- package/src/media/AlbumSidebar.tsx +48 -0
- package/src/media/MediaBrowser.tsx +92 -0
- package/src/media/MediaBrowserModel.ts +138 -0
- package/src/media/MediaGrid.tsx +50 -0
- package/src/media/MediaList.tsx +49 -0
- package/src/media/MediaPreview.tsx +63 -0
- package/src/media/MediaTimeline.tsx +38 -0
- package/src/media/MockMediaProvider.ts +70 -0
- package/src/media/index.ts +18 -0
- package/src/media/types.ts +21 -0
- package/src/styles/variables.css +60 -0
- package/src/tree/DEVELOPMENT_SUMMARY.md +170 -0
- package/src/tree/__tests__/TreeModel.test.ts +16 -0
- package/src/tree/architecture.md +530 -0
- package/src/tree/components/Tree.tsx +283 -0
- package/src/tree/components/TreeCheckbox.tsx +147 -0
- package/src/tree/components/TreeContextMenu.tsx +139 -0
- package/src/tree/components/TreeNodeList.tsx +329 -0
- package/src/tree/components/TreeTable.tsx +382 -0
- package/src/tree/index.ts +58 -0
- package/src/tree/models/TreeModel.ts +839 -0
- package/src/tree/providers/SimpleTreeProvider.ts +463 -0
- package/src/tree/providers/TestTreeProvider.ts +946 -0
- package/src/tree/providers/TreeProvider.ts +308 -0
- package/src/tree/tasks.md +2046 -0
- package/src/tree/types/TreeTypes.ts +279 -0
- package/src/tree/utils/SelectionTheme.ts +150 -0
- package/src/tree/utils/logger.ts +203 -0
- package/src/types-common.ts +21 -0
|
@@ -0,0 +1,839 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TreeModel - MobX state management for Tree Component
|
|
3
|
+
*
|
|
4
|
+
* This class manages all tree state including nodes, selection, expansion,
|
|
5
|
+
* and loading states. It follows strict MobX patterns:
|
|
6
|
+
* - Uses makeAutoObservable(this, {})
|
|
7
|
+
* - Uses observable.map() for reactive collections
|
|
8
|
+
* - Uses composition over inheritance
|
|
9
|
+
* - All async operations use flow
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { makeAutoObservable, observable, flow } from 'mobx';
|
|
13
|
+
import type { TreeProvider } from '../providers/TreeProvider';
|
|
14
|
+
import type { TreeNodeData, TreeLoadOptions } from '../types/TreeTypes';
|
|
15
|
+
import type { TreeContextMenuItem } from '../types/TreeTypes';
|
|
16
|
+
import type { CheckboxState } from '../components/TreeCheckbox';
|
|
17
|
+
import { logger } from '../utils/logger';
|
|
18
|
+
|
|
19
|
+
export class TreeModel {
|
|
20
|
+
// Core Data - using observable collections
|
|
21
|
+
nodes: TreeNodeData[] = [];
|
|
22
|
+
nodeMap = observable.map<string, TreeNodeData>();
|
|
23
|
+
|
|
24
|
+
// Loading State
|
|
25
|
+
isLoading: boolean = false;
|
|
26
|
+
errors = observable.map<string, Error>();
|
|
27
|
+
|
|
28
|
+
// Selection State - using observable collections
|
|
29
|
+
selectedNodes = observable.map<string, TreeNodeData>();
|
|
30
|
+
focusedNode: string | null = null;
|
|
31
|
+
|
|
32
|
+
// Checkbox Selection State - for 3-state checkboxes
|
|
33
|
+
checkboxStates = observable.map<string, CheckboxState>();
|
|
34
|
+
|
|
35
|
+
// Expansion State - using observable collections
|
|
36
|
+
expandedNodes = observable.map<string, boolean>();
|
|
37
|
+
loadingNodes = observable.map<string, boolean>();
|
|
38
|
+
|
|
39
|
+
// Context menu state
|
|
40
|
+
contextMenuVisible: boolean = false;
|
|
41
|
+
contextMenuPosition: { x: number; y: number } = { x: 0, y: 0 };
|
|
42
|
+
contextMenuItems: TreeContextMenuItem[] = [];
|
|
43
|
+
contextMenuNodes: TreeNodeData[] = [];
|
|
44
|
+
|
|
45
|
+
constructor(public provider: TreeProvider) {
|
|
46
|
+
makeAutoObservable(this, {
|
|
47
|
+
// Most defaults are correct, empty overrides object
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// =====================
|
|
52
|
+
// Computed Properties
|
|
53
|
+
// =====================
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether the tree has any nodes loaded
|
|
57
|
+
*/
|
|
58
|
+
get hasNodes(): boolean {
|
|
59
|
+
return this.nodes.length > 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Total count of nodes currently loaded
|
|
64
|
+
*/
|
|
65
|
+
get nodeCount(): number {
|
|
66
|
+
return this.nodes.length;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether data has been loaded (regardless of success/failure)
|
|
71
|
+
*/
|
|
72
|
+
get isLoaded(): boolean {
|
|
73
|
+
return !this.isLoading && (this.hasNodes || this.errors.size > 0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Array of currently selected nodes (computed from selectedNodes map)
|
|
78
|
+
*/
|
|
79
|
+
get selectedNodesArray(): TreeNodeData[] {
|
|
80
|
+
return Array.from(this.selectedNodes.values());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether any nodes are currently selected
|
|
85
|
+
*/
|
|
86
|
+
get hasSelection(): boolean {
|
|
87
|
+
return this.selectedNodes.size > 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if a specific node is selected
|
|
92
|
+
*/
|
|
93
|
+
isNodeSelected(nodeId: string): boolean {
|
|
94
|
+
return this.selectedNodes.has(nodeId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if a specific node is expanded
|
|
99
|
+
*/
|
|
100
|
+
isNodeExpanded(nodeId: string): boolean {
|
|
101
|
+
return this.expandedNodes.get(nodeId) ?? false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if a specific node is loading
|
|
106
|
+
*/
|
|
107
|
+
isNodeLoading(nodeId: string): boolean {
|
|
108
|
+
return this.loadingNodes.get(nodeId) ?? false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// =====================
|
|
112
|
+
// Async Actions (using flow)
|
|
113
|
+
// =====================
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Load nodes from the provider
|
|
117
|
+
* Uses MobX flow for proper async action handling
|
|
118
|
+
*/
|
|
119
|
+
loadNodes = flow(function* (this: TreeModel, options?: TreeLoadOptions) {
|
|
120
|
+
this.isLoading = true;
|
|
121
|
+
this.errors.clear();
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const result = yield this.provider.loadNodes(options);
|
|
125
|
+
|
|
126
|
+
// Update nodes and nodeMap
|
|
127
|
+
this.nodes = result.nodes;
|
|
128
|
+
|
|
129
|
+
// Update node map for fast lookups
|
|
130
|
+
this.nodeMap.clear();
|
|
131
|
+
result.nodes.forEach((node: TreeNodeData) => this.nodeMap.set(node.id, node));
|
|
132
|
+
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const errorKey = 'loadNodes';
|
|
135
|
+
this.errors.set(errorKey, error instanceof Error ? error : new Error(String(error)));
|
|
136
|
+
} finally {
|
|
137
|
+
this.isLoading = false;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// =====================
|
|
142
|
+
// Selection Actions (placeholder implementations)
|
|
143
|
+
// =====================
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Select a specific node
|
|
147
|
+
* @param node Node to select
|
|
148
|
+
*/
|
|
149
|
+
selectNode(node: TreeNodeData): void {
|
|
150
|
+
const previousSelection = this.selectedNodesArray.map(n => n.id);
|
|
151
|
+
logger.selection('selectNode called', node.id, {
|
|
152
|
+
previouslySelected: previousSelection,
|
|
153
|
+
isMultiSelect: this.provider.isMultiSelectEnabled
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Check if provider allows multi-selection
|
|
157
|
+
if (!this.provider.isMultiSelectEnabled) {
|
|
158
|
+
// Clear existing selection for single-select mode
|
|
159
|
+
logger.selection('clearing selection (single-select mode)', node.id);
|
|
160
|
+
this.selectedNodes.clear();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Add node to selection
|
|
164
|
+
this.selectedNodes.set(node.id, node);
|
|
165
|
+
logger.selection('node added to selection', node.id, {
|
|
166
|
+
totalSelected: this.selectedNodes.size
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Set focused node
|
|
170
|
+
const previousFocus = this.focusedNode;
|
|
171
|
+
this.focusedNode = node.id;
|
|
172
|
+
logger.stateChange('TreeModel', 'focusedNode', previousFocus, this.focusedNode);
|
|
173
|
+
|
|
174
|
+
// Notify provider of selection change (if callback exists)
|
|
175
|
+
if (this.provider.onSelectionChange) {
|
|
176
|
+
logger.providerCall('onSelectionChange', {
|
|
177
|
+
selectedCount: this.selectedNodesArray.length,
|
|
178
|
+
selectionType: this.provider.isMultiSelectEnabled ? 'multi' : 'single'
|
|
179
|
+
});
|
|
180
|
+
this.provider.onSelectionChange({
|
|
181
|
+
selectedNodes: this.selectedNodesArray,
|
|
182
|
+
previousSelection: [], // TODO: Track previous selection in future enhancement
|
|
183
|
+
selectionType: this.provider.isMultiSelectEnabled ? 'multi' : 'single',
|
|
184
|
+
trigger: 'click'
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Deselect a specific node
|
|
191
|
+
* @param node Node to deselect
|
|
192
|
+
*/
|
|
193
|
+
deselectNode(node: TreeNodeData): void {
|
|
194
|
+
logger.selection('deselectNode called', node.id, {
|
|
195
|
+
wasSelected: this.selectedNodes.has(node.id)
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Remove from selection if present
|
|
199
|
+
if (this.selectedNodes.has(node.id)) {
|
|
200
|
+
this.selectedNodes.delete(node.id);
|
|
201
|
+
logger.selection('node removed from selection', node.id, {
|
|
202
|
+
remainingSelected: this.selectedNodes.size
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Clear focus if this was the focused node
|
|
206
|
+
if (this.focusedNode === node.id) {
|
|
207
|
+
const previousFocus = this.focusedNode;
|
|
208
|
+
this.focusedNode = null;
|
|
209
|
+
logger.stateChange('TreeModel', 'focusedNode', previousFocus, this.focusedNode);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Notify provider of selection change (if callback exists)
|
|
213
|
+
if (this.provider.onSelectionChange) {
|
|
214
|
+
logger.providerCall('onSelectionChange', {
|
|
215
|
+
selectedCount: this.selectedNodesArray.length,
|
|
216
|
+
selectionType: this.provider.isMultiSelectEnabled ? 'multi' : 'single'
|
|
217
|
+
});
|
|
218
|
+
this.provider.onSelectionChange({
|
|
219
|
+
selectedNodes: this.selectedNodesArray,
|
|
220
|
+
previousSelection: [], // TODO: Track previous selection in future enhancement
|
|
221
|
+
selectionType: this.provider.isMultiSelectEnabled ? 'multi' : 'single',
|
|
222
|
+
trigger: 'click'
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Clear all selected nodes
|
|
230
|
+
*/
|
|
231
|
+
clearSelection(): void {
|
|
232
|
+
// Only proceed if there are selected nodes
|
|
233
|
+
if (this.selectedNodes.size > 0) {
|
|
234
|
+
this.selectedNodes.clear();
|
|
235
|
+
this.focusedNode = null;
|
|
236
|
+
|
|
237
|
+
// Notify provider of selection change (if callback exists)
|
|
238
|
+
if (this.provider.onSelectionChange) {
|
|
239
|
+
this.provider.onSelectionChange({
|
|
240
|
+
selectedNodes: [],
|
|
241
|
+
previousSelection: [], // TODO: Track previous selection in future enhancement
|
|
242
|
+
selectionType: this.provider.isMultiSelectEnabled ? 'multi' : 'single',
|
|
243
|
+
trigger: 'api'
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Select all nodes (only if multi-select is enabled)
|
|
251
|
+
*/
|
|
252
|
+
selectAll(): void {
|
|
253
|
+
if (!this.provider.isMultiSelectEnabled) {
|
|
254
|
+
return; // Not allowed in single-select mode
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Add all nodes to selection
|
|
258
|
+
this.nodes.forEach(node => {
|
|
259
|
+
this.selectedNodes.set(node.id, node);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Set focus to the first node if any exist
|
|
263
|
+
if (this.nodes.length > 0) {
|
|
264
|
+
const firstNode = this.nodes[0];
|
|
265
|
+
if (firstNode) {
|
|
266
|
+
this.focusedNode = firstNode.id;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Notify provider of selection change (if callback exists)
|
|
271
|
+
if (this.provider.onSelectionChange) {
|
|
272
|
+
this.provider.onSelectionChange({
|
|
273
|
+
selectedNodes: this.selectedNodesArray,
|
|
274
|
+
previousSelection: [], // TODO: Track previous selection in future enhancement
|
|
275
|
+
selectionType: 'multi',
|
|
276
|
+
trigger: 'api'
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// =====================
|
|
282
|
+
// Checkbox Selection Methods
|
|
283
|
+
// =====================
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get checkbox state for a specific node
|
|
287
|
+
*/
|
|
288
|
+
getCheckboxState(nodeId: string): CheckboxState {
|
|
289
|
+
return this.checkboxStates.get(nodeId) ?? 'unchecked';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Handle checkbox click for a node
|
|
294
|
+
*/
|
|
295
|
+
handleCheckboxChange(node: TreeNodeData, newState: CheckboxState): void {
|
|
296
|
+
logger.interaction('handleCheckboxChange called', node.id, {
|
|
297
|
+
newState,
|
|
298
|
+
useCheckboxSelection: this.provider.useCheckboxSelection,
|
|
299
|
+
allowPartialSelection: this.provider.allowPartialSelection,
|
|
300
|
+
currentCheckboxState: this.checkboxStates.get(node.id),
|
|
301
|
+
isCurrentlySelected: this.selectedNodes.has(node.id)
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (!this.provider.useCheckboxSelection) {
|
|
305
|
+
logger.interaction('handleCheckboxChange aborted - useCheckboxSelection is false', node.id);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Set the state for this node
|
|
310
|
+
const previousState = this.checkboxStates.get(node.id);
|
|
311
|
+
this.checkboxStates.set(node.id, newState);
|
|
312
|
+
|
|
313
|
+
logger.stateChange('TreeModel', 'checkboxStates', previousState, newState);
|
|
314
|
+
|
|
315
|
+
// Update selection based on checkbox state
|
|
316
|
+
const wasSelected = this.selectedNodes.has(node.id);
|
|
317
|
+
if (newState === 'checked') {
|
|
318
|
+
this.selectedNodes.set(node.id, node);
|
|
319
|
+
logger.selection('node added via checkbox', node.id, {
|
|
320
|
+
wasSelected,
|
|
321
|
+
nowSelected: true
|
|
322
|
+
});
|
|
323
|
+
} else {
|
|
324
|
+
this.selectedNodes.delete(node.id);
|
|
325
|
+
logger.selection('node removed via checkbox', node.id, {
|
|
326
|
+
wasSelected,
|
|
327
|
+
nowSelected: false
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// If partial selection is allowed, update parent/child relationships
|
|
332
|
+
if (this.provider.allowPartialSelection) {
|
|
333
|
+
logger.debug('Updating parent/child checkbox relationships', {
|
|
334
|
+
component: 'Checkbox',
|
|
335
|
+
nodeId: node.id
|
|
336
|
+
});
|
|
337
|
+
this.updateChildCheckboxes(node, newState);
|
|
338
|
+
this.updateParentCheckboxes(node);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Notify provider of selection change
|
|
342
|
+
if (this.provider.onSelectionChange) {
|
|
343
|
+
const selectionInfo = {
|
|
344
|
+
selectedNodes: this.selectedNodesArray,
|
|
345
|
+
previousSelection: [],
|
|
346
|
+
selectionType: 'multi' as const,
|
|
347
|
+
trigger: 'click' as const
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
logger.providerCall('onSelectionChange', {
|
|
351
|
+
selectedCount: selectionInfo.selectedNodes.length,
|
|
352
|
+
trigger: selectionInfo.trigger
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
this.provider.onSelectionChange(selectionInfo);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Update all child checkboxes when parent state changes
|
|
361
|
+
*/
|
|
362
|
+
private updateChildCheckboxes(node: TreeNodeData, state: CheckboxState): void {
|
|
363
|
+
logger.debug('updateChildCheckboxes called', {
|
|
364
|
+
component: 'Checkbox',
|
|
365
|
+
nodeId: node.id,
|
|
366
|
+
operation: 'update-children'
|
|
367
|
+
}, {
|
|
368
|
+
parentState: state,
|
|
369
|
+
hasChildren: !!node.children,
|
|
370
|
+
childrenCount: node.children?.length || 0
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
if (!node.children || state === 'indeterminate') {
|
|
374
|
+
logger.debug('updateChildCheckboxes skipped', {
|
|
375
|
+
component: 'Checkbox',
|
|
376
|
+
nodeId: node.id
|
|
377
|
+
}, {
|
|
378
|
+
reason: !node.children ? 'no-children' : 'indeterminate-state',
|
|
379
|
+
hasChildren: !!node.children,
|
|
380
|
+
state
|
|
381
|
+
});
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
node.children.forEach(child => {
|
|
386
|
+
const previousState = this.checkboxStates.get(child.id);
|
|
387
|
+
this.checkboxStates.set(child.id, state);
|
|
388
|
+
|
|
389
|
+
logger.debug('updateChildCheckboxes - child updated', {
|
|
390
|
+
component: 'Checkbox',
|
|
391
|
+
nodeId: child.id
|
|
392
|
+
}, {
|
|
393
|
+
previousState,
|
|
394
|
+
newState: state,
|
|
395
|
+
parentNode: node.id
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Update selection
|
|
399
|
+
if (state === 'checked') {
|
|
400
|
+
this.selectedNodes.set(child.id, child);
|
|
401
|
+
logger.selection('child selected via parent', child.id, { parentNode: node.id });
|
|
402
|
+
} else {
|
|
403
|
+
this.selectedNodes.delete(child.id);
|
|
404
|
+
logger.selection('child deselected via parent', child.id, { parentNode: node.id });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Recursively update grandchildren
|
|
408
|
+
this.updateChildCheckboxes(child, state);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Update parent checkbox state based on children
|
|
414
|
+
*/
|
|
415
|
+
private updateParentCheckboxes(node: TreeNodeData): void {
|
|
416
|
+
const parent = this.findParentNode(node.id);
|
|
417
|
+
|
|
418
|
+
logger.debug('updateParentCheckboxes called', {
|
|
419
|
+
component: 'Checkbox',
|
|
420
|
+
nodeId: node.id,
|
|
421
|
+
operation: 'update-parent'
|
|
422
|
+
}, {
|
|
423
|
+
parentFound: !!parent,
|
|
424
|
+
parentId: parent?.id
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (!parent || !parent.children) {
|
|
428
|
+
logger.debug('updateParentCheckboxes skipped', {
|
|
429
|
+
component: 'Checkbox',
|
|
430
|
+
nodeId: node.id
|
|
431
|
+
}, {
|
|
432
|
+
reason: !parent ? 'no-parent' : 'parent-no-children'
|
|
433
|
+
});
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const childStates = parent.children.map(child => this.getCheckboxState(child.id));
|
|
438
|
+
const checkedCount = childStates.filter(state => state === 'checked').length;
|
|
439
|
+
const uncheckedCount = childStates.filter(state => state === 'unchecked').length;
|
|
440
|
+
const indeterminateCount = childStates.filter(state => state === 'indeterminate').length;
|
|
441
|
+
|
|
442
|
+
logger.debug('updateParentCheckboxes - analyzing children', {
|
|
443
|
+
component: 'Checkbox',
|
|
444
|
+
nodeId: parent.id
|
|
445
|
+
}, {
|
|
446
|
+
totalChildren: parent.children.length,
|
|
447
|
+
checkedCount,
|
|
448
|
+
uncheckedCount,
|
|
449
|
+
indeterminateCount,
|
|
450
|
+
childStates
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
let parentState: CheckboxState;
|
|
454
|
+
|
|
455
|
+
if (checkedCount === parent.children.length) {
|
|
456
|
+
// All children checked
|
|
457
|
+
parentState = 'checked';
|
|
458
|
+
} else if (checkedCount === 0 && indeterminateCount === 0) {
|
|
459
|
+
// No children checked or indeterminate
|
|
460
|
+
parentState = 'unchecked';
|
|
461
|
+
} else {
|
|
462
|
+
// Some children checked or indeterminate
|
|
463
|
+
parentState = 'indeterminate';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const previousParentState = this.checkboxStates.get(parent.id);
|
|
467
|
+
this.checkboxStates.set(parent.id, parentState);
|
|
468
|
+
|
|
469
|
+
logger.debug('updateParentCheckboxes - parent state calculated', {
|
|
470
|
+
component: 'Checkbox',
|
|
471
|
+
nodeId: parent.id
|
|
472
|
+
}, {
|
|
473
|
+
previousState: previousParentState,
|
|
474
|
+
newState: parentState,
|
|
475
|
+
logic: checkedCount === parent.children.length ? 'all-checked' :
|
|
476
|
+
(checkedCount === 0 && indeterminateCount === 0) ? 'none-checked' : 'mixed'
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Update selection based on parent state
|
|
480
|
+
if (parentState === 'checked') {
|
|
481
|
+
this.selectedNodes.set(parent.id, parent);
|
|
482
|
+
logger.selection('parent selected via children', parent.id, { newState: parentState });
|
|
483
|
+
} else {
|
|
484
|
+
this.selectedNodes.delete(parent.id);
|
|
485
|
+
logger.selection('parent deselected via children', parent.id, { newState: parentState });
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Recursively update grandparent
|
|
489
|
+
this.updateParentCheckboxes(parent);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Find parent node of a given node ID
|
|
494
|
+
*/
|
|
495
|
+
private findParentNode(nodeId: string): TreeNodeData | null {
|
|
496
|
+
const findParent = (nodes: TreeNodeData[], targetId: string, parentNode: TreeNodeData | null = null): TreeNodeData | null => {
|
|
497
|
+
for (const node of nodes) {
|
|
498
|
+
if (node.id === targetId) {
|
|
499
|
+
return parentNode;
|
|
500
|
+
}
|
|
501
|
+
if (node.children && node.children.length > 0) {
|
|
502
|
+
const parent = findParent(node.children, targetId, node);
|
|
503
|
+
if (parent) return parent;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
return findParent(this.nodes, nodeId);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// =====================
|
|
513
|
+
// Focus Management & Navigation
|
|
514
|
+
// =====================
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Set focus to a specific node
|
|
518
|
+
* @param nodeId ID of node to focus
|
|
519
|
+
*/
|
|
520
|
+
setFocus(nodeId: string): void {
|
|
521
|
+
if (this.nodeMap.has(nodeId)) {
|
|
522
|
+
this.focusedNode = nodeId;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Move focus to next visible node
|
|
528
|
+
*/
|
|
529
|
+
focusNext(): boolean {
|
|
530
|
+
const flatNodes = this.getFlattenedVisibleNodes();
|
|
531
|
+
const currentIndex = flatNodes.findIndex(node => node.id === this.focusedNode);
|
|
532
|
+
|
|
533
|
+
if (currentIndex >= 0 && currentIndex < flatNodes.length - 1) {
|
|
534
|
+
const nextNode = flatNodes[currentIndex + 1];
|
|
535
|
+
if (nextNode) {
|
|
536
|
+
this.focusedNode = nextNode.id;
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Move focus to previous visible node
|
|
545
|
+
*/
|
|
546
|
+
focusPrevious(): boolean {
|
|
547
|
+
const flatNodes = this.getFlattenedVisibleNodes();
|
|
548
|
+
const currentIndex = flatNodes.findIndex(node => node.id === this.focusedNode);
|
|
549
|
+
|
|
550
|
+
if (currentIndex > 0) {
|
|
551
|
+
const prevNode = flatNodes[currentIndex - 1];
|
|
552
|
+
if (prevNode) {
|
|
553
|
+
this.focusedNode = prevNode.id;
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Move focus to first visible node
|
|
562
|
+
*/
|
|
563
|
+
focusFirst(): boolean {
|
|
564
|
+
const flatNodes = this.getFlattenedVisibleNodes();
|
|
565
|
+
if (flatNodes.length > 0) {
|
|
566
|
+
const firstNode = flatNodes[0];
|
|
567
|
+
if (firstNode) {
|
|
568
|
+
this.focusedNode = firstNode.id;
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Move focus to last visible node
|
|
577
|
+
*/
|
|
578
|
+
focusLast(): boolean {
|
|
579
|
+
const flatNodes = this.getFlattenedVisibleNodes();
|
|
580
|
+
if (flatNodes.length > 0) {
|
|
581
|
+
const lastNode = flatNodes[flatNodes.length - 1];
|
|
582
|
+
if (lastNode) {
|
|
583
|
+
this.focusedNode = lastNode.id;
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Get flattened list of all currently visible nodes
|
|
592
|
+
* @private
|
|
593
|
+
*/
|
|
594
|
+
private getFlattenedVisibleNodes(): TreeNodeData[] {
|
|
595
|
+
const result: TreeNodeData[] = [];
|
|
596
|
+
|
|
597
|
+
const processNodes = (nodes: TreeNodeData[]) => {
|
|
598
|
+
for (const node of nodes) {
|
|
599
|
+
result.push(node);
|
|
600
|
+
// Add children if node is expanded
|
|
601
|
+
if (this.isNodeExpanded(node.id) && node.children && node.children.length > 0) {
|
|
602
|
+
processNodes(node.children);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
processNodes(this.nodes);
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// =====================
|
|
612
|
+
// Expansion Actions (placeholder implementations)
|
|
613
|
+
// =====================
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Expand a specific node
|
|
617
|
+
* @param node Node to expand
|
|
618
|
+
*/
|
|
619
|
+
expandNode = flow(function* (this: TreeModel, node: TreeNodeData) {
|
|
620
|
+
logger.expansion('expandNode called', node.id, {
|
|
621
|
+
hasChildren: node.hasChildren,
|
|
622
|
+
currentlyExpanded: this.isNodeExpanded(node.id),
|
|
623
|
+
hasLoadedChildren: !!(node.children && node.children.length > 0)
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// Guard: skip if node no longer exists in the tree (stale reference after rename/delete)
|
|
627
|
+
if (!this.nodeMap.has(node.id)) {
|
|
628
|
+
logger.expansion('expandNode skipped - node not in tree', node.id);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Set expansion state
|
|
633
|
+
this.expandedNodes.set(node.id, true);
|
|
634
|
+
logger.expansion('expansion state set to true', node.id);
|
|
635
|
+
|
|
636
|
+
// Notify provider of expansion (if callback exists)
|
|
637
|
+
if (this.provider.onNodeExpansion) {
|
|
638
|
+
logger.providerCall('onNodeExpansion', { nodeId: node.id, expanded: true });
|
|
639
|
+
this.provider.onNodeExpansion(node, true);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Load children if they haven't been loaded yet
|
|
643
|
+
if (node.hasChildren && (!node.children || node.children.length === 0)) {
|
|
644
|
+
logger.expansion('loading children', node.id, { hasChildren: node.hasChildren });
|
|
645
|
+
this.loadingNodes.set(node.id, true);
|
|
646
|
+
|
|
647
|
+
try {
|
|
648
|
+
logger.providerCall('loadChildren', { nodeId: node.id });
|
|
649
|
+
const result = yield this.provider.loadChildren(node);
|
|
650
|
+
logger.expansion('children loaded successfully', node.id, {
|
|
651
|
+
childCount: result.nodes.length
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// Update the node with children data both in nodeMap and original node
|
|
655
|
+
const existingNode = this.nodeMap.get(node.id);
|
|
656
|
+
if (existingNode) {
|
|
657
|
+
existingNode.children = result.nodes;
|
|
658
|
+
|
|
659
|
+
// Add children to nodeMap for fast lookups
|
|
660
|
+
result.nodes.forEach((child: TreeNodeData) => {
|
|
661
|
+
this.nodeMap.set(child.id, child);
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Also update the original node object to ensure reactivity
|
|
666
|
+
node.children = result.nodes;
|
|
667
|
+
|
|
668
|
+
// Update any references to this node in the main nodes array recursively
|
|
669
|
+
const updateNodeInTree = (nodes: TreeNodeData[]): void => {
|
|
670
|
+
for (const treeNode of nodes) {
|
|
671
|
+
if (treeNode.id === node.id) {
|
|
672
|
+
treeNode.children = result.nodes;
|
|
673
|
+
}
|
|
674
|
+
if (treeNode.children) {
|
|
675
|
+
updateNodeInTree(treeNode.children);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
updateNodeInTree(this.nodes);
|
|
681
|
+
|
|
682
|
+
// IMPORTANT: Apply parent's checkbox state to newly loaded children
|
|
683
|
+
if (this.provider.allowPartialSelection && this.provider.useCheckboxSelection) {
|
|
684
|
+
const parentCheckboxState = this.getCheckboxState(node.id);
|
|
685
|
+
|
|
686
|
+
logger.debug('Applying parent checkbox state to newly loaded children', {
|
|
687
|
+
component: 'Checkbox',
|
|
688
|
+
nodeId: node.id,
|
|
689
|
+
operation: 'children-loaded-inheritance'
|
|
690
|
+
}, {
|
|
691
|
+
parentState: parentCheckboxState,
|
|
692
|
+
childrenCount: result.nodes.length,
|
|
693
|
+
childIds: result.nodes.map((child: TreeNodeData) => child.id)
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
if (parentCheckboxState === 'checked' || parentCheckboxState === 'unchecked') {
|
|
697
|
+
// Apply parent's state to all newly loaded children
|
|
698
|
+
result.nodes.forEach((child: TreeNodeData) => {
|
|
699
|
+
const previousChildState = this.checkboxStates.get(child.id);
|
|
700
|
+
this.checkboxStates.set(child.id, parentCheckboxState);
|
|
701
|
+
|
|
702
|
+
logger.debug('Child checkbox state inherited from parent', {
|
|
703
|
+
component: 'Checkbox',
|
|
704
|
+
nodeId: child.id
|
|
705
|
+
}, {
|
|
706
|
+
parentNode: node.id,
|
|
707
|
+
parentState: parentCheckboxState,
|
|
708
|
+
previousChildState,
|
|
709
|
+
newChildState: parentCheckboxState
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// Update selection state accordingly
|
|
713
|
+
if (parentCheckboxState === 'checked') {
|
|
714
|
+
this.selectedNodes.set(child.id, child);
|
|
715
|
+
logger.selection('child auto-selected via parent inheritance', child.id, {
|
|
716
|
+
parentNode: node.id
|
|
717
|
+
});
|
|
718
|
+
} else {
|
|
719
|
+
this.selectedNodes.delete(child.id);
|
|
720
|
+
logger.selection('child auto-deselected via parent inheritance', child.id, {
|
|
721
|
+
parentNode: node.id
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Recursively apply to grandchildren if they exist
|
|
726
|
+
if (child.children && child.children.length > 0) {
|
|
727
|
+
this.updateChildCheckboxes(child, parentCheckboxState);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
} catch (error) {
|
|
734
|
+
const errorKey = `loadChildren-${node.id}`;
|
|
735
|
+
this.errors.set(errorKey, error instanceof Error ? error : new Error(String(error)));
|
|
736
|
+
|
|
737
|
+
// Collapse node on error
|
|
738
|
+
this.expandedNodes.set(node.id, false);
|
|
739
|
+
} finally {
|
|
740
|
+
this.loadingNodes.delete(node.id);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Collapse a specific node
|
|
747
|
+
* @param node Node to collapse
|
|
748
|
+
*/
|
|
749
|
+
collapseNode(node: TreeNodeData): void {
|
|
750
|
+
logger.expansion('collapseNode called', node.id, {
|
|
751
|
+
currentlyExpanded: this.isNodeExpanded(node.id)
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
// Set collapsed state
|
|
755
|
+
this.expandedNodes.set(node.id, false);
|
|
756
|
+
logger.expansion('expansion state set to false', node.id);
|
|
757
|
+
|
|
758
|
+
// Notify provider of collapse (if callback exists)
|
|
759
|
+
if (this.provider.onNodeExpansion) {
|
|
760
|
+
logger.providerCall('onNodeExpansion', { nodeId: node.id, expanded: false });
|
|
761
|
+
this.provider.onNodeExpansion(node, false);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Toggle expansion state of a specific node
|
|
767
|
+
* @param node Node to toggle
|
|
768
|
+
*/
|
|
769
|
+
toggleExpansion(node: TreeNodeData): void {
|
|
770
|
+
if (this.isNodeExpanded(node.id)) {
|
|
771
|
+
this.collapseNode(node);
|
|
772
|
+
} else {
|
|
773
|
+
this.expandNode(node);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// =====================
|
|
778
|
+
// Context Menu Actions
|
|
779
|
+
// =====================
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Show context menu for a specific node
|
|
783
|
+
* @param node Target node
|
|
784
|
+
* @param event Mouse event
|
|
785
|
+
*/
|
|
786
|
+
showContextMenu(node: TreeNodeData, event: MouseEvent): void {
|
|
787
|
+
// Get context menu items from provider
|
|
788
|
+
const menuItems = this.provider.getNodeContextMenu?.(node) || [];
|
|
789
|
+
|
|
790
|
+
if (menuItems.length === 0) return;
|
|
791
|
+
|
|
792
|
+
// Set context menu state
|
|
793
|
+
this.contextMenuVisible = true;
|
|
794
|
+
this.contextMenuPosition = { x: event.clientX, y: event.clientY };
|
|
795
|
+
this.contextMenuItems = menuItems;
|
|
796
|
+
this.contextMenuNodes = [node];
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Show context menu for multiple selected nodes
|
|
801
|
+
* @param nodes Selected nodes
|
|
802
|
+
* @param event Mouse event
|
|
803
|
+
*/
|
|
804
|
+
showMultiNodeContextMenu(nodes: TreeNodeData[], event: MouseEvent): void {
|
|
805
|
+
// Get context menu items from provider
|
|
806
|
+
const menuItems = this.provider.getMultiNodeContextMenu?.(nodes) || [];
|
|
807
|
+
|
|
808
|
+
if (menuItems.length === 0) return;
|
|
809
|
+
|
|
810
|
+
// Set context menu state
|
|
811
|
+
this.contextMenuVisible = true;
|
|
812
|
+
this.contextMenuPosition = { x: event.clientX, y: event.clientY };
|
|
813
|
+
this.contextMenuItems = menuItems;
|
|
814
|
+
this.contextMenuNodes = nodes;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Hide context menu
|
|
819
|
+
*/
|
|
820
|
+
hideContextMenu(): void {
|
|
821
|
+
this.contextMenuVisible = false;
|
|
822
|
+
this.contextMenuItems = [];
|
|
823
|
+
this.contextMenuNodes = [];
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Handle context menu item click
|
|
828
|
+
* @param menuItem Clicked menu item
|
|
829
|
+
*/
|
|
830
|
+
handleContextMenuAction(menuItem: TreeContextMenuItem): void {
|
|
831
|
+
// Call provider callback if available
|
|
832
|
+
if (this.provider.onContextMenuAction) {
|
|
833
|
+
this.provider.onContextMenuAction(menuItem.id, this.contextMenuNodes);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Hide context menu
|
|
837
|
+
this.hideContextMenu();
|
|
838
|
+
}
|
|
839
|
+
}
|