@headless-tree/core 0.0.0-20230802230636

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/lib/cjs/core/create-tree.d.ts +2 -0
  3. package/lib/cjs/core/create-tree.js +138 -0
  4. package/lib/cjs/features/async-data-loader/feature.d.ts +5 -0
  5. package/lib/cjs/features/async-data-loader/feature.js +88 -0
  6. package/lib/cjs/features/async-data-loader/types.d.ts +41 -0
  7. package/lib/cjs/features/async-data-loader/types.js +2 -0
  8. package/lib/cjs/features/drag-and-drop/feature.d.ts +3 -0
  9. package/lib/cjs/features/drag-and-drop/feature.js +138 -0
  10. package/lib/cjs/features/drag-and-drop/types.d.ts +69 -0
  11. package/lib/cjs/features/drag-and-drop/types.js +9 -0
  12. package/lib/cjs/features/drag-and-drop/utils.d.ts +6 -0
  13. package/lib/cjs/features/drag-and-drop/utils.js +79 -0
  14. package/lib/cjs/features/expand-all/feature.d.ts +6 -0
  15. package/lib/cjs/features/expand-all/feature.js +41 -0
  16. package/lib/cjs/features/expand-all/types.d.ts +17 -0
  17. package/lib/cjs/features/expand-all/types.js +2 -0
  18. package/lib/cjs/features/hotkeys-core/feature.d.ts +4 -0
  19. package/lib/cjs/features/hotkeys-core/feature.js +71 -0
  20. package/lib/cjs/features/hotkeys-core/types.d.ts +25 -0
  21. package/lib/cjs/features/hotkeys-core/types.js +2 -0
  22. package/lib/cjs/features/main/types.d.ts +39 -0
  23. package/lib/cjs/features/main/types.js +2 -0
  24. package/lib/cjs/features/renaming/feature.d.ts +5 -0
  25. package/lib/cjs/features/renaming/feature.js +64 -0
  26. package/lib/cjs/features/renaming/types.d.ts +27 -0
  27. package/lib/cjs/features/renaming/types.js +2 -0
  28. package/lib/cjs/features/search/feature.d.ts +5 -0
  29. package/lib/cjs/features/search/feature.js +103 -0
  30. package/lib/cjs/features/search/types.d.ts +33 -0
  31. package/lib/cjs/features/search/types.js +2 -0
  32. package/lib/cjs/features/selection/feature.d.ts +5 -0
  33. package/lib/cjs/features/selection/feature.js +113 -0
  34. package/lib/cjs/features/selection/types.d.ts +21 -0
  35. package/lib/cjs/features/selection/types.js +2 -0
  36. package/lib/cjs/features/sync-data-loader/feature.d.ts +4 -0
  37. package/lib/cjs/features/sync-data-loader/feature.js +14 -0
  38. package/lib/cjs/features/sync-data-loader/types.d.ts +19 -0
  39. package/lib/cjs/features/sync-data-loader/types.js +2 -0
  40. package/lib/cjs/features/tree/feature.d.ts +6 -0
  41. package/lib/cjs/features/tree/feature.js +230 -0
  42. package/lib/cjs/features/tree/types.d.ts +62 -0
  43. package/lib/cjs/features/tree/types.js +2 -0
  44. package/lib/cjs/index.d.ts +23 -0
  45. package/lib/cjs/index.js +39 -0
  46. package/lib/cjs/mddocs-entry.d.ts +21 -0
  47. package/lib/cjs/mddocs-entry.js +17 -0
  48. package/lib/cjs/types/core.d.ts +67 -0
  49. package/lib/cjs/types/core.js +2 -0
  50. package/lib/cjs/types/deep-merge.d.ts +13 -0
  51. package/lib/cjs/types/deep-merge.js +2 -0
  52. package/lib/cjs/utilities/create-on-drop-handler.d.ts +3 -0
  53. package/lib/cjs/utilities/create-on-drop-handler.js +11 -0
  54. package/lib/cjs/utilities/insert-items-at-target.d.ts +3 -0
  55. package/lib/cjs/utilities/insert-items-at-target.js +24 -0
  56. package/lib/cjs/utilities/remove-items-from-parents.d.ts +2 -0
  57. package/lib/cjs/utilities/remove-items-from-parents.js +17 -0
  58. package/lib/cjs/utils.d.ts +6 -0
  59. package/lib/cjs/utils.js +53 -0
  60. package/lib/esm/core/create-tree.d.ts +2 -0
  61. package/lib/esm/core/create-tree.js +134 -0
  62. package/lib/esm/features/async-data-loader/feature.d.ts +5 -0
  63. package/lib/esm/features/async-data-loader/feature.js +85 -0
  64. package/lib/esm/features/async-data-loader/types.d.ts +41 -0
  65. package/lib/esm/features/async-data-loader/types.js +1 -0
  66. package/lib/esm/features/drag-and-drop/feature.d.ts +3 -0
  67. package/lib/esm/features/drag-and-drop/feature.js +135 -0
  68. package/lib/esm/features/drag-and-drop/types.d.ts +69 -0
  69. package/lib/esm/features/drag-and-drop/types.js +6 -0
  70. package/lib/esm/features/drag-and-drop/utils.d.ts +6 -0
  71. package/lib/esm/features/drag-and-drop/utils.js +72 -0
  72. package/lib/esm/features/expand-all/feature.d.ts +6 -0
  73. package/lib/esm/features/expand-all/feature.js +38 -0
  74. package/lib/esm/features/expand-all/types.d.ts +17 -0
  75. package/lib/esm/features/expand-all/types.js +1 -0
  76. package/lib/esm/features/hotkeys-core/feature.d.ts +4 -0
  77. package/lib/esm/features/hotkeys-core/feature.js +68 -0
  78. package/lib/esm/features/hotkeys-core/types.d.ts +25 -0
  79. package/lib/esm/features/hotkeys-core/types.js +1 -0
  80. package/lib/esm/features/main/types.d.ts +39 -0
  81. package/lib/esm/features/main/types.js +1 -0
  82. package/lib/esm/features/renaming/feature.d.ts +5 -0
  83. package/lib/esm/features/renaming/feature.js +61 -0
  84. package/lib/esm/features/renaming/types.d.ts +27 -0
  85. package/lib/esm/features/renaming/types.js +1 -0
  86. package/lib/esm/features/search/feature.d.ts +5 -0
  87. package/lib/esm/features/search/feature.js +100 -0
  88. package/lib/esm/features/search/types.d.ts +33 -0
  89. package/lib/esm/features/search/types.js +1 -0
  90. package/lib/esm/features/selection/feature.d.ts +5 -0
  91. package/lib/esm/features/selection/feature.js +110 -0
  92. package/lib/esm/features/selection/types.d.ts +21 -0
  93. package/lib/esm/features/selection/types.js +1 -0
  94. package/lib/esm/features/sync-data-loader/feature.d.ts +4 -0
  95. package/lib/esm/features/sync-data-loader/feature.js +11 -0
  96. package/lib/esm/features/sync-data-loader/types.d.ts +19 -0
  97. package/lib/esm/features/sync-data-loader/types.js +1 -0
  98. package/lib/esm/features/tree/feature.d.ts +6 -0
  99. package/lib/esm/features/tree/feature.js +227 -0
  100. package/lib/esm/features/tree/types.d.ts +62 -0
  101. package/lib/esm/features/tree/types.js +1 -0
  102. package/lib/esm/index.d.ts +23 -0
  103. package/lib/esm/index.js +23 -0
  104. package/lib/esm/mddocs-entry.d.ts +21 -0
  105. package/lib/esm/mddocs-entry.js +1 -0
  106. package/lib/esm/types/core.d.ts +67 -0
  107. package/lib/esm/types/core.js +1 -0
  108. package/lib/esm/types/deep-merge.d.ts +13 -0
  109. package/lib/esm/types/deep-merge.js +1 -0
  110. package/lib/esm/utilities/create-on-drop-handler.d.ts +3 -0
  111. package/lib/esm/utilities/create-on-drop-handler.js +7 -0
  112. package/lib/esm/utilities/insert-items-at-target.d.ts +3 -0
  113. package/lib/esm/utilities/insert-items-at-target.js +20 -0
  114. package/lib/esm/utilities/remove-items-from-parents.d.ts +2 -0
  115. package/lib/esm/utilities/remove-items-from-parents.js +13 -0
  116. package/lib/esm/utils.d.ts +6 -0
  117. package/lib/esm/utils.js +46 -0
  118. package/package.json +23 -0
  119. package/src/core/create-tree.ts +228 -0
  120. package/src/features/async-data-loader/feature.ts +126 -0
  121. package/src/features/async-data-loader/types.ts +41 -0
  122. package/src/features/drag-and-drop/feature.ts +214 -0
  123. package/src/features/drag-and-drop/types.ts +89 -0
  124. package/src/features/drag-and-drop/utils.ts +117 -0
  125. package/src/features/expand-all/feature.ts +63 -0
  126. package/src/features/expand-all/types.ts +13 -0
  127. package/src/features/hotkeys-core/feature.ts +110 -0
  128. package/src/features/hotkeys-core/types.ts +36 -0
  129. package/src/features/main/types.ts +48 -0
  130. package/src/features/renaming/feature.ts +105 -0
  131. package/src/features/renaming/types.ts +28 -0
  132. package/src/features/search/feature.ts +158 -0
  133. package/src/features/search/types.ts +40 -0
  134. package/src/features/selection/feature.ts +157 -0
  135. package/src/features/selection/types.ts +28 -0
  136. package/src/features/sync-data-loader/feature.ts +41 -0
  137. package/src/features/sync-data-loader/types.ts +20 -0
  138. package/src/features/tree/feature.ts +326 -0
  139. package/src/features/tree/types.ts +78 -0
  140. package/src/index.ts +26 -0
  141. package/src/mddocs-entry.ts +26 -0
  142. package/src/types/core.ts +183 -0
  143. package/src/types/deep-merge.ts +31 -0
  144. package/src/utilities/create-on-drop-handler.ts +14 -0
  145. package/src/utilities/insert-items-at-target.ts +30 -0
  146. package/src/utilities/remove-items-from-parents.ts +21 -0
  147. package/src/utils.ts +68 -0
  148. package/tsconfig.json +7 -0
  149. package/typedoc.json +4 -0
@@ -0,0 +1,40 @@
1
+ import { ItemInstance, SetStateFn } from "../../types/core";
2
+ import { HotkeysCoreDataRef } from "../hotkeys-core/types";
3
+
4
+ export type SearchFeatureDataRef<T = any> = HotkeysCoreDataRef & {
5
+ matchingItems: ItemInstance<T>[];
6
+ searchInput: HTMLInputElement | null;
7
+ };
8
+
9
+ export type SearchFeatureDef<T> = {
10
+ state: {
11
+ search: string | null;
12
+ };
13
+ config: {
14
+ setSearch?: SetStateFn<string | null>;
15
+ onOpenSearch?: () => void;
16
+ onCloseSearch?: () => void;
17
+ onSearchMatchesItems?: (search: string, items: ItemInstance<T>[]) => void;
18
+ isSearchMatchingItem?: (search: string, item: ItemInstance<T>) => boolean;
19
+ };
20
+ treeInstance: {
21
+ setSearch: (search: string | null) => void;
22
+ openSearch: (initialValue?: string) => void;
23
+ closeSearch: () => void;
24
+ isSearchOpen: () => boolean;
25
+ getSearchValue: () => string;
26
+ registerSearchInputElement: (element: HTMLInputElement | null) => void;
27
+ getSearchInputElement: () => HTMLInputElement | null;
28
+ getSearchInputElementProps: () => any;
29
+ getSearchMatchingItems: () => ItemInstance<T>[];
30
+ };
31
+ itemInstance: {
32
+ isMatchingSearch: () => boolean;
33
+ };
34
+ hotkeys:
35
+ | "openSearch"
36
+ | "closeSearch"
37
+ | "submitSearch"
38
+ | "nextSearchItem"
39
+ | "previousSearchItem";
40
+ };
@@ -0,0 +1,157 @@
1
+ import { FeatureImplementation } from "../../types/core";
2
+ import { SelectionFeatureDef } from "./types";
3
+ import { MainFeatureDef } from "../main/types";
4
+ import { TreeFeatureDef } from "../tree/types";
5
+ import { makeStateUpdater } from "../../utils";
6
+
7
+ export const selectionFeature: FeatureImplementation<
8
+ any,
9
+ SelectionFeatureDef<any>,
10
+ MainFeatureDef | TreeFeatureDef<any> | SelectionFeatureDef<any>
11
+ > = {
12
+ key: "selection",
13
+
14
+ getInitialState: (initialState) => ({
15
+ selectedItems: [],
16
+ ...initialState,
17
+ }),
18
+
19
+ getDefaultConfig: (defaultConfig, tree) => ({
20
+ setSelectedItems: makeStateUpdater("selectedItems", tree),
21
+ ...defaultConfig,
22
+ }),
23
+
24
+ stateHandlerNames: {
25
+ selectedItems: "setSelectedItems",
26
+ },
27
+
28
+ createTreeInstance: (prev, instance) => ({
29
+ ...prev,
30
+
31
+ setSelectedItems: (selectedItems) => {
32
+ instance.applySubStateUpdate("selectedItems", selectedItems);
33
+ },
34
+
35
+ // TODO memo
36
+ getSelectedItems: () => {
37
+ return instance.getState().selectedItems.map(instance.getItemInstance);
38
+ },
39
+ }),
40
+
41
+ createItemInstance: (prev, item, tree) => ({
42
+ ...prev,
43
+
44
+ select: () => {
45
+ const { selectedItems } = tree.getState();
46
+ tree.setSelectedItems(
47
+ selectedItems.includes(item.getItemMeta().itemId)
48
+ ? selectedItems
49
+ : [...selectedItems, item.getItemMeta().itemId]
50
+ );
51
+ },
52
+
53
+ deselect: () => {
54
+ const { selectedItems } = tree.getState();
55
+ tree.setSelectedItems(
56
+ selectedItems.filter((id) => id !== item.getItemMeta().itemId)
57
+ );
58
+ },
59
+
60
+ isSelected: () => {
61
+ const { selectedItems } = tree.getState();
62
+ return selectedItems.includes(item.getItemMeta().itemId);
63
+ },
64
+
65
+ selectUpTo: (ctrl: boolean) => {
66
+ const indexA = item.getItemMeta().index;
67
+ // TODO dont use focused item as anchor, but last primary-clicked item
68
+ const indexB = tree.getFocusedItem().getItemMeta().index;
69
+ const [a, b] = indexA < indexB ? [indexA, indexB] : [indexB, indexA];
70
+ const newSelectedItems = tree
71
+ .getItems()
72
+ .slice(a, b + 1)
73
+ .map((treeItem) => treeItem.getItemMeta().itemId);
74
+
75
+ if (!ctrl) {
76
+ tree.setSelectedItems(newSelectedItems);
77
+ return;
78
+ }
79
+
80
+ const { selectedItems } = tree.getState();
81
+ const uniqueSelectedItems = [
82
+ ...new Set([...selectedItems, ...newSelectedItems]),
83
+ ];
84
+ tree.setSelectedItems(uniqueSelectedItems);
85
+ },
86
+
87
+ toggleSelect: () => {
88
+ if (item.isSelected()) {
89
+ item.deselect();
90
+ } else {
91
+ item.select();
92
+ }
93
+ },
94
+
95
+ getProps: () => ({
96
+ ...prev.getProps(),
97
+ "aria-selected": item.isSelected() ? "true" : "false",
98
+ onClick: item.getMemoizedProp("selection/onClick", () => (e) => {
99
+ if (e.shiftKey) {
100
+ item.selectUpTo(e.ctrlKey || e.metaKey);
101
+ } else if (e.ctrlKey || e.metaKey) {
102
+ item.toggleSelect();
103
+ } else {
104
+ tree.setSelectedItems([item.getItemMeta().itemId]);
105
+ }
106
+
107
+ prev.getProps().onClick?.(e);
108
+ }),
109
+ }),
110
+ }),
111
+
112
+ hotkeys: {
113
+ // setSelectedItem: {
114
+ // hotkey: "space",
115
+ // handler: (e, tree) => {
116
+ // tree.setSelectedItems([tree.getFocusedItem().getId()]);
117
+ // },
118
+ // },
119
+ toggleSelectItem: {
120
+ hotkey: "ctrl+space",
121
+ handler: (e, tree) => {
122
+ tree.getFocusedItem().toggleSelect();
123
+ },
124
+ },
125
+ selectUpwards: {
126
+ hotkey: "shift+ArrowUp",
127
+ handler: () => {
128
+ // TODO
129
+ },
130
+ },
131
+ selectDownwards: {
132
+ hotkey: "shift+ArrowDown",
133
+ handler: () => {
134
+ // TODO
135
+ },
136
+ },
137
+ selectUpwardsCtrl: {
138
+ hotkey: "shift+ctrl+ArrowUp",
139
+ handler: () => {
140
+ // TODO
141
+ },
142
+ },
143
+ selectDownwardsCtrl: {
144
+ hotkey: "shift+ctrl+ArrowUp",
145
+ handler: () => {
146
+ // TODO
147
+ },
148
+ },
149
+ selectAll: {
150
+ hotkey: "Control+a",
151
+ preventDefault: true,
152
+ handler: (e, tree) => {
153
+ tree.setSelectedItems(tree.getItems().map((item) => item.getId()));
154
+ },
155
+ },
156
+ },
157
+ };
@@ -0,0 +1,28 @@
1
+ import { ItemInstance, SetStateFn } from "../../types/core";
2
+
3
+ export type SelectionFeatureDef<T> = {
4
+ state: {
5
+ selectedItems: string[];
6
+ };
7
+ config: {
8
+ setSelectedItems?: SetStateFn<string[]>;
9
+ };
10
+ treeInstance: {
11
+ setSelectedItems: (selectedItems: string[]) => void;
12
+ getSelectedItems: () => ItemInstance<T>[];
13
+ };
14
+ itemInstance: {
15
+ select: () => void;
16
+ deselect: () => void;
17
+ toggleSelect: () => void;
18
+ isSelected: () => boolean;
19
+ selectUpTo: (ctrl: boolean) => void;
20
+ };
21
+ hotkeys:
22
+ | "toggleSelectItem"
23
+ | "selectUpwards"
24
+ | "selectDownwards"
25
+ | "selectUpwardsCtrl"
26
+ | "selectDownwardsCtrl"
27
+ | "selectAll";
28
+ };
@@ -0,0 +1,41 @@
1
+ import { FeatureImplementation } from "../../types/core";
2
+ import { SyncDataLoaderFeatureDef } from "./types";
3
+ import { MainFeatureDef } from "../main/types";
4
+ import { makeStateUpdater } from "../../utils";
5
+
6
+ export const syncDataLoaderFeature: FeatureImplementation<
7
+ any,
8
+ SyncDataLoaderFeatureDef<any>,
9
+ MainFeatureDef | SyncDataLoaderFeatureDef<any>
10
+ > = {
11
+ key: "sync-data-loader",
12
+
13
+ getInitialState: (initialState) => ({
14
+ loadingItems: [],
15
+ ...initialState,
16
+ }),
17
+
18
+ getDefaultConfig: (defaultConfig, tree) => ({
19
+ setLoadingItems: makeStateUpdater("loadingItems", tree),
20
+ ...defaultConfig,
21
+ }),
22
+
23
+ stateHandlerNames: {
24
+ loadingItems: "setLoadingItems",
25
+ },
26
+
27
+ createTreeInstance: (prev, instance) => ({
28
+ ...prev,
29
+
30
+ retrieveItemData: (itemId) =>
31
+ instance.getConfig().dataLoader!.getItem(itemId),
32
+
33
+ retrieveChildrenIds: (itemId) =>
34
+ instance.getConfig().dataLoader!.getChildren(itemId),
35
+ }),
36
+
37
+ createItemInstance: (prev) => ({
38
+ ...prev,
39
+ isLoading: () => false,
40
+ }),
41
+ };
@@ -0,0 +1,20 @@
1
+ export type SyncTreeDataLoader<T> = {
2
+ getItem: (itemId: string) => T;
3
+ getChildren: (itemId: string) => string[];
4
+ };
5
+
6
+ export type SyncDataLoaderFeatureDef<T> = {
7
+ state: {};
8
+ config: {
9
+ rootItemId: string;
10
+ dataLoader?: SyncTreeDataLoader<T>;
11
+ };
12
+ treeInstance: {
13
+ retrieveItemData: (itemId: string) => T;
14
+ retrieveChildrenIds: (itemId: string) => string[];
15
+ };
16
+ itemInstance: {
17
+ isLoading: () => boolean;
18
+ };
19
+ hotkeys: never;
20
+ };
@@ -0,0 +1,326 @@
1
+ import { FeatureImplementation, ItemInstance } from "../../types/core";
2
+ import { ItemMeta, TreeFeatureDef, TreeItemDataRef } from "./types";
3
+ import { makeStateUpdater, memo, poll } from "../../utils";
4
+ import { MainFeatureDef } from "../main/types";
5
+ import { HotkeysCoreFeatureDef } from "../hotkeys-core/types";
6
+ import { SyncDataLoaderFeatureDef } from "../sync-data-loader/types";
7
+
8
+ export const treeFeature: FeatureImplementation<
9
+ any,
10
+ TreeFeatureDef<any>,
11
+ | MainFeatureDef
12
+ | TreeFeatureDef<any>
13
+ | HotkeysCoreFeatureDef<any>
14
+ | SyncDataLoaderFeatureDef<any>
15
+ > = {
16
+ key: "tree",
17
+
18
+ getInitialState: (initialState) => ({
19
+ expandedItems: [],
20
+ focusedItem: null,
21
+ ...initialState,
22
+ }),
23
+
24
+ getDefaultConfig: (defaultConfig, tree) => ({
25
+ setExpandedItems: makeStateUpdater("expandedItems", tree),
26
+ setFocusedItem: makeStateUpdater("focusedItem", tree),
27
+ ...defaultConfig,
28
+ }),
29
+
30
+ stateHandlerNames: {
31
+ expandedItems: "setExpandedItems",
32
+ focusedItem: "setFocusedItem",
33
+ },
34
+
35
+ createTreeInstance: (prev, instance) => ({
36
+ ...prev,
37
+
38
+ retrieveItemData: () => {
39
+ throw new Error("No data-loader registered");
40
+ },
41
+
42
+ retrieveChildrenIds: () => {
43
+ throw new Error("No data-loader registered");
44
+ },
45
+
46
+ isItemExpanded: (itemId) =>
47
+ instance.getState().expandedItems.includes(itemId),
48
+
49
+ getItemsMeta: () => {
50
+ const { rootItemId } = instance.getConfig();
51
+ const { expandedItems } = instance.getState();
52
+ const flatItems: ItemMeta[] = [];
53
+
54
+ const recursiveAdd = (
55
+ itemId: string,
56
+ parentId: string,
57
+ level: number,
58
+ setSize: number,
59
+ posInSet: number
60
+ ) => {
61
+ flatItems.push({
62
+ itemId,
63
+ level,
64
+ index: flatItems.length,
65
+ parentId,
66
+ setSize,
67
+ posInSet,
68
+ });
69
+
70
+ if (expandedItems.includes(itemId)) {
71
+ const children = instance.retrieveChildrenIds(itemId) ?? [];
72
+ let i = 0;
73
+ for (const childId of children) {
74
+ recursiveAdd(childId, itemId, level + 1, children.length, i++);
75
+ }
76
+ }
77
+ };
78
+
79
+ const children = instance.retrieveChildrenIds(rootItemId);
80
+ let i = 0;
81
+ for (const itemId of children) {
82
+ recursiveAdd(itemId, rootItemId, 0, children.length, i++);
83
+ }
84
+
85
+ return flatItems;
86
+ },
87
+
88
+ expandItem: (itemId) => {
89
+ if (!instance.getItemInstance(itemId).isFolder()) {
90
+ return;
91
+ }
92
+
93
+ if (instance.getState().loadingItems?.includes(itemId)) {
94
+ return;
95
+ }
96
+
97
+ instance.applySubStateUpdate("expandedItems", (expandedItems) => [
98
+ ...expandedItems,
99
+ itemId,
100
+ ]);
101
+ instance.rebuildTree();
102
+ },
103
+
104
+ collapseItem: (itemId) => {
105
+ if (!instance.getItemInstance(itemId).isFolder()) {
106
+ return;
107
+ }
108
+
109
+ instance.applySubStateUpdate("expandedItems", (expandedItems) =>
110
+ expandedItems.filter((id) => id !== itemId)
111
+ );
112
+ instance.rebuildTree();
113
+ },
114
+
115
+ // TODO memo
116
+ getFocusedItem: () => {
117
+ return (
118
+ instance.getItemInstance(instance.getState().focusedItem ?? "") ??
119
+ instance.getItems()[0]
120
+ );
121
+ },
122
+
123
+ focusItem: (itemId) => {
124
+ instance.applySubStateUpdate("focusedItem", itemId);
125
+ },
126
+
127
+ focusNextItem: () => {
128
+ const { index } = instance.getFocusedItem().getItemMeta();
129
+ const nextIndex = Math.min(index + 1, instance.getItems().length - 1);
130
+ instance.focusItem(instance.getItems()[nextIndex].getId());
131
+ },
132
+
133
+ focusPreviousItem: () => {
134
+ const { index } = instance.getFocusedItem().getItemMeta();
135
+ const nextIndex = Math.max(index - 1, 0);
136
+ instance.focusItem(instance.getItems()[nextIndex].getId());
137
+ },
138
+
139
+ updateDomFocus: () => {
140
+ // Required because if the state is managed outside in react, the state only updated during next render
141
+ setTimeout(async () => {
142
+ const focusedItem = instance.getFocusedItem();
143
+ instance.getConfig().scrollToItem?.(focusedItem);
144
+ await poll(() => focusedItem.getElement() !== null, 20);
145
+ const focusedElement = focusedItem.getElement();
146
+ if (!focusedElement) return;
147
+ focusedElement.focus();
148
+ });
149
+ },
150
+
151
+ getContainerProps: () => ({
152
+ ...prev.getContainerProps?.(),
153
+ role: "tree",
154
+ ariaLabel: "",
155
+ ariaActivedescendant: "",
156
+ }),
157
+ }),
158
+
159
+ createItemInstance: (prev, item, tree) => ({
160
+ ...prev,
161
+ isLoading: () => {
162
+ throw new Error("No data-loader registered");
163
+ },
164
+ scrollTo: async (scrollIntoViewArg) => {
165
+ tree.getConfig().scrollToItem?.(item as any);
166
+ await poll(() => item.getElement() !== null, 20);
167
+ item.getElement()!.scrollIntoView(scrollIntoViewArg);
168
+ },
169
+ getId: () => item.getItemMeta().itemId,
170
+ getProps: () => {
171
+ const itemMeta = item.getItemMeta();
172
+ return {
173
+ ...prev.getProps?.(),
174
+ role: "treeitem",
175
+ "aria-setsize": itemMeta.setSize,
176
+ "aria-posinset": itemMeta.posInSet,
177
+ "aria-selected": "false",
178
+ "aria-label": item.getItemName(),
179
+ "aria-level": itemMeta.level,
180
+ tabIndex: item.isFocused() ? 0 : -1,
181
+ onClick: item.getMemoizedProp("tree/onClick", () => (e) => {
182
+ item.setFocused();
183
+ item.primaryAction();
184
+
185
+ if (e.ctrlKey || e.shiftKey || e.metaKey) {
186
+ return;
187
+ }
188
+
189
+ if (!item.isFolder()) {
190
+ return;
191
+ }
192
+
193
+ if (item.isExpanded()) {
194
+ item.collapse();
195
+ } else {
196
+ item.expand();
197
+ }
198
+ }),
199
+ };
200
+ },
201
+ expand: () => tree.expandItem(item.getItemMeta().itemId),
202
+ collapse: () => tree.collapseItem(item.getItemMeta().itemId),
203
+ getItemData: () => tree.retrieveItemData(item.getItemMeta().itemId),
204
+ isExpanded: () =>
205
+ tree.getState().expandedItems.includes(item.getItemMeta().itemId),
206
+ isFocused: () =>
207
+ tree.getState().focusedItem === item.getItemMeta().itemId ||
208
+ (tree.getState().focusedItem === null && item.getItemMeta().index === 0),
209
+ isFolder: () =>
210
+ item.getItemMeta().level === -1 ||
211
+ tree.getConfig().isItemFolder(item as ItemInstance<any>),
212
+ getItemName: () => {
213
+ const config = tree.getConfig();
214
+ return config.getItemName(item as ItemInstance<any>);
215
+ },
216
+ setFocused: () => tree.focusItem(item.getItemMeta().itemId),
217
+ primaryAction: () =>
218
+ tree.getConfig().onPrimaryAction?.(item as ItemInstance<any>),
219
+ getParent: memo(
220
+ (itemMeta) => {
221
+ for (let i = itemMeta.index - 1; i >= 0; i--) {
222
+ const potentialParent = tree.getItems()[i];
223
+ if (potentialParent.getItemMeta().level < itemMeta.level) {
224
+ return potentialParent;
225
+ }
226
+ }
227
+ return tree.getItemInstance(tree.getConfig().rootItemId);
228
+ },
229
+ () => [item.getItemMeta()]
230
+ ),
231
+ // TODO remove
232
+ getIndexInParent: () => item.getItemMeta().posInSet,
233
+ getChildren: () =>
234
+ tree
235
+ .retrieveChildrenIds(item.getItemMeta().itemId)
236
+ .map((id) => tree.getItemInstance(id)),
237
+ getTree: () => tree as any,
238
+ getItemAbove: () => tree.getItems()[item.getItemMeta().index - 1],
239
+ getItemBelow: () => tree.getItems()[item.getItemMeta().index + 1],
240
+ getMemoizedProp: (name, create, deps) => {
241
+ const data = item.getDataRef<TreeItemDataRef>();
242
+ const memoizedValue = data.current.memoizedValues?.[name];
243
+ if (
244
+ memoizedValue &&
245
+ (!deps ||
246
+ data.current.memoizedDeps?.[name]?.every((d, i) => d === deps![i]))
247
+ ) {
248
+ return memoizedValue;
249
+ }
250
+ data.current.memoizedDeps ??= {};
251
+ data.current.memoizedValues ??= {};
252
+ const value = create();
253
+ data.current.memoizedDeps[name] = deps;
254
+ data.current.memoizedValues[name] = value;
255
+ return value;
256
+ },
257
+ }),
258
+
259
+ hotkeys: {
260
+ focusNextItem: {
261
+ hotkey: "ArrowDown",
262
+ canRepeat: true,
263
+ preventDefault: true,
264
+ isEnabled: (tree) =>
265
+ !(tree.isSearchOpen?.() ?? false) && !tree.getState().dnd,
266
+ handler: (e, tree) => {
267
+ tree.focusNextItem();
268
+ tree.updateDomFocus();
269
+ },
270
+ },
271
+ focusPreviousItem: {
272
+ hotkey: "ArrowUp",
273
+ canRepeat: true,
274
+ preventDefault: true,
275
+ isEnabled: (tree) =>
276
+ !(tree.isSearchOpen?.() ?? false) && !tree.getState().dnd,
277
+ handler: (e, tree) => {
278
+ tree.focusPreviousItem();
279
+ tree.updateDomFocus();
280
+ },
281
+ },
282
+ expandOrDown: {
283
+ hotkey: "ArrowRight",
284
+ canRepeat: true,
285
+ handler: (e, tree) => {
286
+ const item = tree.getFocusedItem();
287
+ if (item.isExpanded() || !item.isFolder()) {
288
+ tree.focusNextItem();
289
+ tree.updateDomFocus();
290
+ } else {
291
+ item.expand();
292
+ }
293
+ },
294
+ },
295
+ collapseOrUp: {
296
+ hotkey: "ArrowLeft",
297
+ canRepeat: true,
298
+ handler: (e, tree) => {
299
+ const item = tree.getFocusedItem();
300
+ if (
301
+ (!item.isExpanded() || !item.isFolder()) &&
302
+ item.getItemMeta().level !== 0
303
+ ) {
304
+ item.getParent()?.setFocused();
305
+ tree.updateDomFocus();
306
+ } else {
307
+ item.collapse();
308
+ }
309
+ },
310
+ },
311
+ focusFirstItem: {
312
+ hotkey: "Home",
313
+ handler: (e, tree) => {
314
+ tree.focusItem(tree.getItems()[0].getId());
315
+ tree.updateDomFocus();
316
+ },
317
+ },
318
+ focusLastItem: {
319
+ hotkey: "End",
320
+ handler: (e, tree) => {
321
+ tree.focusItem(tree.getItems()[tree.getItems().length - 1].getId());
322
+ tree.updateDomFocus();
323
+ },
324
+ },
325
+ },
326
+ };
@@ -0,0 +1,78 @@
1
+ import { ItemInstance, SetStateFn, TreeInstance } from "../../types/core";
2
+
3
+ export type ItemMeta = {
4
+ itemId: string;
5
+ parentId: string;
6
+ level: number;
7
+ index: number;
8
+ setSize: number;
9
+ posInSet: number;
10
+ };
11
+
12
+ export type TreeItemDataRef = {
13
+ memoizedValues: Record<string, any>;
14
+ memoizedDeps: Record<string, any[] | undefined>;
15
+ };
16
+
17
+ export type TreeFeatureDef<T> = {
18
+ state: {
19
+ expandedItems: string[];
20
+ focusedItem: string | null;
21
+ };
22
+ config: {
23
+ isItemFolder: (item: ItemInstance<T>) => boolean;
24
+ getItemName: (item: ItemInstance<T>) => string;
25
+
26
+ onPrimaryAction?: (item: ItemInstance<T>) => void;
27
+ scrollToItem?: (item: ItemInstance<T>) => void;
28
+
29
+ setExpandedItems?: SetStateFn<string[]>;
30
+ setFocusedItem?: SetStateFn<string | null>;
31
+ };
32
+ treeInstance: {
33
+ /** @internal */
34
+ getItemsMeta: () => ItemMeta[];
35
+
36
+ expandItem: (itemId: string) => void;
37
+ collapseItem: (itemId: string) => void;
38
+ isItemExpanded: (itemId: string) => boolean;
39
+
40
+ focusItem: (itemId: string) => void;
41
+ getFocusedItem: () => ItemInstance<any>;
42
+ focusNextItem: () => void;
43
+ focusPreviousItem: () => void;
44
+ updateDomFocus: () => void;
45
+
46
+ getContainerProps: () => Record<string, any>;
47
+ };
48
+ itemInstance: {
49
+ getId: () => string;
50
+ getProps: () => Record<string, any>;
51
+ getItemName: () => string;
52
+ getItemData: () => T;
53
+ expand: () => void;
54
+ collapse: () => void;
55
+ isExpanded: () => boolean;
56
+ isFocused: () => boolean;
57
+ isFolder: () => boolean;
58
+ setFocused: () => void;
59
+ getParent: () => ItemInstance<T>;
60
+ getChildren: () => ItemInstance<T>[];
61
+ getIndexInParent: () => number;
62
+ primaryAction: () => void;
63
+ getTree: () => TreeInstance<T>;
64
+ getItemAbove: () => ItemInstance<T> | null;
65
+ getItemBelow: () => ItemInstance<T> | null;
66
+ getMemoizedProp: <X>(name: string, create: () => X, deps?: any[]) => X;
67
+ scrollTo: (
68
+ scrollIntoViewArg?: boolean | ScrollIntoViewOptions
69
+ ) => Promise<void>;
70
+ };
71
+ hotkeys:
72
+ | "focusNextItem"
73
+ | "focusPreviousItem"
74
+ | "expandOrDown"
75
+ | "collapseOrUp"
76
+ | "focusFirstItem"
77
+ | "focusLastItem";
78
+ };