@headless-tree/core 1.4.0 → 1.5.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 +17 -0
- package/dist/index.d.mts +34 -10
- package/dist/index.d.ts +34 -10
- package/dist/index.js +150 -72
- package/dist/index.mjs +150 -72
- 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 +1 -0
- package/src/features/main/types.ts +9 -0
- package/src/features/sync-data-loader/types.ts +7 -1
- package/src/mddocs-entry.ts +13 -0
- package/src/test-utils/test-tree-do.ts +6 -0
- package/src/test-utils/test-tree.ts +14 -3
- package/src/types/core.ts +5 -5
package/dist/index.mjs
CHANGED
|
@@ -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,8 @@ 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)$/
|
|
889
967
|
};
|
|
890
968
|
var testHotkeyMatch = (pressedKeys, tree, hotkey) => {
|
|
891
969
|
const supposedKeys = hotkey.hotkey.toLowerCase().split("+");
|
|
@@ -990,6 +1068,12 @@ var loadItemData = (tree, itemId) => __async(null, null, function* () {
|
|
|
990
1068
|
var _a;
|
|
991
1069
|
const config = tree.getConfig();
|
|
992
1070
|
const dataRef = getDataRef(tree);
|
|
1071
|
+
if (!dataRef.current.itemData[itemId]) {
|
|
1072
|
+
tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
|
|
1073
|
+
...loadingItemData,
|
|
1074
|
+
itemId
|
|
1075
|
+
]);
|
|
1076
|
+
}
|
|
993
1077
|
const item = yield config.dataLoader.getItem(itemId);
|
|
994
1078
|
dataRef.current.itemData[itemId] = item;
|
|
995
1079
|
(_a = config.onLoadedItem) == null ? void 0 : _a.call(config, itemId, item);
|
|
@@ -1004,6 +1088,12 @@ var loadChildrenIds = (tree, itemId) => __async(null, null, function* () {
|
|
|
1004
1088
|
const config = tree.getConfig();
|
|
1005
1089
|
const dataRef = getDataRef(tree);
|
|
1006
1090
|
let childrenIds;
|
|
1091
|
+
if (!dataRef.current.childrenIds[itemId]) {
|
|
1092
|
+
tree.applySubStateUpdate("loadingItemChildrens", (loadingItemChildrens) => [
|
|
1093
|
+
...loadingItemChildrens,
|
|
1094
|
+
itemId
|
|
1095
|
+
]);
|
|
1096
|
+
}
|
|
1007
1097
|
if ("getChildrenWithData" in config.dataLoader) {
|
|
1008
1098
|
const children = yield config.dataLoader.getChildrenWithData(itemId);
|
|
1009
1099
|
childrenIds = children.map((c) => c.id);
|
|
@@ -1064,11 +1154,7 @@ var asyncDataLoaderFeature = {
|
|
|
1064
1154
|
return dataRef.current.itemData[itemId];
|
|
1065
1155
|
}
|
|
1066
1156
|
if (!tree.getState().loadingItemData.includes(itemId) && !skipFetch) {
|
|
1067
|
-
|
|
1068
|
-
...loadingItemData,
|
|
1069
|
-
itemId
|
|
1070
|
-
]);
|
|
1071
|
-
loadItemData(tree, itemId);
|
|
1157
|
+
setTimeout(() => loadItemData(tree, itemId));
|
|
1072
1158
|
}
|
|
1073
1159
|
return (_b = (_a = config.createLoadingItemData) == null ? void 0 : _a.call(config)) != null ? _b : null;
|
|
1074
1160
|
},
|
|
@@ -1080,11 +1166,7 @@ var asyncDataLoaderFeature = {
|
|
|
1080
1166
|
if (tree.getState().loadingItemChildrens.includes(itemId) || skipFetch) {
|
|
1081
1167
|
return [];
|
|
1082
1168
|
}
|
|
1083
|
-
tree
|
|
1084
|
-
"loadingItemChildrens",
|
|
1085
|
-
(loadingItemChildrens) => [...loadingItemChildrens, itemId]
|
|
1086
|
-
);
|
|
1087
|
-
loadChildrenIds(tree, itemId);
|
|
1169
|
+
setTimeout(() => loadChildrenIds(tree, itemId));
|
|
1088
1170
|
return [];
|
|
1089
1171
|
}
|
|
1090
1172
|
},
|
|
@@ -1094,10 +1176,6 @@ var asyncDataLoaderFeature = {
|
|
|
1094
1176
|
var _a;
|
|
1095
1177
|
if (!optimistic) {
|
|
1096
1178
|
(_a = getDataRef(tree).current.itemData) == null ? true : delete _a[itemId];
|
|
1097
|
-
tree.applySubStateUpdate("loadingItemData", (loadingItemData) => [
|
|
1098
|
-
...loadingItemData,
|
|
1099
|
-
itemId
|
|
1100
|
-
]);
|
|
1101
1179
|
}
|
|
1102
1180
|
yield loadItemData(tree, itemId);
|
|
1103
1181
|
}),
|
|
@@ -1105,10 +1183,6 @@ var asyncDataLoaderFeature = {
|
|
|
1105
1183
|
var _a;
|
|
1106
1184
|
if (!optimistic) {
|
|
1107
1185
|
(_a = getDataRef(tree).current.childrenIds) == null ? true : delete _a[itemId];
|
|
1108
|
-
tree.applySubStateUpdate(
|
|
1109
|
-
"loadingItemChildrens",
|
|
1110
|
-
(loadingItemChildrens) => [...loadingItemChildrens, itemId]
|
|
1111
|
-
);
|
|
1112
1186
|
}
|
|
1113
1187
|
yield loadChildrenIds(tree, itemId);
|
|
1114
1188
|
}),
|
|
@@ -1560,6 +1634,10 @@ var dragAndDropFeature = {
|
|
|
1560
1634
|
const target = tree.getDragTarget();
|
|
1561
1635
|
return target ? target.item.getId() === item.getId() : false;
|
|
1562
1636
|
},
|
|
1637
|
+
isUnorderedDragTarget: ({ tree, item }) => {
|
|
1638
|
+
const target = tree.getDragTarget();
|
|
1639
|
+
return target ? !isOrderedDragTarget(target) && target.item.getId() === item.getId() : false;
|
|
1640
|
+
},
|
|
1563
1641
|
isDragTargetAbove: ({ tree, item }) => {
|
|
1564
1642
|
const target = tree.getDragTarget();
|
|
1565
1643
|
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.0",
|
|
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
|
};
|