@headless-tree/core 1.5.0 → 1.6.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/CHANGELOG.md +21 -0
- package/dist/index.d.mts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +64 -20
- package/dist/index.mjs +64 -20
- package/package.json +1 -1
- package/src/features/async-data-loader/feature.ts +28 -0
- package/src/features/async-data-loader/types.ts +6 -0
- package/src/features/checkboxes/feature.ts +24 -12
- package/src/features/hotkeys-core/feature.ts +1 -0
- package/src/features/selection/feature.ts +12 -2
- package/src/features/selection/types.ts +4 -0
- package/src/features/sync-data-loader/feature.ts +1 -0
- package/src/features/sync-data-loader/types.ts +3 -0
- package/src/features/tree/feature.ts +3 -2
- package/src/features/tree/tree.spec.ts +55 -4
- package/src/test-utils/test-tree.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @headless-tree/core
|
|
2
2
|
|
|
3
|
+
## 1.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 297b575: The anchor for shift-selecting (`item.selectUpTo()`) is now the last item that was clicked while not holding shift, or alternatively the focused item if that didn't exist. The previous behavior was always using the focused item as anchor, which doesn't match common multi-select behaviors in similar applications (#176)
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 287fe40: Added missing `aria-expanded` attribute to tree items (thanks to @gordey4doronin for the contribution) (#171)
|
|
12
|
+
- 7158afe: Improve rerendering behavior by skipping changes to the item loading state when items are already loading, and skipping changes to loading state in case of checkbox click propagation if items are loaded immediately (#173)
|
|
13
|
+
|
|
14
|
+
## 1.5.1
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 08f10f1: Fixed an issue where `isFolder` returns incorrect values for leafs if they are not visibly rendered (#166)
|
|
19
|
+
|
|
3
20
|
## 1.5.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
|
@@ -17,6 +34,10 @@
|
|
|
17
34
|
- 597faad: Checkbox propagation is now supported for trees with async data loaders!
|
|
18
35
|
- b0ee382: triggering a data refetch will now always set the loadingItemData/loadingItemChildrens state variable to the associated items if they where not apart of the cache before
|
|
19
36
|
|
|
37
|
+
### Sponsorship appreciation
|
|
38
|
+
|
|
39
|
+
Thanks for [Docmost](https://docmost.com/), who have supported the development of Headless Tree with a sponsor contribution. Docmost is a wiki-software that can be self-hosted or used in cloud. Thank you!
|
|
40
|
+
|
|
20
41
|
## 1.4.0
|
|
21
42
|
|
|
22
43
|
### Minor Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -199,6 +199,9 @@ type MainFeatureDef<T = any> = {
|
|
|
199
199
|
hotkeys: never;
|
|
200
200
|
};
|
|
201
201
|
|
|
202
|
+
interface SelectionDataRef {
|
|
203
|
+
selectUpToAnchorId?: string | null;
|
|
204
|
+
}
|
|
202
205
|
type SelectionFeatureDef<T> = {
|
|
203
206
|
state: {
|
|
204
207
|
selectedItems: string[];
|
|
@@ -276,7 +279,10 @@ type SyncDataLoaderFeatureDef<T> = {
|
|
|
276
279
|
retrieveChildrenIds: (itemId: string, skipFetch?: boolean) => string[];
|
|
277
280
|
};
|
|
278
281
|
itemInstance: {
|
|
282
|
+
/** Returns false. Provided for consistency with async data loader */
|
|
279
283
|
isLoading: () => boolean;
|
|
284
|
+
/** Returns true. Provided for consistency with async data loader */
|
|
285
|
+
hasLoadedData: () => boolean;
|
|
280
286
|
};
|
|
281
287
|
hotkeys: never;
|
|
282
288
|
};
|
|
@@ -284,6 +290,8 @@ type SyncDataLoaderFeatureDef<T> = {
|
|
|
284
290
|
interface AsyncDataLoaderDataRef<T = any> {
|
|
285
291
|
itemData: Record<string, T>;
|
|
286
292
|
childrenIds: Record<string, string[]>;
|
|
293
|
+
loadingDataSubs: Record<string, (() => void)[]>;
|
|
294
|
+
loadingChildrenSubs: Record<string, (() => void)[]>;
|
|
287
295
|
}
|
|
288
296
|
/**
|
|
289
297
|
* @category Async Data Loader/General
|
|
@@ -324,6 +332,7 @@ type AsyncDataLoaderFeatureDef<T> = {
|
|
|
324
332
|
/** Set to undefined to clear cache without triggering automatic refetch. Use @invalidateItemData to clear and triggering refetch. */
|
|
325
333
|
updateCachedData: (data: T | undefined) => void;
|
|
326
334
|
updateCachedChildrenIds: (childrenIds: string[]) => void;
|
|
335
|
+
hasLoadedData: () => boolean;
|
|
327
336
|
isLoading: () => boolean;
|
|
328
337
|
};
|
|
329
338
|
hotkeys: SyncDataLoaderFeatureDef<T>["hotkeys"];
|
|
@@ -601,4 +610,4 @@ declare const isOrderedDragTarget: <T>(dragTarget: DragTarget<T>) => dragTarget
|
|
|
601
610
|
dragLineLevel: number;
|
|
602
611
|
};
|
|
603
612
|
|
|
604
|
-
export { AssistiveDndState, type AsyncDataLoaderDataRef, type AsyncDataLoaderFeatureDef, type CheckboxesFeatureDef, CheckedState, type CustomHotkeysConfig, type DndDataRef, type DndState, type DragAndDropFeatureDef, type DragLineData, type DragTarget, DragTargetPosition, type EmptyFeatureDef, type ExpandAllDataRef, type ExpandAllFeatureDef, type FeatureDef, type FeatureImplementation, type HotkeyConfig, type HotkeyName, type HotkeysConfig, type HotkeysCoreDataRef, type HotkeysCoreFeatureDef, type InstanceBuilder, type ItemInstance, type ItemInstanceOpts, type ItemMeta, type KDndDataRef, type KeyboardDragAndDropFeatureDef, type MainFeatureDef, type PropMemoizationDataRef, type PropMemoizationFeatureDef, type RegisteredFeatures, type RenamingFeatureDef, type SearchFeatureDataRef, type SearchFeatureDef, type SelectionFeatureDef, type SetStateFn, type SyncDataLoaderFeatureDef, type TreeConfig, type TreeDataLoader, type TreeFeatureDef, type TreeInstance, type TreeInstanceOpts, type TreeItemDataRef, type TreeState, type Updater, asyncDataLoaderFeature, buildProxiedInstance, buildStaticInstance, checkboxesFeature, createOnDropHandler, createTree, dragAndDropFeature, expandAllFeature, hotkeysCoreFeature, insertItemsAtTarget, isOrderedDragTarget, keyboardDragAndDropFeature, makeStateUpdater, propMemoizationFeature, removeItemsFromParents, renamingFeature, searchFeature, selectionFeature, syncDataLoaderFeature };
|
|
613
|
+
export { AssistiveDndState, type AsyncDataLoaderDataRef, type AsyncDataLoaderFeatureDef, type CheckboxesFeatureDef, CheckedState, type CustomHotkeysConfig, type DndDataRef, type DndState, type DragAndDropFeatureDef, type DragLineData, type DragTarget, DragTargetPosition, type EmptyFeatureDef, type ExpandAllDataRef, type ExpandAllFeatureDef, type FeatureDef, type FeatureImplementation, type HotkeyConfig, type HotkeyName, type HotkeysConfig, type HotkeysCoreDataRef, type HotkeysCoreFeatureDef, type InstanceBuilder, type ItemInstance, type ItemInstanceOpts, type ItemMeta, type KDndDataRef, type KeyboardDragAndDropFeatureDef, type MainFeatureDef, type PropMemoizationDataRef, type PropMemoizationFeatureDef, type RegisteredFeatures, type RenamingFeatureDef, type SearchFeatureDataRef, type SearchFeatureDef, type SelectionDataRef, type SelectionFeatureDef, type SetStateFn, type SyncDataLoaderFeatureDef, type TreeConfig, type TreeDataLoader, type TreeFeatureDef, type TreeInstance, type TreeInstanceOpts, type TreeItemDataRef, type TreeState, type Updater, asyncDataLoaderFeature, buildProxiedInstance, buildStaticInstance, checkboxesFeature, createOnDropHandler, createTree, dragAndDropFeature, expandAllFeature, hotkeysCoreFeature, insertItemsAtTarget, isOrderedDragTarget, keyboardDragAndDropFeature, makeStateUpdater, propMemoizationFeature, removeItemsFromParents, renamingFeature, searchFeature, selectionFeature, syncDataLoaderFeature };
|
package/dist/index.d.ts
CHANGED
|
@@ -199,6 +199,9 @@ type MainFeatureDef<T = any> = {
|
|
|
199
199
|
hotkeys: never;
|
|
200
200
|
};
|
|
201
201
|
|
|
202
|
+
interface SelectionDataRef {
|
|
203
|
+
selectUpToAnchorId?: string | null;
|
|
204
|
+
}
|
|
202
205
|
type SelectionFeatureDef<T> = {
|
|
203
206
|
state: {
|
|
204
207
|
selectedItems: string[];
|
|
@@ -276,7 +279,10 @@ type SyncDataLoaderFeatureDef<T> = {
|
|
|
276
279
|
retrieveChildrenIds: (itemId: string, skipFetch?: boolean) => string[];
|
|
277
280
|
};
|
|
278
281
|
itemInstance: {
|
|
282
|
+
/** Returns false. Provided for consistency with async data loader */
|
|
279
283
|
isLoading: () => boolean;
|
|
284
|
+
/** Returns true. Provided for consistency with async data loader */
|
|
285
|
+
hasLoadedData: () => boolean;
|
|
280
286
|
};
|
|
281
287
|
hotkeys: never;
|
|
282
288
|
};
|
|
@@ -284,6 +290,8 @@ type SyncDataLoaderFeatureDef<T> = {
|
|
|
284
290
|
interface AsyncDataLoaderDataRef<T = any> {
|
|
285
291
|
itemData: Record<string, T>;
|
|
286
292
|
childrenIds: Record<string, string[]>;
|
|
293
|
+
loadingDataSubs: Record<string, (() => void)[]>;
|
|
294
|
+
loadingChildrenSubs: Record<string, (() => void)[]>;
|
|
287
295
|
}
|
|
288
296
|
/**
|
|
289
297
|
* @category Async Data Loader/General
|
|
@@ -324,6 +332,7 @@ type AsyncDataLoaderFeatureDef<T> = {
|
|
|
324
332
|
/** Set to undefined to clear cache without triggering automatic refetch. Use @invalidateItemData to clear and triggering refetch. */
|
|
325
333
|
updateCachedData: (data: T | undefined) => void;
|
|
326
334
|
updateCachedChildrenIds: (childrenIds: string[]) => void;
|
|
335
|
+
hasLoadedData: () => boolean;
|
|
327
336
|
isLoading: () => boolean;
|
|
328
337
|
};
|
|
329
338
|
hotkeys: SyncDataLoaderFeatureDef<T>["hotkeys"];
|
|
@@ -601,4 +610,4 @@ declare const isOrderedDragTarget: <T>(dragTarget: DragTarget<T>) => dragTarget
|
|
|
601
610
|
dragLineLevel: number;
|
|
602
611
|
};
|
|
603
612
|
|
|
604
|
-
export { AssistiveDndState, type AsyncDataLoaderDataRef, type AsyncDataLoaderFeatureDef, type CheckboxesFeatureDef, CheckedState, type CustomHotkeysConfig, type DndDataRef, type DndState, type DragAndDropFeatureDef, type DragLineData, type DragTarget, DragTargetPosition, type EmptyFeatureDef, type ExpandAllDataRef, type ExpandAllFeatureDef, type FeatureDef, type FeatureImplementation, type HotkeyConfig, type HotkeyName, type HotkeysConfig, type HotkeysCoreDataRef, type HotkeysCoreFeatureDef, type InstanceBuilder, type ItemInstance, type ItemInstanceOpts, type ItemMeta, type KDndDataRef, type KeyboardDragAndDropFeatureDef, type MainFeatureDef, type PropMemoizationDataRef, type PropMemoizationFeatureDef, type RegisteredFeatures, type RenamingFeatureDef, type SearchFeatureDataRef, type SearchFeatureDef, type SelectionFeatureDef, type SetStateFn, type SyncDataLoaderFeatureDef, type TreeConfig, type TreeDataLoader, type TreeFeatureDef, type TreeInstance, type TreeInstanceOpts, type TreeItemDataRef, type TreeState, type Updater, asyncDataLoaderFeature, buildProxiedInstance, buildStaticInstance, checkboxesFeature, createOnDropHandler, createTree, dragAndDropFeature, expandAllFeature, hotkeysCoreFeature, insertItemsAtTarget, isOrderedDragTarget, keyboardDragAndDropFeature, makeStateUpdater, propMemoizationFeature, removeItemsFromParents, renamingFeature, searchFeature, selectionFeature, syncDataLoaderFeature };
|
|
613
|
+
export { AssistiveDndState, type AsyncDataLoaderDataRef, type AsyncDataLoaderFeatureDef, type CheckboxesFeatureDef, CheckedState, type CustomHotkeysConfig, type DndDataRef, type DndState, type DragAndDropFeatureDef, type DragLineData, type DragTarget, DragTargetPosition, type EmptyFeatureDef, type ExpandAllDataRef, type ExpandAllFeatureDef, type FeatureDef, type FeatureImplementation, type HotkeyConfig, type HotkeyName, type HotkeysConfig, type HotkeysCoreDataRef, type HotkeysCoreFeatureDef, type InstanceBuilder, type ItemInstance, type ItemInstanceOpts, type ItemMeta, type KDndDataRef, type KeyboardDragAndDropFeatureDef, type MainFeatureDef, type PropMemoizationDataRef, type PropMemoizationFeatureDef, type RegisteredFeatures, type RenamingFeatureDef, type SearchFeatureDataRef, type SearchFeatureDef, type SelectionDataRef, type SelectionFeatureDef, type SetStateFn, type SyncDataLoaderFeatureDef, type TreeConfig, type TreeDataLoader, type TreeFeatureDef, type TreeInstance, type TreeInstanceOpts, type TreeItemDataRef, type TreeState, type Updater, asyncDataLoaderFeature, buildProxiedInstance, buildStaticInstance, checkboxesFeature, createOnDropHandler, createTree, dragAndDropFeature, expandAllFeature, hotkeysCoreFeature, insertItemsAtTarget, isOrderedDragTarget, keyboardDragAndDropFeature, makeStateUpdater, propMemoizationFeature, removeItemsFromParents, renamingFeature, searchFeature, selectionFeature, syncDataLoaderFeature };
|
package/dist/index.js
CHANGED
|
@@ -251,6 +251,7 @@ var treeFeature = {
|
|
|
251
251
|
"aria-selected": "false",
|
|
252
252
|
"aria-label": item.getItemName(),
|
|
253
253
|
"aria-level": itemMeta.level + 1,
|
|
254
|
+
"aria-expanded": item.isFolder() ? item.isExpanded() : void 0,
|
|
254
255
|
tabIndex: item.isFocused() ? 0 : -1,
|
|
255
256
|
onClick: (e) => {
|
|
256
257
|
item.setFocused();
|
|
@@ -303,7 +304,7 @@ var treeFeature = {
|
|
|
303
304
|
);
|
|
304
305
|
},
|
|
305
306
|
isFocused: ({ tree, item, itemId }) => tree.getState().focusedItem === itemId || tree.getState().focusedItem === null && item.getItemMeta().index === 0,
|
|
306
|
-
isFolder: ({ tree, item }) =>
|
|
307
|
+
isFolder: ({ tree, item, itemId }) => itemId === tree.getConfig().rootItemId || tree.getConfig().isItemFolder(item),
|
|
307
308
|
getItemName: ({ tree, item }) => {
|
|
308
309
|
const config = tree.getConfig();
|
|
309
310
|
return config.getItemName(item);
|
|
@@ -760,7 +761,9 @@ var selectionFeature = {
|
|
|
760
761
|
},
|
|
761
762
|
selectUpTo: ({ tree, item }, ctrl) => {
|
|
762
763
|
const indexA = item.getItemMeta().index;
|
|
763
|
-
const
|
|
764
|
+
const { selectUpToAnchorId } = tree.getDataRef().current;
|
|
765
|
+
const itemB = selectUpToAnchorId ? tree.getItemInstance(selectUpToAnchorId) : tree.getFocusedItem();
|
|
766
|
+
const indexB = itemB.getItemMeta().index;
|
|
764
767
|
const [a, b] = indexA < indexB ? [indexA, indexB] : [indexB, indexA];
|
|
765
768
|
const newSelectedItems = tree.getItems().slice(a, b + 1).map((treeItem) => treeItem.getItemMeta().itemId);
|
|
766
769
|
if (!ctrl) {
|
|
@@ -791,6 +794,9 @@ var selectionFeature = {
|
|
|
791
794
|
} else {
|
|
792
795
|
tree.setSelectedItems([item.getItemMeta().itemId]);
|
|
793
796
|
}
|
|
797
|
+
if (!e.shiftKey) {
|
|
798
|
+
tree.getDataRef().current.selectUpToAnchorId = item.getId();
|
|
799
|
+
}
|
|
794
800
|
(_b = (_a = prev == null ? void 0 : prev()) == null ? void 0 : _a.onClick) == null ? void 0 : _b.call(_a, e);
|
|
795
801
|
}
|
|
796
802
|
})
|
|
@@ -858,8 +864,11 @@ var getAllLoadedDescendants = (tree, itemId, includeFolders = false) => {
|
|
|
858
864
|
return includeFolders ? [itemId, ...descendants] : descendants;
|
|
859
865
|
};
|
|
860
866
|
var getAllDescendants = (tree, itemId, includeFolders = false) => __async(null, null, function* () {
|
|
861
|
-
|
|
862
|
-
if (!
|
|
867
|
+
const item = tree.getItemInstance(itemId);
|
|
868
|
+
if (!item.hasLoadedData()) {
|
|
869
|
+
yield tree.loadItemData(itemId);
|
|
870
|
+
}
|
|
871
|
+
if (!tree.getConfig().isItemFolder(item)) {
|
|
863
872
|
return [itemId];
|
|
864
873
|
}
|
|
865
874
|
const childrenIds = yield tree.loadChildrenIds(itemId);
|
|
@@ -871,17 +880,24 @@ var getAllDescendants = (tree, itemId, includeFolders = false) => __async(null,
|
|
|
871
880
|
return includeFolders ? [itemId, ...descendants] : descendants;
|
|
872
881
|
});
|
|
873
882
|
var withLoadingState = (tree, itemId, callback) => __async(null, null, function* () {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
883
|
+
const prom = callback();
|
|
884
|
+
const immediate = {};
|
|
885
|
+
const firstCompleted = yield Promise.race([prom, immediate]);
|
|
886
|
+
if (firstCompleted !== immediate) {
|
|
887
|
+
tree.applySubStateUpdate("loadingCheckPropagationItems", (items) => [
|
|
888
|
+
...items,
|
|
889
|
+
itemId
|
|
890
|
+
]);
|
|
891
|
+
try {
|
|
892
|
+
yield prom;
|
|
893
|
+
} finally {
|
|
894
|
+
tree.applySubStateUpdate(
|
|
895
|
+
"loadingCheckPropagationItems",
|
|
896
|
+
(items) => items.filter((id) => id !== itemId)
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
} else {
|
|
900
|
+
yield prom;
|
|
885
901
|
}
|
|
886
902
|
});
|
|
887
903
|
var checkboxesFeature = {
|
|
@@ -1007,7 +1023,8 @@ var specialKeys = {
|
|
|
1007
1023
|
minus: /^(NumpadSubtract|Minus)$/,
|
|
1008
1024
|
control: /^(ControlLeft|ControlRight)$/,
|
|
1009
1025
|
shift: /^(ShiftLeft|ShiftRight)$/,
|
|
1010
|
-
metaorcontrol: /^(MetaLeft|MetaRight|ControlLeft|ControlRight)
|
|
1026
|
+
metaorcontrol: /^(MetaLeft|MetaRight|ControlLeft|ControlRight)$/,
|
|
1027
|
+
enter: /^(Enter|NumpadEnter)$/
|
|
1011
1028
|
};
|
|
1012
1029
|
var testHotkeyMatch = (pressedKeys, tree, hotkey) => {
|
|
1013
1030
|
const supposedKeys = hotkey.hotkey.toLowerCase().split("+");
|
|
@@ -1102,16 +1119,27 @@ var hotkeysCoreFeature = {
|
|
|
1102
1119
|
|
|
1103
1120
|
// src/features/async-data-loader/feature.ts
|
|
1104
1121
|
var getDataRef = (tree) => {
|
|
1105
|
-
var _a, _b, _c, _d;
|
|
1122
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1106
1123
|
const dataRef = tree.getDataRef();
|
|
1107
1124
|
(_b = (_a = dataRef.current).itemData) != null ? _b : _a.itemData = {};
|
|
1108
1125
|
(_d = (_c = dataRef.current).childrenIds) != null ? _d : _c.childrenIds = {};
|
|
1126
|
+
(_f = (_e = dataRef.current).loadingDataSubs) != null ? _f : _e.loadingDataSubs = {};
|
|
1127
|
+
(_h = (_g = dataRef.current).loadingChildrenSubs) != null ? _h : _g.loadingChildrenSubs = {};
|
|
1109
1128
|
return dataRef;
|
|
1110
1129
|
};
|
|
1111
1130
|
var loadItemData = (tree, itemId) => __async(null, null, function* () {
|
|
1112
|
-
var _a;
|
|
1131
|
+
var _a, _b;
|
|
1113
1132
|
const config = tree.getConfig();
|
|
1114
1133
|
const dataRef = getDataRef(tree);
|
|
1134
|
+
if (tree.getState().loadingItemData.includes(itemId)) {
|
|
1135
|
+
return new Promise((resolve) => {
|
|
1136
|
+
var _a2, _b2;
|
|
1137
|
+
(_b2 = (_a2 = dataRef.current.loadingDataSubs)[itemId]) != null ? _b2 : _a2[itemId] = [];
|
|
1138
|
+
dataRef.current.loadingDataSubs[itemId].push(() => {
|
|
1139
|
+
resolve(dataRef.current.itemData[itemId]);
|
|
1140
|
+
});
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1115
1143
|
if (!dataRef.current.itemData[itemId]) {
|
|
1116
1144
|
tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
|
|
1117
1145
|
...loadingItemData,
|
|
@@ -1125,13 +1153,23 @@ var loadItemData = (tree, itemId) => __async(null, null, function* () {
|
|
|
1125
1153
|
"loadingItemData",
|
|
1126
1154
|
(loadingItemData) => loadingItemData.filter((id) => id !== itemId)
|
|
1127
1155
|
);
|
|
1156
|
+
(_b = dataRef.current.loadingDataSubs[itemId]) == null ? void 0 : _b.forEach((cb) => cb());
|
|
1128
1157
|
return item;
|
|
1129
1158
|
});
|
|
1130
1159
|
var loadChildrenIds = (tree, itemId) => __async(null, null, function* () {
|
|
1131
|
-
var _a, _b;
|
|
1160
|
+
var _a, _b, _c;
|
|
1132
1161
|
const config = tree.getConfig();
|
|
1133
1162
|
const dataRef = getDataRef(tree);
|
|
1134
1163
|
let childrenIds;
|
|
1164
|
+
if (tree.getState().loadingItemChildrens.includes(itemId)) {
|
|
1165
|
+
return new Promise((resolve) => {
|
|
1166
|
+
var _a2, _b2;
|
|
1167
|
+
(_b2 = (_a2 = dataRef.current.loadingChildrenSubs)[itemId]) != null ? _b2 : _a2[itemId] = [];
|
|
1168
|
+
dataRef.current.loadingChildrenSubs[itemId].push(() => {
|
|
1169
|
+
resolve(dataRef.current.childrenIds[itemId]);
|
|
1170
|
+
});
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1135
1173
|
if (!dataRef.current.childrenIds[itemId]) {
|
|
1136
1174
|
tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [
|
|
1137
1175
|
...loadingItemChildrens,
|
|
@@ -1163,6 +1201,7 @@ var loadChildrenIds = (tree, itemId) => __async(null, null, function* () {
|
|
|
1163
1201
|
"loadingItemChildrens",
|
|
1164
1202
|
(loadingItemChildrens) => loadingItemChildrens.filter((id) => id !== itemId)
|
|
1165
1203
|
);
|
|
1204
|
+
(_c = dataRef.current.loadingChildrenSubs[itemId]) == null ? void 0 : _c.forEach((cb) => cb());
|
|
1166
1205
|
return childrenIds;
|
|
1167
1206
|
});
|
|
1168
1207
|
var asyncDataLoaderFeature = {
|
|
@@ -1239,6 +1278,10 @@ var asyncDataLoaderFeature = {
|
|
|
1239
1278
|
const dataRef = tree.getDataRef();
|
|
1240
1279
|
dataRef.current.itemData[itemId] = data;
|
|
1241
1280
|
tree.rebuildTree();
|
|
1281
|
+
},
|
|
1282
|
+
hasLoadedData: ({ tree, itemId }) => {
|
|
1283
|
+
const dataRef = tree.getDataRef();
|
|
1284
|
+
return dataRef.current.itemData[itemId] !== void 0;
|
|
1242
1285
|
}
|
|
1243
1286
|
}
|
|
1244
1287
|
};
|
|
@@ -1290,7 +1333,8 @@ var syncDataLoaderFeature = {
|
|
|
1290
1333
|
loadChildrenIds: ({ tree }, itemId) => tree.retrieveChildrenIds(itemId)
|
|
1291
1334
|
},
|
|
1292
1335
|
itemInstance: {
|
|
1293
|
-
isLoading: () => false
|
|
1336
|
+
isLoading: () => false,
|
|
1337
|
+
hasLoadedData: () => true
|
|
1294
1338
|
}
|
|
1295
1339
|
};
|
|
1296
1340
|
|
package/dist/index.mjs
CHANGED
|
@@ -207,6 +207,7 @@ var treeFeature = {
|
|
|
207
207
|
"aria-selected": "false",
|
|
208
208
|
"aria-label": item.getItemName(),
|
|
209
209
|
"aria-level": itemMeta.level + 1,
|
|
210
|
+
"aria-expanded": item.isFolder() ? item.isExpanded() : void 0,
|
|
210
211
|
tabIndex: item.isFocused() ? 0 : -1,
|
|
211
212
|
onClick: (e) => {
|
|
212
213
|
item.setFocused();
|
|
@@ -259,7 +260,7 @@ var treeFeature = {
|
|
|
259
260
|
);
|
|
260
261
|
},
|
|
261
262
|
isFocused: ({ tree, item, itemId }) => tree.getState().focusedItem === itemId || tree.getState().focusedItem === null && item.getItemMeta().index === 0,
|
|
262
|
-
isFolder: ({ tree, item }) =>
|
|
263
|
+
isFolder: ({ tree, item, itemId }) => itemId === tree.getConfig().rootItemId || tree.getConfig().isItemFolder(item),
|
|
263
264
|
getItemName: ({ tree, item }) => {
|
|
264
265
|
const config = tree.getConfig();
|
|
265
266
|
return config.getItemName(item);
|
|
@@ -716,7 +717,9 @@ var selectionFeature = {
|
|
|
716
717
|
},
|
|
717
718
|
selectUpTo: ({ tree, item }, ctrl) => {
|
|
718
719
|
const indexA = item.getItemMeta().index;
|
|
719
|
-
const
|
|
720
|
+
const { selectUpToAnchorId } = tree.getDataRef().current;
|
|
721
|
+
const itemB = selectUpToAnchorId ? tree.getItemInstance(selectUpToAnchorId) : tree.getFocusedItem();
|
|
722
|
+
const indexB = itemB.getItemMeta().index;
|
|
720
723
|
const [a, b] = indexA < indexB ? [indexA, indexB] : [indexB, indexA];
|
|
721
724
|
const newSelectedItems = tree.getItems().slice(a, b + 1).map((treeItem) => treeItem.getItemMeta().itemId);
|
|
722
725
|
if (!ctrl) {
|
|
@@ -747,6 +750,9 @@ var selectionFeature = {
|
|
|
747
750
|
} else {
|
|
748
751
|
tree.setSelectedItems([item.getItemMeta().itemId]);
|
|
749
752
|
}
|
|
753
|
+
if (!e.shiftKey) {
|
|
754
|
+
tree.getDataRef().current.selectUpToAnchorId = item.getId();
|
|
755
|
+
}
|
|
750
756
|
(_b = (_a = prev == null ? void 0 : prev()) == null ? void 0 : _a.onClick) == null ? void 0 : _b.call(_a, e);
|
|
751
757
|
}
|
|
752
758
|
})
|
|
@@ -814,8 +820,11 @@ var getAllLoadedDescendants = (tree, itemId, includeFolders = false) => {
|
|
|
814
820
|
return includeFolders ? [itemId, ...descendants] : descendants;
|
|
815
821
|
};
|
|
816
822
|
var getAllDescendants = (tree, itemId, includeFolders = false) => __async(null, null, function* () {
|
|
817
|
-
|
|
818
|
-
if (!
|
|
823
|
+
const item = tree.getItemInstance(itemId);
|
|
824
|
+
if (!item.hasLoadedData()) {
|
|
825
|
+
yield tree.loadItemData(itemId);
|
|
826
|
+
}
|
|
827
|
+
if (!tree.getConfig().isItemFolder(item)) {
|
|
819
828
|
return [itemId];
|
|
820
829
|
}
|
|
821
830
|
const childrenIds = yield tree.loadChildrenIds(itemId);
|
|
@@ -827,17 +836,24 @@ var getAllDescendants = (tree, itemId, includeFolders = false) => __async(null,
|
|
|
827
836
|
return includeFolders ? [itemId, ...descendants] : descendants;
|
|
828
837
|
});
|
|
829
838
|
var withLoadingState = (tree, itemId, callback) => __async(null, null, function* () {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
839
|
+
const prom = callback();
|
|
840
|
+
const immediate = {};
|
|
841
|
+
const firstCompleted = yield Promise.race([prom, immediate]);
|
|
842
|
+
if (firstCompleted !== immediate) {
|
|
843
|
+
tree.applySubStateUpdate("loadingCheckPropagationItems", (items) => [
|
|
844
|
+
...items,
|
|
845
|
+
itemId
|
|
846
|
+
]);
|
|
847
|
+
try {
|
|
848
|
+
yield prom;
|
|
849
|
+
} finally {
|
|
850
|
+
tree.applySubStateUpdate(
|
|
851
|
+
"loadingCheckPropagationItems",
|
|
852
|
+
(items) => items.filter((id) => id !== itemId)
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
} else {
|
|
856
|
+
yield prom;
|
|
841
857
|
}
|
|
842
858
|
});
|
|
843
859
|
var checkboxesFeature = {
|
|
@@ -963,7 +979,8 @@ var specialKeys = {
|
|
|
963
979
|
minus: /^(NumpadSubtract|Minus)$/,
|
|
964
980
|
control: /^(ControlLeft|ControlRight)$/,
|
|
965
981
|
shift: /^(ShiftLeft|ShiftRight)$/,
|
|
966
|
-
metaorcontrol: /^(MetaLeft|MetaRight|ControlLeft|ControlRight)
|
|
982
|
+
metaorcontrol: /^(MetaLeft|MetaRight|ControlLeft|ControlRight)$/,
|
|
983
|
+
enter: /^(Enter|NumpadEnter)$/
|
|
967
984
|
};
|
|
968
985
|
var testHotkeyMatch = (pressedKeys, tree, hotkey) => {
|
|
969
986
|
const supposedKeys = hotkey.hotkey.toLowerCase().split("+");
|
|
@@ -1058,16 +1075,27 @@ var hotkeysCoreFeature = {
|
|
|
1058
1075
|
|
|
1059
1076
|
// src/features/async-data-loader/feature.ts
|
|
1060
1077
|
var getDataRef = (tree) => {
|
|
1061
|
-
var _a, _b, _c, _d;
|
|
1078
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
1062
1079
|
const dataRef = tree.getDataRef();
|
|
1063
1080
|
(_b = (_a = dataRef.current).itemData) != null ? _b : _a.itemData = {};
|
|
1064
1081
|
(_d = (_c = dataRef.current).childrenIds) != null ? _d : _c.childrenIds = {};
|
|
1082
|
+
(_f = (_e = dataRef.current).loadingDataSubs) != null ? _f : _e.loadingDataSubs = {};
|
|
1083
|
+
(_h = (_g = dataRef.current).loadingChildrenSubs) != null ? _h : _g.loadingChildrenSubs = {};
|
|
1065
1084
|
return dataRef;
|
|
1066
1085
|
};
|
|
1067
1086
|
var loadItemData = (tree, itemId) => __async(null, null, function* () {
|
|
1068
|
-
var _a;
|
|
1087
|
+
var _a, _b;
|
|
1069
1088
|
const config = tree.getConfig();
|
|
1070
1089
|
const dataRef = getDataRef(tree);
|
|
1090
|
+
if (tree.getState().loadingItemData.includes(itemId)) {
|
|
1091
|
+
return new Promise((resolve) => {
|
|
1092
|
+
var _a2, _b2;
|
|
1093
|
+
(_b2 = (_a2 = dataRef.current.loadingDataSubs)[itemId]) != null ? _b2 : _a2[itemId] = [];
|
|
1094
|
+
dataRef.current.loadingDataSubs[itemId].push(() => {
|
|
1095
|
+
resolve(dataRef.current.itemData[itemId]);
|
|
1096
|
+
});
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1071
1099
|
if (!dataRef.current.itemData[itemId]) {
|
|
1072
1100
|
tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
|
|
1073
1101
|
...loadingItemData,
|
|
@@ -1081,13 +1109,23 @@ var loadItemData = (tree, itemId) => __async(null, null, function* () {
|
|
|
1081
1109
|
"loadingItemData",
|
|
1082
1110
|
(loadingItemData) => loadingItemData.filter((id) => id !== itemId)
|
|
1083
1111
|
);
|
|
1112
|
+
(_b = dataRef.current.loadingDataSubs[itemId]) == null ? void 0 : _b.forEach((cb) => cb());
|
|
1084
1113
|
return item;
|
|
1085
1114
|
});
|
|
1086
1115
|
var loadChildrenIds = (tree, itemId) => __async(null, null, function* () {
|
|
1087
|
-
var _a, _b;
|
|
1116
|
+
var _a, _b, _c;
|
|
1088
1117
|
const config = tree.getConfig();
|
|
1089
1118
|
const dataRef = getDataRef(tree);
|
|
1090
1119
|
let childrenIds;
|
|
1120
|
+
if (tree.getState().loadingItemChildrens.includes(itemId)) {
|
|
1121
|
+
return new Promise((resolve) => {
|
|
1122
|
+
var _a2, _b2;
|
|
1123
|
+
(_b2 = (_a2 = dataRef.current.loadingChildrenSubs)[itemId]) != null ? _b2 : _a2[itemId] = [];
|
|
1124
|
+
dataRef.current.loadingChildrenSubs[itemId].push(() => {
|
|
1125
|
+
resolve(dataRef.current.childrenIds[itemId]);
|
|
1126
|
+
});
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1091
1129
|
if (!dataRef.current.childrenIds[itemId]) {
|
|
1092
1130
|
tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [
|
|
1093
1131
|
...loadingItemChildrens,
|
|
@@ -1119,6 +1157,7 @@ var loadChildrenIds = (tree, itemId) => __async(null, null, function* () {
|
|
|
1119
1157
|
"loadingItemChildrens",
|
|
1120
1158
|
(loadingItemChildrens) => loadingItemChildrens.filter((id) => id !== itemId)
|
|
1121
1159
|
);
|
|
1160
|
+
(_c = dataRef.current.loadingChildrenSubs[itemId]) == null ? void 0 : _c.forEach((cb) => cb());
|
|
1122
1161
|
return childrenIds;
|
|
1123
1162
|
});
|
|
1124
1163
|
var asyncDataLoaderFeature = {
|
|
@@ -1195,6 +1234,10 @@ var asyncDataLoaderFeature = {
|
|
|
1195
1234
|
const dataRef = tree.getDataRef();
|
|
1196
1235
|
dataRef.current.itemData[itemId] = data;
|
|
1197
1236
|
tree.rebuildTree();
|
|
1237
|
+
},
|
|
1238
|
+
hasLoadedData: ({ tree, itemId }) => {
|
|
1239
|
+
const dataRef = tree.getDataRef();
|
|
1240
|
+
return dataRef.current.itemData[itemId] !== void 0;
|
|
1198
1241
|
}
|
|
1199
1242
|
}
|
|
1200
1243
|
};
|
|
@@ -1246,7 +1289,8 @@ var syncDataLoaderFeature = {
|
|
|
1246
1289
|
loadChildrenIds: ({ tree }, itemId) => tree.retrieveChildrenIds(itemId)
|
|
1247
1290
|
},
|
|
1248
1291
|
itemInstance: {
|
|
1249
|
-
isLoading: () => false
|
|
1292
|
+
isLoading: () => false,
|
|
1293
|
+
hasLoadedData: () => true
|
|
1250
1294
|
}
|
|
1251
1295
|
};
|
|
1252
1296
|
|
package/package.json
CHANGED
|
@@ -6,6 +6,8 @@ const getDataRef = <T>(tree: TreeInstance<T>) => {
|
|
|
6
6
|
const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
|
|
7
7
|
dataRef.current.itemData ??= {};
|
|
8
8
|
dataRef.current.childrenIds ??= {};
|
|
9
|
+
dataRef.current.loadingDataSubs ??= {};
|
|
10
|
+
dataRef.current.loadingChildrenSubs ??= {};
|
|
9
11
|
return dataRef;
|
|
10
12
|
};
|
|
11
13
|
|
|
@@ -13,6 +15,16 @@ const loadItemData = async <T>(tree: TreeInstance<T>, itemId: string) => {
|
|
|
13
15
|
const config = tree.getConfig();
|
|
14
16
|
const dataRef = getDataRef(tree);
|
|
15
17
|
|
|
18
|
+
if (tree.getState().loadingItemData.includes(itemId)) {
|
|
19
|
+
// if currently loading, await existing load
|
|
20
|
+
return new Promise<T>((resolve) => {
|
|
21
|
+
dataRef.current.loadingDataSubs[itemId] ??= [];
|
|
22
|
+
dataRef.current.loadingDataSubs[itemId].push(() => {
|
|
23
|
+
resolve(dataRef.current.itemData[itemId]);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
if (!dataRef.current.itemData[itemId]) {
|
|
17
29
|
tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
|
|
18
30
|
...loadingItemData,
|
|
@@ -26,6 +38,7 @@ const loadItemData = async <T>(tree: TreeInstance<T>, itemId: string) => {
|
|
|
26
38
|
tree.applySubStateUpdate("loadingItemData", (loadingItemData) =>
|
|
27
39
|
loadingItemData.filter((id) => id !== itemId),
|
|
28
40
|
);
|
|
41
|
+
dataRef.current.loadingDataSubs[itemId]?.forEach((cb) => cb());
|
|
29
42
|
|
|
30
43
|
return item;
|
|
31
44
|
};
|
|
@@ -35,6 +48,16 @@ const loadChildrenIds = async <T>(tree: TreeInstance<T>, itemId: string) => {
|
|
|
35
48
|
const dataRef = getDataRef(tree);
|
|
36
49
|
let childrenIds: string[];
|
|
37
50
|
|
|
51
|
+
if (tree.getState().loadingItemChildrens.includes(itemId)) {
|
|
52
|
+
// if currently loading, await existing load
|
|
53
|
+
return new Promise<string[]>((resolve) => {
|
|
54
|
+
dataRef.current.loadingChildrenSubs[itemId] ??= [];
|
|
55
|
+
dataRef.current.loadingChildrenSubs[itemId].push(() => {
|
|
56
|
+
resolve(dataRef.current.childrenIds[itemId]);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
38
61
|
if (!dataRef.current.childrenIds[itemId]) {
|
|
39
62
|
tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [
|
|
40
63
|
...loadingItemChildrens,
|
|
@@ -66,6 +89,7 @@ const loadChildrenIds = async <T>(tree: TreeInstance<T>, itemId: string) => {
|
|
|
66
89
|
tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) =>
|
|
67
90
|
loadingItemChildrens.filter((id) => id !== itemId),
|
|
68
91
|
);
|
|
92
|
+
dataRef.current.loadingChildrenSubs[itemId]?.forEach((cb) => cb());
|
|
69
93
|
|
|
70
94
|
return childrenIds;
|
|
71
95
|
};
|
|
@@ -166,5 +190,9 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
|
|
|
166
190
|
dataRef.current.itemData[itemId] = data;
|
|
167
191
|
tree.rebuildTree();
|
|
168
192
|
},
|
|
193
|
+
hasLoadedData: ({ tree, itemId }) => {
|
|
194
|
+
const dataRef = tree.getDataRef<AsyncDataLoaderDataRef>();
|
|
195
|
+
return dataRef.current.itemData[itemId] !== undefined;
|
|
196
|
+
},
|
|
169
197
|
},
|
|
170
198
|
};
|
|
@@ -4,6 +4,11 @@ import { SyncDataLoaderFeatureDef } from "../sync-data-loader/types";
|
|
|
4
4
|
export interface AsyncDataLoaderDataRef<T = any> {
|
|
5
5
|
itemData: Record<string, T>;
|
|
6
6
|
childrenIds: Record<string, string[]>;
|
|
7
|
+
|
|
8
|
+
// If an item load is requested while it is already loading, we reuse the existing load promise
|
|
9
|
+
// and store callbacks to be called when the load completes
|
|
10
|
+
loadingDataSubs: Record<string, (() => void)[]>;
|
|
11
|
+
loadingChildrenSubs: Record<string, (() => void)[]>;
|
|
7
12
|
}
|
|
8
13
|
|
|
9
14
|
/**
|
|
@@ -50,6 +55,7 @@ export type AsyncDataLoaderFeatureDef<T> = {
|
|
|
50
55
|
/** Set to undefined to clear cache without triggering automatic refetch. Use @invalidateItemData to clear and triggering refetch. */
|
|
51
56
|
updateCachedData: (data: T | undefined) => void;
|
|
52
57
|
updateCachedChildrenIds: (childrenIds: string[]) => void;
|
|
58
|
+
hasLoadedData: () => boolean;
|
|
53
59
|
isLoading: () => boolean;
|
|
54
60
|
};
|
|
55
61
|
hotkeys: SyncDataLoaderFeatureDef<T>["hotkeys"];
|
|
@@ -22,8 +22,11 @@ const getAllDescendants = async <T>(
|
|
|
22
22
|
itemId: string,
|
|
23
23
|
includeFolders = false,
|
|
24
24
|
): Promise<string[]> => {
|
|
25
|
-
|
|
26
|
-
if (!
|
|
25
|
+
const item = tree.getItemInstance(itemId);
|
|
26
|
+
if (!item.hasLoadedData()) {
|
|
27
|
+
await tree.loadItemData(itemId);
|
|
28
|
+
}
|
|
29
|
+
if (!tree.getConfig().isItemFolder(item)) {
|
|
27
30
|
return [itemId];
|
|
28
31
|
}
|
|
29
32
|
const childrenIds = await tree.loadChildrenIds(itemId);
|
|
@@ -42,16 +45,25 @@ const withLoadingState = async <T>(
|
|
|
42
45
|
itemId: string,
|
|
43
46
|
callback: () => Promise<void>,
|
|
44
47
|
) => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
const prom = callback();
|
|
49
|
+
const immediate = {};
|
|
50
|
+
const firstCompleted = await Promise.race([prom, immediate]);
|
|
51
|
+
if (firstCompleted !== immediate) {
|
|
52
|
+
// don't update loading state if load is immediate
|
|
53
|
+
tree.applySubStateUpdate("loadingCheckPropagationItems", (items) => [
|
|
54
|
+
...items,
|
|
55
|
+
itemId,
|
|
56
|
+
]);
|
|
57
|
+
try {
|
|
58
|
+
await prom;
|
|
59
|
+
} finally {
|
|
60
|
+
tree.applySubStateUpdate("loadingCheckPropagationItems", (items) =>
|
|
61
|
+
items.filter((id) => id !== itemId),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
// immediately completed, await anyway to keep errors in the same execution flow
|
|
66
|
+
await prom;
|
|
55
67
|
}
|
|
56
68
|
};
|
|
57
69
|
|
|
@@ -14,6 +14,7 @@ const specialKeys: Record<string, RegExp> = {
|
|
|
14
14
|
control: /^(ControlLeft|ControlRight)$/,
|
|
15
15
|
shift: /^(ShiftLeft|ShiftRight)$/,
|
|
16
16
|
metaorcontrol: /^(MetaLeft|MetaRight|ControlLeft|ControlRight)$/,
|
|
17
|
+
enter: /^(Enter|NumpadEnter)$/,
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
const testHotkeyMatch = (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FeatureImplementation } from "../../types/core";
|
|
2
2
|
import { makeStateUpdater } from "../../utils";
|
|
3
|
+
import type { SelectionDataRef } from "./types";
|
|
3
4
|
|
|
4
5
|
export const selectionFeature: FeatureImplementation = {
|
|
5
6
|
key: "selection",
|
|
@@ -50,8 +51,12 @@ export const selectionFeature: FeatureImplementation = {
|
|
|
50
51
|
|
|
51
52
|
selectUpTo: ({ tree, item }, ctrl: boolean) => {
|
|
52
53
|
const indexA = item.getItemMeta().index;
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
const { selectUpToAnchorId } =
|
|
55
|
+
tree.getDataRef<SelectionDataRef>().current;
|
|
56
|
+
const itemB = selectUpToAnchorId
|
|
57
|
+
? tree.getItemInstance(selectUpToAnchorId)
|
|
58
|
+
: tree.getFocusedItem();
|
|
59
|
+
const indexB = itemB.getItemMeta().index;
|
|
55
60
|
const [a, b] = indexA < indexB ? [indexA, indexB] : [indexB, indexA];
|
|
56
61
|
const newSelectedItems = tree
|
|
57
62
|
.getItems()
|
|
@@ -90,6 +95,11 @@ export const selectionFeature: FeatureImplementation = {
|
|
|
90
95
|
tree.setSelectedItems([item.getItemMeta().itemId]);
|
|
91
96
|
}
|
|
92
97
|
|
|
98
|
+
if (!e.shiftKey) {
|
|
99
|
+
tree.getDataRef<SelectionDataRef>().current.selectUpToAnchorId =
|
|
100
|
+
item.getId();
|
|
101
|
+
}
|
|
102
|
+
|
|
93
103
|
prev?.()?.onClick?.(e);
|
|
94
104
|
},
|
|
95
105
|
}),
|
|
@@ -27,7 +27,10 @@ export type SyncDataLoaderFeatureDef<T> = {
|
|
|
27
27
|
retrieveChildrenIds: (itemId: string, skipFetch?: boolean) => string[];
|
|
28
28
|
};
|
|
29
29
|
itemInstance: {
|
|
30
|
+
/** Returns false. Provided for consistency with async data loader */
|
|
30
31
|
isLoading: () => boolean;
|
|
32
|
+
/** Returns true. Provided for consistency with async data loader */
|
|
33
|
+
hasLoadedData: () => boolean;
|
|
31
34
|
};
|
|
32
35
|
hotkeys: never;
|
|
33
36
|
};
|
|
@@ -144,6 +144,7 @@ export const treeFeature: FeatureImplementation<any> = {
|
|
|
144
144
|
"aria-selected": "false",
|
|
145
145
|
"aria-label": item.getItemName(),
|
|
146
146
|
"aria-level": itemMeta.level + 1,
|
|
147
|
+
"aria-expanded": item.isFolder() ? item.isExpanded() : undefined,
|
|
147
148
|
tabIndex: item.isFocused() ? 0 : -1,
|
|
148
149
|
onClick: (e: MouseEvent) => {
|
|
149
150
|
item.setFocused();
|
|
@@ -203,8 +204,8 @@ export const treeFeature: FeatureImplementation<any> = {
|
|
|
203
204
|
isFocused: ({ tree, item, itemId }) =>
|
|
204
205
|
tree.getState().focusedItem === itemId ||
|
|
205
206
|
(tree.getState().focusedItem === null && item.getItemMeta().index === 0),
|
|
206
|
-
isFolder: ({ tree, item }) =>
|
|
207
|
-
|
|
207
|
+
isFolder: ({ tree, item, itemId }) =>
|
|
208
|
+
itemId === tree.getConfig().rootItemId ||
|
|
208
209
|
tree.getConfig().isItemFolder(item as ItemInstance<any>),
|
|
209
210
|
getItemName: ({ tree, item }) => {
|
|
210
211
|
const config = tree.getConfig();
|
|
@@ -4,8 +4,8 @@ import { propMemoizationFeature } from "../prop-memoization/feature";
|
|
|
4
4
|
|
|
5
5
|
const factory = TestTree.default({}).withFeatures(propMemoizationFeature);
|
|
6
6
|
|
|
7
|
-
describe("core-feature/
|
|
8
|
-
factory.forSuits((tree) => {
|
|
7
|
+
describe("core-feature/tree", () => {
|
|
8
|
+
factory.forSuits((tree, title) => {
|
|
9
9
|
describe("expanded items", () => {
|
|
10
10
|
it("can expand via tree instance", () => {
|
|
11
11
|
const setExpandedItems = tree.mockedHandler("setExpandedItems");
|
|
@@ -156,6 +156,17 @@ describe("core-feature/selections", () => {
|
|
|
156
156
|
});
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
+
it("unloaded item", () => {
|
|
160
|
+
expect(tree.instance.getItemInstance("x444").getItemMeta()).toEqual({
|
|
161
|
+
index: -1,
|
|
162
|
+
itemId: "x444",
|
|
163
|
+
level: -1,
|
|
164
|
+
parentId: null,
|
|
165
|
+
posInSet: 0,
|
|
166
|
+
setSize: 1,
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
159
170
|
it("expanded container", () => {
|
|
160
171
|
expect(tree.instance.getItemInstance("x11").getItemMeta()).toEqual({
|
|
161
172
|
index: 1,
|
|
@@ -239,6 +250,7 @@ describe("core-feature/selections", () => {
|
|
|
239
250
|
"aria-posinset": 2,
|
|
240
251
|
"aria-selected": "false",
|
|
241
252
|
"aria-setsize": 4,
|
|
253
|
+
"aria-expanded": false,
|
|
242
254
|
onClick: expect.any(Function),
|
|
243
255
|
ref: expect.any(Function),
|
|
244
256
|
role: "treeitem",
|
|
@@ -253,12 +265,29 @@ describe("core-feature/selections", () => {
|
|
|
253
265
|
"aria-posinset": 1,
|
|
254
266
|
"aria-selected": "false",
|
|
255
267
|
"aria-setsize": 4,
|
|
268
|
+
"aria-expanded": true,
|
|
256
269
|
onClick: expect.any(Function),
|
|
257
270
|
ref: expect.any(Function),
|
|
258
271
|
role: "treeitem",
|
|
259
272
|
tabIndex: 0,
|
|
260
273
|
});
|
|
261
274
|
});
|
|
275
|
+
|
|
276
|
+
it("generates aria-expanded for non expanded folder", () => {
|
|
277
|
+
expect(tree.instance.getItemInstance("x3").getProps()).toEqual(
|
|
278
|
+
expect.objectContaining({
|
|
279
|
+
"aria-expanded": false,
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("generates aria-expanded for leaf", () => {
|
|
285
|
+
expect(tree.instance.getItemInstance("x111").getProps()).toEqual(
|
|
286
|
+
expect.objectContaining({
|
|
287
|
+
"aria-expanded": undefined,
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
});
|
|
262
291
|
});
|
|
263
292
|
|
|
264
293
|
describe("util functions", () => {
|
|
@@ -312,8 +341,30 @@ describe("core-feature/selections", () => {
|
|
|
312
341
|
expect(tree.instance.getItemInstance("x1").isFolder()).toBe(true);
|
|
313
342
|
});
|
|
314
343
|
|
|
315
|
-
it("returns correctly for
|
|
316
|
-
expect(tree.instance.getItemInstance("
|
|
344
|
+
it("returns correctly for true cases of isFolder() ", () => {
|
|
345
|
+
expect(tree.instance.getItemInstance("x1").isFolder()).toBe(true);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("returns correct isFolder for hidden items", async () => {
|
|
349
|
+
if (title.toLocaleLowerCase().includes("async")) {
|
|
350
|
+
// async test tree defaults to "loading" item names
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const testTree = await tree
|
|
354
|
+
.with({
|
|
355
|
+
isItemFolder: (item: any) => item.getItemData().length < 4,
|
|
356
|
+
})
|
|
357
|
+
.createTestCaseTree();
|
|
358
|
+
|
|
359
|
+
// Reference: https://github.com/lukasbach/headless-tree/issues/166
|
|
360
|
+
expect(testTree.instance.getItemInstance("x44").isFolder()).toBe(true);
|
|
361
|
+
expect(testTree.instance.getItemInstance("x444").isFolder()).toBe(
|
|
362
|
+
false,
|
|
363
|
+
);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("returns isFolder=true for root item", () => {
|
|
367
|
+
expect(tree.instance.getItemInstance("x").isFolder()).toBe(true);
|
|
317
368
|
});
|
|
318
369
|
|
|
319
370
|
it("returns correctly for getParent()", () => {
|
|
@@ -63,15 +63,15 @@ export class TestTree<T = string> {
|
|
|
63
63
|
}),
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
forSuits(runSuite: (tree: TestTree<T
|
|
66
|
+
forSuits(runSuite: (tree: TestTree<T>, title: string) => void) {
|
|
67
67
|
describe.for([
|
|
68
68
|
this.suits.sync(),
|
|
69
69
|
this.suits.async(),
|
|
70
70
|
this.suits.proxifiedSync(),
|
|
71
71
|
this.suits.proxifiedAsync(),
|
|
72
|
-
])("$title", ({ tree }) => {
|
|
72
|
+
])("$title", ({ tree, title }) => {
|
|
73
73
|
tree.resetBeforeEach();
|
|
74
|
-
runSuite(tree);
|
|
74
|
+
runSuite(tree, title);
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
|