@headless-tree/core 1.4.0 → 1.5.1
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 +27 -0
- package/dist/index.d.mts +34 -10
- package/dist/index.d.ts +34 -10
- package/dist/index.js +152 -73
- package/dist/index.mjs +152 -73
- package/package.json +19 -3
- package/readme.md +6 -6
- package/src/core/create-tree.ts +45 -9
- package/src/features/async-data-loader/async-data-loader.spec.ts +1 -0
- package/src/features/async-data-loader/feature.ts +16 -20
- package/src/features/async-data-loader/types.ts +2 -1
- package/src/features/checkboxes/checkboxes.spec.ts +111 -122
- package/src/features/checkboxes/feature.ts +89 -40
- package/src/features/checkboxes/types.ts +16 -3
- package/src/features/drag-and-drop/feature.ts +7 -0
- package/src/features/drag-and-drop/types.ts +6 -0
- package/src/features/hotkeys-core/feature.ts +2 -0
- package/src/features/main/types.ts +9 -0
- package/src/features/sync-data-loader/types.ts +7 -1
- package/src/features/tree/feature.ts +2 -2
- package/src/features/tree/tree.spec.ts +37 -4
- package/src/mddocs-entry.ts +13 -0
- package/src/test-utils/test-tree-do.ts +6 -0
- package/src/test-utils/test-tree.ts +17 -6
- package/src/types/core.ts +5 -5
package/dist/index.mjs
CHANGED
|
@@ -259,7 +259,7 @@ var treeFeature = {
|
|
|
259
259
|
);
|
|
260
260
|
},
|
|
261
261
|
isFocused: ({ tree, item, itemId }) => tree.getState().focusedItem === itemId || tree.getState().focusedItem === null && item.getItemMeta().index === 0,
|
|
262
|
-
isFolder: ({ tree, item }) =>
|
|
262
|
+
isFolder: ({ tree, item, itemId }) => itemId === tree.getConfig().rootItemId || tree.getConfig().isItemFolder(item),
|
|
263
263
|
getItemName: ({ tree, item }) => {
|
|
264
264
|
const config = tree.getConfig();
|
|
265
265
|
return config.getItemName(item);
|
|
@@ -441,6 +441,7 @@ var createTree = (initialConfig) => {
|
|
|
441
441
|
);
|
|
442
442
|
let treeElement;
|
|
443
443
|
const treeDataRef = { current: {} };
|
|
444
|
+
let rebuildScheduled = false;
|
|
444
445
|
const itemInstancesMap = {};
|
|
445
446
|
let itemInstances = [];
|
|
446
447
|
const itemElementsMap = {};
|
|
@@ -484,6 +485,7 @@ var createTree = (initialConfig) => {
|
|
|
484
485
|
itemInstances.push(itemInstancesMap[item.itemId]);
|
|
485
486
|
}
|
|
486
487
|
}
|
|
488
|
+
rebuildScheduled = false;
|
|
487
489
|
};
|
|
488
490
|
const eachFeature = (fn) => {
|
|
489
491
|
for (const feature of additionalFeatures) {
|
|
@@ -498,16 +500,48 @@ var createTree = (initialConfig) => {
|
|
|
498
500
|
var _a2;
|
|
499
501
|
(_a2 = config.setState) == null ? void 0 : _a2.call(config, state);
|
|
500
502
|
},
|
|
503
|
+
setMounted: ({}, isMounted) => {
|
|
504
|
+
var _a2;
|
|
505
|
+
const ref = treeDataRef.current;
|
|
506
|
+
ref.isMounted = isMounted;
|
|
507
|
+
if (isMounted) {
|
|
508
|
+
(_a2 = ref.waitingForMount) == null ? void 0 : _a2.forEach((cb) => cb());
|
|
509
|
+
ref.waitingForMount = [];
|
|
510
|
+
}
|
|
511
|
+
},
|
|
501
512
|
applySubStateUpdate: ({}, stateName, updater) => {
|
|
502
|
-
|
|
503
|
-
const
|
|
504
|
-
|
|
513
|
+
var _a2;
|
|
514
|
+
const apply = () => {
|
|
515
|
+
state[stateName] = typeof updater === "function" ? updater(state[stateName]) : updater;
|
|
516
|
+
const externalStateSetter = config[stateHandlerNames[stateName]];
|
|
517
|
+
externalStateSetter == null ? void 0 : externalStateSetter(state[stateName]);
|
|
518
|
+
};
|
|
519
|
+
const ref = treeDataRef.current;
|
|
520
|
+
if (ref.isMounted) {
|
|
521
|
+
apply();
|
|
522
|
+
} else {
|
|
523
|
+
(_a2 = ref.waitingForMount) != null ? _a2 : ref.waitingForMount = [];
|
|
524
|
+
ref.waitingForMount.push(apply);
|
|
525
|
+
}
|
|
505
526
|
},
|
|
506
527
|
// TODO rebuildSubTree: (itemId: string) => void;
|
|
507
528
|
rebuildTree: () => {
|
|
508
|
-
var _a2;
|
|
509
|
-
|
|
510
|
-
(
|
|
529
|
+
var _a2, _b2;
|
|
530
|
+
const ref = treeDataRef.current;
|
|
531
|
+
if (ref.isMounted) {
|
|
532
|
+
rebuildItemMeta();
|
|
533
|
+
(_a2 = config.setState) == null ? void 0 : _a2.call(config, state);
|
|
534
|
+
} else {
|
|
535
|
+
(_b2 = ref.waitingForMount) != null ? _b2 : ref.waitingForMount = [];
|
|
536
|
+
ref.waitingForMount.push(() => {
|
|
537
|
+
var _a3;
|
|
538
|
+
rebuildItemMeta();
|
|
539
|
+
(_a3 = config.setState) == null ? void 0 : _a3.call(config, state);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
scheduleRebuildTree: () => {
|
|
544
|
+
rebuildScheduled = true;
|
|
511
545
|
},
|
|
512
546
|
getConfig: () => config,
|
|
513
547
|
setConfig: (_, updater) => {
|
|
@@ -540,7 +574,10 @@ var createTree = (initialConfig) => {
|
|
|
540
574
|
}
|
|
541
575
|
return existingInstance;
|
|
542
576
|
},
|
|
543
|
-
getItems: () =>
|
|
577
|
+
getItems: () => {
|
|
578
|
+
if (rebuildScheduled) rebuildItemMeta();
|
|
579
|
+
return itemInstances;
|
|
580
|
+
},
|
|
544
581
|
registerElement: ({}, element) => {
|
|
545
582
|
if (treeElement === element) {
|
|
546
583
|
return;
|
|
@@ -773,33 +810,60 @@ var getAllLoadedDescendants = (tree, itemId, includeFolders = false) => {
|
|
|
773
810
|
if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
|
|
774
811
|
return [itemId];
|
|
775
812
|
}
|
|
776
|
-
const descendants = tree.retrieveChildrenIds(itemId).map((child) => getAllLoadedDescendants(tree, child, includeFolders)).flat();
|
|
813
|
+
const descendants = tree.retrieveChildrenIds(itemId, true).map((child) => getAllLoadedDescendants(tree, child, includeFolders)).flat();
|
|
777
814
|
return includeFolders ? [itemId, ...descendants] : descendants;
|
|
778
815
|
};
|
|
816
|
+
var getAllDescendants = (tree, itemId, includeFolders = false) => __async(null, null, function* () {
|
|
817
|
+
yield tree.loadItemData(itemId);
|
|
818
|
+
if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
|
|
819
|
+
return [itemId];
|
|
820
|
+
}
|
|
821
|
+
const childrenIds = yield tree.loadChildrenIds(itemId);
|
|
822
|
+
const descendants = (yield Promise.all(
|
|
823
|
+
childrenIds.map(
|
|
824
|
+
(child) => getAllDescendants(tree, child, includeFolders)
|
|
825
|
+
)
|
|
826
|
+
)).flat();
|
|
827
|
+
return includeFolders ? [itemId, ...descendants] : descendants;
|
|
828
|
+
});
|
|
829
|
+
var withLoadingState = (tree, itemId, callback) => __async(null, null, function* () {
|
|
830
|
+
tree.applySubStateUpdate("loadingCheckPropagationItems", (items) => [
|
|
831
|
+
...items,
|
|
832
|
+
itemId
|
|
833
|
+
]);
|
|
834
|
+
try {
|
|
835
|
+
yield callback();
|
|
836
|
+
} finally {
|
|
837
|
+
tree.applySubStateUpdate(
|
|
838
|
+
"loadingCheckPropagationItems",
|
|
839
|
+
(items) => items.filter((id) => id !== itemId)
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
});
|
|
779
843
|
var checkboxesFeature = {
|
|
780
844
|
key: "checkboxes",
|
|
781
845
|
overwrites: ["selection"],
|
|
782
846
|
getInitialState: (initialState) => __spreadValues({
|
|
783
|
-
checkedItems: []
|
|
847
|
+
checkedItems: [],
|
|
848
|
+
loadingCheckPropagationItems: []
|
|
784
849
|
}, initialState),
|
|
785
850
|
getDefaultConfig: (defaultConfig, tree) => {
|
|
786
|
-
var _a, _b
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
);
|
|
790
|
-
if (hasAsyncLoader && defaultConfig.propagateCheckedState) {
|
|
791
|
-
throwError(`propagateCheckedState not supported with async trees`);
|
|
792
|
-
}
|
|
793
|
-
const propagateCheckedState = (_b = defaultConfig.propagateCheckedState) != null ? _b : !hasAsyncLoader;
|
|
794
|
-
const canCheckFolders = (_c = defaultConfig.canCheckFolders) != null ? _c : !propagateCheckedState;
|
|
851
|
+
var _a, _b;
|
|
852
|
+
const propagateCheckedState = (_a = defaultConfig.propagateCheckedState) != null ? _a : true;
|
|
853
|
+
const canCheckFolders = (_b = defaultConfig.canCheckFolders) != null ? _b : !propagateCheckedState;
|
|
795
854
|
return __spreadValues({
|
|
796
855
|
setCheckedItems: makeStateUpdater("checkedItems", tree),
|
|
856
|
+
setLoadingCheckPropagationItems: makeStateUpdater(
|
|
857
|
+
"loadingCheckPropagationItems",
|
|
858
|
+
tree
|
|
859
|
+
),
|
|
797
860
|
propagateCheckedState,
|
|
798
861
|
canCheckFolders
|
|
799
862
|
}, defaultConfig);
|
|
800
863
|
},
|
|
801
864
|
stateHandlerNames: {
|
|
802
|
-
checkedItems: "setCheckedItems"
|
|
865
|
+
checkedItems: "setCheckedItems",
|
|
866
|
+
loadingCheckPropagationItems: "setLoadingCheckPropagationItems"
|
|
803
867
|
},
|
|
804
868
|
treeInstance: {
|
|
805
869
|
setCheckedItems: ({ tree }, checkedItems) => {
|
|
@@ -819,13 +883,13 @@ var checkboxesFeature = {
|
|
|
819
883
|
}
|
|
820
884
|
};
|
|
821
885
|
},
|
|
822
|
-
toggleCheckedState: ({ item })
|
|
886
|
+
toggleCheckedState: (_0) => __async(null, [_0], function* ({ item }) {
|
|
823
887
|
if (item.getCheckedState() === "checked" /* Checked */) {
|
|
824
|
-
item.setUnchecked();
|
|
888
|
+
yield item.setUnchecked();
|
|
825
889
|
} else {
|
|
826
|
-
item.setChecked();
|
|
890
|
+
yield item.setChecked();
|
|
827
891
|
}
|
|
828
|
-
},
|
|
892
|
+
}),
|
|
829
893
|
getCheckedState: ({ item, tree }) => {
|
|
830
894
|
const { checkedItems } = tree.getState();
|
|
831
895
|
const { propagateCheckedState } = tree.getConfig();
|
|
@@ -835,6 +899,7 @@ var checkboxesFeature = {
|
|
|
835
899
|
}
|
|
836
900
|
if (item.isFolder() && propagateCheckedState) {
|
|
837
901
|
const descendants = getAllLoadedDescendants(tree, itemId);
|
|
902
|
+
if (descendants.length === 0) return "unchecked" /* Unchecked */;
|
|
838
903
|
if (descendants.every((d) => checkedItems.includes(d))) {
|
|
839
904
|
return "checked" /* Checked */;
|
|
840
905
|
}
|
|
@@ -844,36 +909,48 @@ var checkboxesFeature = {
|
|
|
844
909
|
}
|
|
845
910
|
return "unchecked" /* Unchecked */;
|
|
846
911
|
},
|
|
847
|
-
setChecked: ({ item, tree, itemId })
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
912
|
+
setChecked: (_0) => __async(null, [_0], function* ({ item, tree, itemId }) {
|
|
913
|
+
yield withLoadingState(tree, itemId, () => __async(null, null, function* () {
|
|
914
|
+
const { propagateCheckedState, canCheckFolders } = tree.getConfig();
|
|
915
|
+
if (item.isFolder() && propagateCheckedState) {
|
|
916
|
+
const descendants = yield getAllDescendants(
|
|
917
|
+
tree,
|
|
918
|
+
itemId,
|
|
919
|
+
canCheckFolders
|
|
920
|
+
);
|
|
921
|
+
tree.applySubStateUpdate("checkedItems", (items) => [
|
|
922
|
+
...items,
|
|
923
|
+
...descendants
|
|
924
|
+
]);
|
|
925
|
+
} else if (!item.isFolder() || canCheckFolders) {
|
|
926
|
+
tree.applySubStateUpdate("checkedItems", (items) => [
|
|
927
|
+
...items,
|
|
928
|
+
itemId
|
|
929
|
+
]);
|
|
930
|
+
}
|
|
931
|
+
}));
|
|
932
|
+
}),
|
|
933
|
+
setUnchecked: (_0) => __async(null, [_0], function* ({ item, tree, itemId }) {
|
|
934
|
+
yield withLoadingState(tree, itemId, () => __async(null, null, function* () {
|
|
935
|
+
const { propagateCheckedState, canCheckFolders } = tree.getConfig();
|
|
936
|
+
if (item.isFolder() && propagateCheckedState) {
|
|
937
|
+
const descendants = yield getAllDescendants(
|
|
938
|
+
tree,
|
|
939
|
+
itemId,
|
|
940
|
+
canCheckFolders
|
|
941
|
+
);
|
|
942
|
+
tree.applySubStateUpdate(
|
|
943
|
+
"checkedItems",
|
|
944
|
+
(items) => items.filter((id) => !descendants.includes(id) && id !== itemId)
|
|
945
|
+
);
|
|
946
|
+
} else {
|
|
947
|
+
tree.applySubStateUpdate(
|
|
948
|
+
"checkedItems",
|
|
949
|
+
(items) => items.filter((id) => id !== itemId)
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
}));
|
|
953
|
+
})
|
|
877
954
|
}
|
|
878
955
|
};
|
|
879
956
|
|
|
@@ -885,7 +962,9 @@ var specialKeys = {
|
|
|
885
962
|
plus: /^(NumpadAdd|Plus)$/,
|
|
886
963
|
minus: /^(NumpadSubtract|Minus)$/,
|
|
887
964
|
control: /^(ControlLeft|ControlRight)$/,
|
|
888
|
-
shift: /^(ShiftLeft|ShiftRight)
|
|
965
|
+
shift: /^(ShiftLeft|ShiftRight)$/,
|
|
966
|
+
metaorcontrol: /^(MetaLeft|MetaRight|ControlLeft|ControlRight)$/,
|
|
967
|
+
enter: /^(Enter|NumpadEnter)$/
|
|
889
968
|
};
|
|
890
969
|
var testHotkeyMatch = (pressedKeys, tree, hotkey) => {
|
|
891
970
|
const supposedKeys = hotkey.hotkey.toLowerCase().split("+");
|
|
@@ -990,6 +1069,12 @@ var loadItemData = (tree, itemId) => __async(null, null, function* () {
|
|
|
990
1069
|
var _a;
|
|
991
1070
|
const config = tree.getConfig();
|
|
992
1071
|
const dataRef = getDataRef(tree);
|
|
1072
|
+
if (!dataRef.current.itemData[itemId]) {
|
|
1073
|
+
tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
|
|
1074
|
+
...loadingItemData,
|
|
1075
|
+
itemId
|
|
1076
|
+
]);
|
|
1077
|
+
}
|
|
993
1078
|
const item = yield config.dataLoader.getItem(itemId);
|
|
994
1079
|
dataRef.current.itemData[itemId] = item;
|
|
995
1080
|
(_a = config.onLoadedItem) == null ? void 0 : _a.call(config, itemId, item);
|
|
@@ -1004,6 +1089,12 @@ var loadChildrenIds = (tree, itemId) => __async(null, null, function* () {
|
|
|
1004
1089
|
const config = tree.getConfig();
|
|
1005
1090
|
const dataRef = getDataRef(tree);
|
|
1006
1091
|
let childrenIds;
|
|
1092
|
+
if (!dataRef.current.childrenIds[itemId]) {
|
|
1093
|
+
tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [
|
|
1094
|
+
...loadingItemChildrens,
|
|
1095
|
+
itemId
|
|
1096
|
+
]);
|
|
1097
|
+
}
|
|
1007
1098
|
if ("getChildrenWithData" in config.dataLoader) {
|
|
1008
1099
|
const children = yield config.dataLoader.getChildrenWithData(itemId);
|
|
1009
1100
|
childrenIds = children.map((c) => c.id);
|
|
@@ -1064,11 +1155,7 @@ var asyncDataLoaderFeature = {
|
|
|
1064
1155
|
return dataRef.current.itemData[itemId];
|
|
1065
1156
|
}
|
|
1066
1157
|
if (!tree.getState().loadingItemData.includes(itemId) && !skipFetch) {
|
|
1067
|
-
|
|
1068
|
-
...loadingItemData,
|
|
1069
|
-
itemId
|
|
1070
|
-
]);
|
|
1071
|
-
loadItemData(tree, itemId);
|
|
1158
|
+
setTimeout(() => loadItemData(tree, itemId));
|
|
1072
1159
|
}
|
|
1073
1160
|
return (_b = (_a = config.createLoadingItemData) == null ? void 0 : _a.call(config)) != null ? _b : null;
|
|
1074
1161
|
},
|
|
@@ -1080,11 +1167,7 @@ var asyncDataLoaderFeature = {
|
|
|
1080
1167
|
if (tree.getState().loadingItemChildrens.includes(itemId) || skipFetch) {
|
|
1081
1168
|
return [];
|
|
1082
1169
|
}
|
|
1083
|
-
tree
|
|
1084
|
-
"loadingItemChildrens",
|
|
1085
|
-
(loadingItemChildrens) => [...loadingItemChildrens, itemId]
|
|
1086
|
-
);
|
|
1087
|
-
loadChildrenIds(tree, itemId);
|
|
1170
|
+
setTimeout(() => loadChildrenIds(tree, itemId));
|
|
1088
1171
|
return [];
|
|
1089
1172
|
}
|
|
1090
1173
|
},
|
|
@@ -1094,10 +1177,6 @@ var asyncDataLoaderFeature = {
|
|
|
1094
1177
|
var _a;
|
|
1095
1178
|
if (!optimistic) {
|
|
1096
1179
|
(_a = getDataRef(tree).current.itemData) == null ? true : delete _a[itemId];
|
|
1097
|
-
tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
|
|
1098
|
-
...loadingItemData,
|
|
1099
|
-
itemId
|
|
1100
|
-
]);
|
|
1101
1180
|
}
|
|
1102
1181
|
yield loadItemData(tree, itemId);
|
|
1103
1182
|
}),
|
|
@@ -1105,10 +1184,6 @@ var asyncDataLoaderFeature = {
|
|
|
1105
1184
|
var _a;
|
|
1106
1185
|
if (!optimistic) {
|
|
1107
1186
|
(_a = getDataRef(tree).current.childrenIds) == null ? true : delete _a[itemId];
|
|
1108
|
-
tree.applySubStateUpdate(
|
|
1109
|
-
"loadingItemChildrens",
|
|
1110
|
-
(loadingItemChildrens) => [...loadingItemChildrens, itemId]
|
|
1111
|
-
);
|
|
1112
1187
|
}
|
|
1113
1188
|
yield loadChildrenIds(tree, itemId);
|
|
1114
1189
|
}),
|
|
@@ -1560,6 +1635,10 @@ var dragAndDropFeature = {
|
|
|
1560
1635
|
const target = tree.getDragTarget();
|
|
1561
1636
|
return target ? target.item.getId() === item.getId() : false;
|
|
1562
1637
|
},
|
|
1638
|
+
isUnorderedDragTarget: ({ tree, item }) => {
|
|
1639
|
+
const target = tree.getDragTarget();
|
|
1640
|
+
return target ? !isOrderedDragTarget(target) && target.item.getId() === item.getId() : false;
|
|
1641
|
+
},
|
|
1563
1642
|
isDragTargetAbove: ({ tree, item }) => {
|
|
1564
1643
|
const target = tree.getDragTarget();
|
|
1565
1644
|
if (!target || !isOrderedDragTarget(target) || target.item !== item.getParent())
|
package/package.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@headless-tree/core",
|
|
3
|
-
"
|
|
3
|
+
"description": "The definitive tree component for the Web",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"tree",
|
|
6
|
+
"component",
|
|
7
|
+
"web",
|
|
8
|
+
"headless",
|
|
9
|
+
"ui",
|
|
10
|
+
"react",
|
|
11
|
+
"nested",
|
|
12
|
+
"async",
|
|
13
|
+
"checkbox",
|
|
14
|
+
"hook"
|
|
15
|
+
],
|
|
16
|
+
"version": "1.5.1",
|
|
4
17
|
"main": "dist/index.d.ts",
|
|
5
18
|
"module": "dist/index.mjs",
|
|
6
19
|
"types": "dist/index.d.mts",
|
|
@@ -11,13 +24,16 @@
|
|
|
11
24
|
"default": "./dist/index.mjs"
|
|
12
25
|
},
|
|
13
26
|
"require": {
|
|
14
|
-
"types": "./dist/index.
|
|
15
|
-
"default": "./dist/index.
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"default": "./dist/index.js"
|
|
16
29
|
}
|
|
17
30
|
},
|
|
18
31
|
"./package.json": "./package.json"
|
|
19
32
|
},
|
|
20
33
|
"sideEffects": false,
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"provenance": true
|
|
36
|
+
},
|
|
21
37
|
"scripts": {
|
|
22
38
|
"build": "tsup ./src/index.ts --format esm,cjs --dts",
|
|
23
39
|
"start": "tsup ./src/index.ts --format esm,cjs --dts --watch",
|
package/readme.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://headless-tree.lukasbach.com/)
|
|
4
4
|
[](https://discord.gg/KuZ6EezzVw)
|
|
5
|
-
[](https://bsky.app/profile/lukasbach.bsky.social)
|
|
6
|
+
[](https://x.com/lukasmbach)
|
|
6
7
|
[](https://github.com/sponsors/lukasbach)
|
|
7
8
|
[](https://github.com/lukasbach)
|
|
8
9
|
[](https://www.npmjs.com/package/@headless-tree/core)
|
|
9
10
|
[](https://www.npmjs.com/package/@headless-tree/react)
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
Super-easy integration of complex tree components into React. Supports ordered
|
|
12
|
+
Super-easy integration of complex tree components into React. Supports ordered
|
|
13
13
|
and unordered drag-and-drop, extensive keybindings, search, renaming and more.
|
|
14
14
|
Fully customizable and accessible. Headless Tree is the official successor for
|
|
15
15
|
[react-complex-tree](https://github.com/lukasbach/react-complex-tree).
|
|
@@ -18,7 +18,7 @@ It aims to bring the many features of complex tree views, like multi-select,
|
|
|
18
18
|
drag-and-drop, keyboard navigation, tree search, renaming and more, while
|
|
19
19
|
being unopinionated about the styling and rendering of the tree itself.
|
|
20
20
|
Accessibility is ensured by default, and the integration is extremely
|
|
21
|
-
simple and flexible.
|
|
21
|
+
simple and flexible.
|
|
22
22
|
|
|
23
23
|
The interface gives you a flat list of tree nodes
|
|
24
24
|
that you can easily render yourself, which keeps the complexity of the
|
|
@@ -39,7 +39,7 @@ to get an idea of what you can do with it.
|
|
|
39
39
|
> I have collected feedback and fixed any bugs that might arise. I've written
|
|
40
40
|
> [a blog post](https://medium.com/@lukasbach/headless-tree-and-the-future-of-react-complex-tree-fc920700e82a)
|
|
41
41
|
> about the details of the change, and the future of the library.
|
|
42
|
-
>
|
|
42
|
+
>
|
|
43
43
|
> Join
|
|
44
44
|
> [the Discord](https://discord.gg/KuZ6EezzVw) to get involved, and
|
|
45
45
|
> [follow on Bluesky](https://bsky.app/profile/lukasbach.bsky.social) to
|
|
@@ -154,4 +154,4 @@ Then, render your tree based on the tree instance returned from the hook:
|
|
|
154
154
|
```
|
|
155
155
|
|
|
156
156
|
Read on in the [get started guide](https://headless-tree.lukasbach.com/getstarted) to learn more about
|
|
157
|
-
how to use Headless Tree, and how to customize it to your needs.
|
|
157
|
+
how to use Headless Tree, and how to customize it to your needs.
|
package/src/core/create-tree.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { treeFeature } from "../features/tree/feature";
|
|
|
11
11
|
import { ItemMeta } from "../features/tree/types";
|
|
12
12
|
import { buildStaticInstance } from "./build-static-instance";
|
|
13
13
|
import { throwError } from "../utilities/errors";
|
|
14
|
+
import type { TreeDataRef } from "../features/main/types";
|
|
14
15
|
|
|
15
16
|
const verifyFeatures = (features: FeatureImplementation[] | undefined) => {
|
|
16
17
|
const loadedFeatures = features?.map((feature) => feature.key);
|
|
@@ -92,6 +93,7 @@ export const createTree = <T>(
|
|
|
92
93
|
let treeElement: HTMLElement | undefined | null;
|
|
93
94
|
const treeDataRef: { current: any } = { current: {} };
|
|
94
95
|
|
|
96
|
+
let rebuildScheduled = false;
|
|
95
97
|
const itemInstancesMap: Record<string, ItemInstance<T>> = {};
|
|
96
98
|
let itemInstances: ItemInstance<T>[] = [];
|
|
97
99
|
const itemElementsMap: Record<string, HTMLElement | undefined | null> = {};
|
|
@@ -140,6 +142,8 @@ export const createTree = <T>(
|
|
|
140
142
|
itemInstances.push(itemInstancesMap[item.itemId]);
|
|
141
143
|
}
|
|
142
144
|
}
|
|
145
|
+
|
|
146
|
+
rebuildScheduled = false;
|
|
143
147
|
};
|
|
144
148
|
|
|
145
149
|
const eachFeature = (fn: (feature: FeatureImplementation<any>) => void) => {
|
|
@@ -158,22 +162,51 @@ export const createTree = <T>(
|
|
|
158
162
|
config.setState?.(state); // TODO this cant be right... This doesnt allow external state updates
|
|
159
163
|
// TODO this is never used, remove
|
|
160
164
|
},
|
|
165
|
+
setMounted: ({}, isMounted) => {
|
|
166
|
+
const ref = treeDataRef.current as TreeDataRef;
|
|
167
|
+
ref.isMounted = isMounted;
|
|
168
|
+
if (isMounted) {
|
|
169
|
+
ref.waitingForMount?.forEach((cb) => cb());
|
|
170
|
+
ref.waitingForMount = [];
|
|
171
|
+
}
|
|
172
|
+
},
|
|
161
173
|
applySubStateUpdate: <K extends keyof TreeState<any>>(
|
|
162
174
|
{},
|
|
163
175
|
stateName: K,
|
|
164
176
|
updater: Updater<TreeState<T>[K]>,
|
|
165
177
|
) => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
178
|
+
const apply = () => {
|
|
179
|
+
state[stateName] =
|
|
180
|
+
typeof updater === "function" ? updater(state[stateName]) : updater;
|
|
181
|
+
const externalStateSetter = config[
|
|
182
|
+
stateHandlerNames[stateName]
|
|
183
|
+
] as Function;
|
|
184
|
+
externalStateSetter?.(state[stateName]);
|
|
185
|
+
};
|
|
186
|
+
const ref = treeDataRef.current as TreeDataRef;
|
|
187
|
+
if (ref.isMounted) {
|
|
188
|
+
apply();
|
|
189
|
+
} else {
|
|
190
|
+
ref.waitingForMount ??= [];
|
|
191
|
+
ref.waitingForMount.push(apply);
|
|
192
|
+
}
|
|
172
193
|
},
|
|
173
194
|
// TODO rebuildSubTree: (itemId: string) => void;
|
|
174
195
|
rebuildTree: () => {
|
|
175
|
-
|
|
176
|
-
|
|
196
|
+
const ref = treeDataRef.current as TreeDataRef;
|
|
197
|
+
if (ref.isMounted) {
|
|
198
|
+
rebuildItemMeta();
|
|
199
|
+
config.setState?.(state);
|
|
200
|
+
} else {
|
|
201
|
+
ref.waitingForMount ??= [];
|
|
202
|
+
ref.waitingForMount.push(() => {
|
|
203
|
+
rebuildItemMeta();
|
|
204
|
+
config.setState?.(state);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
scheduleRebuildTree: () => {
|
|
209
|
+
rebuildScheduled = true;
|
|
177
210
|
},
|
|
178
211
|
getConfig: () => config,
|
|
179
212
|
setConfig: (_, updater) => {
|
|
@@ -210,7 +243,10 @@ export const createTree = <T>(
|
|
|
210
243
|
}
|
|
211
244
|
return existingInstance;
|
|
212
245
|
},
|
|
213
|
-
getItems: () =>
|
|
246
|
+
getItems: () => {
|
|
247
|
+
if (rebuildScheduled) rebuildItemMeta();
|
|
248
|
+
return itemInstances;
|
|
249
|
+
},
|
|
214
250
|
registerElement: ({}, element) => {
|
|
215
251
|
if (treeElement === element) {
|
|
216
252
|
return;
|
|
@@ -46,6 +46,7 @@ describe("core-feature/selections", () => {
|
|
|
46
46
|
);
|
|
47
47
|
const setLoadingItemData = tree.mockedHandler("setLoadingItemData");
|
|
48
48
|
tree.do.selectItem("x12");
|
|
49
|
+
await tree.do.awaitNextTick();
|
|
49
50
|
expect(setLoadingItemChildrens).toHaveBeenCalledWith(["x12"]);
|
|
50
51
|
expect(setLoadingItemData).not.toHaveBeenCalled();
|
|
51
52
|
await tree.resolveAsyncVisibleItems();
|
|
@@ -13,6 +13,13 @@ const loadItemData = async <T>(tree: TreeInstance<T>, itemId: string) => {
|
|
|
13
13
|
const config = tree.getConfig();
|
|
14
14
|
const dataRef = getDataRef(tree);
|
|
15
15
|
|
|
16
|
+
if (!dataRef.current.itemData[itemId]) {
|
|
17
|
+
tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
|
|
18
|
+
...loadingItemData,
|
|
19
|
+
itemId,
|
|
20
|
+
]);
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
const item = await config.dataLoader.getItem(itemId);
|
|
17
24
|
dataRef.current.itemData[itemId] = item;
|
|
18
25
|
config.onLoadedItem?.(itemId, item);
|
|
@@ -28,6 +35,13 @@ const loadChildrenIds = async <T>(tree: TreeInstance<T>, itemId: string) => {
|
|
|
28
35
|
const dataRef = getDataRef(tree);
|
|
29
36
|
let childrenIds: string[];
|
|
30
37
|
|
|
38
|
+
if (!dataRef.current.childrenIds[itemId]) {
|
|
39
|
+
tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [
|
|
40
|
+
...loadingItemChildrens,
|
|
41
|
+
itemId,
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
|
|
31
45
|
if ("getChildrenWithData" in config.dataLoader) {
|
|
32
46
|
const children = await config.dataLoader.getChildrenWithData(itemId);
|
|
33
47
|
childrenIds = children.map((c) => c.id);
|
|
@@ -104,12 +118,7 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
|
|
|
104
118
|
}
|
|
105
119
|
|
|
106
120
|
if (!tree.getState().loadingItemData.includes(itemId) && !skipFetch) {
|
|
107
|
-
|
|
108
|
-
...loadingItemData,
|
|
109
|
-
itemId,
|
|
110
|
-
]);
|
|
111
|
-
|
|
112
|
-
loadItemData(tree, itemId);
|
|
121
|
+
setTimeout(() => loadItemData(tree, itemId));
|
|
113
122
|
}
|
|
114
123
|
|
|
115
124
|
return config.createLoadingItemData?.() ?? null;
|
|
@@ -125,12 +134,7 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
|
|
|
125
134
|
return [];
|
|
126
135
|
}
|
|
127
136
|
|
|
128
|
-
tree
|
|
129
|
-
"loadingItemChildrens",
|
|
130
|
-
(loadingItemChildrens) => [...loadingItemChildrens, itemId],
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
loadChildrenIds(tree, itemId);
|
|
137
|
+
setTimeout(() => loadChildrenIds(tree, itemId));
|
|
134
138
|
|
|
135
139
|
return [];
|
|
136
140
|
},
|
|
@@ -143,20 +147,12 @@ export const asyncDataLoaderFeature: FeatureImplementation = {
|
|
|
143
147
|
invalidateItemData: async ({ tree, itemId }, optimistic) => {
|
|
144
148
|
if (!optimistic) {
|
|
145
149
|
delete getDataRef(tree).current.itemData?.[itemId];
|
|
146
|
-
tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
|
|
147
|
-
...loadingItemData,
|
|
148
|
-
itemId,
|
|
149
|
-
]);
|
|
150
150
|
}
|
|
151
151
|
await loadItemData(tree, itemId);
|
|
152
152
|
},
|
|
153
153
|
invalidateChildrenIds: async ({ tree, itemId }, optimistic) => {
|
|
154
154
|
if (!optimistic) {
|
|
155
155
|
delete getDataRef(tree).current.childrenIds?.[itemId];
|
|
156
|
-
tree.applySubStateUpdate(
|
|
157
|
-
"loadingItemChildrens",
|
|
158
|
-
(loadingItemChildrens) => [...loadingItemChildrens, itemId],
|
|
159
|
-
);
|
|
160
156
|
}
|
|
161
157
|
await loadChildrenIds(tree, itemId);
|
|
162
158
|
},
|
|
@@ -47,7 +47,8 @@ export type AsyncDataLoaderFeatureDef<T> = {
|
|
|
47
47
|
* the tree will continue to display the old data until the new data has loaded. */
|
|
48
48
|
invalidateChildrenIds: (optimistic?: boolean) => Promise<void>;
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
/** Set to undefined to clear cache without triggering automatic refetch. Use @invalidateItemData to clear and triggering refetch. */
|
|
51
|
+
updateCachedData: (data: T | undefined) => void;
|
|
51
52
|
updateCachedChildrenIds: (childrenIds: string[]) => void;
|
|
52
53
|
isLoading: () => boolean;
|
|
53
54
|
};
|