@headless-tree/core 0.0.0-20230802230636

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/lib/cjs/core/create-tree.d.ts +2 -0
  3. package/lib/cjs/core/create-tree.js +138 -0
  4. package/lib/cjs/features/async-data-loader/feature.d.ts +5 -0
  5. package/lib/cjs/features/async-data-loader/feature.js +88 -0
  6. package/lib/cjs/features/async-data-loader/types.d.ts +41 -0
  7. package/lib/cjs/features/async-data-loader/types.js +2 -0
  8. package/lib/cjs/features/drag-and-drop/feature.d.ts +3 -0
  9. package/lib/cjs/features/drag-and-drop/feature.js +138 -0
  10. package/lib/cjs/features/drag-and-drop/types.d.ts +69 -0
  11. package/lib/cjs/features/drag-and-drop/types.js +9 -0
  12. package/lib/cjs/features/drag-and-drop/utils.d.ts +6 -0
  13. package/lib/cjs/features/drag-and-drop/utils.js +79 -0
  14. package/lib/cjs/features/expand-all/feature.d.ts +6 -0
  15. package/lib/cjs/features/expand-all/feature.js +41 -0
  16. package/lib/cjs/features/expand-all/types.d.ts +17 -0
  17. package/lib/cjs/features/expand-all/types.js +2 -0
  18. package/lib/cjs/features/hotkeys-core/feature.d.ts +4 -0
  19. package/lib/cjs/features/hotkeys-core/feature.js +71 -0
  20. package/lib/cjs/features/hotkeys-core/types.d.ts +25 -0
  21. package/lib/cjs/features/hotkeys-core/types.js +2 -0
  22. package/lib/cjs/features/main/types.d.ts +39 -0
  23. package/lib/cjs/features/main/types.js +2 -0
  24. package/lib/cjs/features/renaming/feature.d.ts +5 -0
  25. package/lib/cjs/features/renaming/feature.js +64 -0
  26. package/lib/cjs/features/renaming/types.d.ts +27 -0
  27. package/lib/cjs/features/renaming/types.js +2 -0
  28. package/lib/cjs/features/search/feature.d.ts +5 -0
  29. package/lib/cjs/features/search/feature.js +103 -0
  30. package/lib/cjs/features/search/types.d.ts +33 -0
  31. package/lib/cjs/features/search/types.js +2 -0
  32. package/lib/cjs/features/selection/feature.d.ts +5 -0
  33. package/lib/cjs/features/selection/feature.js +113 -0
  34. package/lib/cjs/features/selection/types.d.ts +21 -0
  35. package/lib/cjs/features/selection/types.js +2 -0
  36. package/lib/cjs/features/sync-data-loader/feature.d.ts +4 -0
  37. package/lib/cjs/features/sync-data-loader/feature.js +14 -0
  38. package/lib/cjs/features/sync-data-loader/types.d.ts +19 -0
  39. package/lib/cjs/features/sync-data-loader/types.js +2 -0
  40. package/lib/cjs/features/tree/feature.d.ts +6 -0
  41. package/lib/cjs/features/tree/feature.js +230 -0
  42. package/lib/cjs/features/tree/types.d.ts +62 -0
  43. package/lib/cjs/features/tree/types.js +2 -0
  44. package/lib/cjs/index.d.ts +23 -0
  45. package/lib/cjs/index.js +39 -0
  46. package/lib/cjs/mddocs-entry.d.ts +21 -0
  47. package/lib/cjs/mddocs-entry.js +17 -0
  48. package/lib/cjs/types/core.d.ts +67 -0
  49. package/lib/cjs/types/core.js +2 -0
  50. package/lib/cjs/types/deep-merge.d.ts +13 -0
  51. package/lib/cjs/types/deep-merge.js +2 -0
  52. package/lib/cjs/utilities/create-on-drop-handler.d.ts +3 -0
  53. package/lib/cjs/utilities/create-on-drop-handler.js +11 -0
  54. package/lib/cjs/utilities/insert-items-at-target.d.ts +3 -0
  55. package/lib/cjs/utilities/insert-items-at-target.js +24 -0
  56. package/lib/cjs/utilities/remove-items-from-parents.d.ts +2 -0
  57. package/lib/cjs/utilities/remove-items-from-parents.js +17 -0
  58. package/lib/cjs/utils.d.ts +6 -0
  59. package/lib/cjs/utils.js +53 -0
  60. package/lib/esm/core/create-tree.d.ts +2 -0
  61. package/lib/esm/core/create-tree.js +134 -0
  62. package/lib/esm/features/async-data-loader/feature.d.ts +5 -0
  63. package/lib/esm/features/async-data-loader/feature.js +85 -0
  64. package/lib/esm/features/async-data-loader/types.d.ts +41 -0
  65. package/lib/esm/features/async-data-loader/types.js +1 -0
  66. package/lib/esm/features/drag-and-drop/feature.d.ts +3 -0
  67. package/lib/esm/features/drag-and-drop/feature.js +135 -0
  68. package/lib/esm/features/drag-and-drop/types.d.ts +69 -0
  69. package/lib/esm/features/drag-and-drop/types.js +6 -0
  70. package/lib/esm/features/drag-and-drop/utils.d.ts +6 -0
  71. package/lib/esm/features/drag-and-drop/utils.js +72 -0
  72. package/lib/esm/features/expand-all/feature.d.ts +6 -0
  73. package/lib/esm/features/expand-all/feature.js +38 -0
  74. package/lib/esm/features/expand-all/types.d.ts +17 -0
  75. package/lib/esm/features/expand-all/types.js +1 -0
  76. package/lib/esm/features/hotkeys-core/feature.d.ts +4 -0
  77. package/lib/esm/features/hotkeys-core/feature.js +68 -0
  78. package/lib/esm/features/hotkeys-core/types.d.ts +25 -0
  79. package/lib/esm/features/hotkeys-core/types.js +1 -0
  80. package/lib/esm/features/main/types.d.ts +39 -0
  81. package/lib/esm/features/main/types.js +1 -0
  82. package/lib/esm/features/renaming/feature.d.ts +5 -0
  83. package/lib/esm/features/renaming/feature.js +61 -0
  84. package/lib/esm/features/renaming/types.d.ts +27 -0
  85. package/lib/esm/features/renaming/types.js +1 -0
  86. package/lib/esm/features/search/feature.d.ts +5 -0
  87. package/lib/esm/features/search/feature.js +100 -0
  88. package/lib/esm/features/search/types.d.ts +33 -0
  89. package/lib/esm/features/search/types.js +1 -0
  90. package/lib/esm/features/selection/feature.d.ts +5 -0
  91. package/lib/esm/features/selection/feature.js +110 -0
  92. package/lib/esm/features/selection/types.d.ts +21 -0
  93. package/lib/esm/features/selection/types.js +1 -0
  94. package/lib/esm/features/sync-data-loader/feature.d.ts +4 -0
  95. package/lib/esm/features/sync-data-loader/feature.js +11 -0
  96. package/lib/esm/features/sync-data-loader/types.d.ts +19 -0
  97. package/lib/esm/features/sync-data-loader/types.js +1 -0
  98. package/lib/esm/features/tree/feature.d.ts +6 -0
  99. package/lib/esm/features/tree/feature.js +227 -0
  100. package/lib/esm/features/tree/types.d.ts +62 -0
  101. package/lib/esm/features/tree/types.js +1 -0
  102. package/lib/esm/index.d.ts +23 -0
  103. package/lib/esm/index.js +23 -0
  104. package/lib/esm/mddocs-entry.d.ts +21 -0
  105. package/lib/esm/mddocs-entry.js +1 -0
  106. package/lib/esm/types/core.d.ts +67 -0
  107. package/lib/esm/types/core.js +1 -0
  108. package/lib/esm/types/deep-merge.d.ts +13 -0
  109. package/lib/esm/types/deep-merge.js +1 -0
  110. package/lib/esm/utilities/create-on-drop-handler.d.ts +3 -0
  111. package/lib/esm/utilities/create-on-drop-handler.js +7 -0
  112. package/lib/esm/utilities/insert-items-at-target.d.ts +3 -0
  113. package/lib/esm/utilities/insert-items-at-target.js +20 -0
  114. package/lib/esm/utilities/remove-items-from-parents.d.ts +2 -0
  115. package/lib/esm/utilities/remove-items-from-parents.js +13 -0
  116. package/lib/esm/utils.d.ts +6 -0
  117. package/lib/esm/utils.js +46 -0
  118. package/package.json +23 -0
  119. package/src/core/create-tree.ts +228 -0
  120. package/src/features/async-data-loader/feature.ts +126 -0
  121. package/src/features/async-data-loader/types.ts +41 -0
  122. package/src/features/drag-and-drop/feature.ts +214 -0
  123. package/src/features/drag-and-drop/types.ts +89 -0
  124. package/src/features/drag-and-drop/utils.ts +117 -0
  125. package/src/features/expand-all/feature.ts +63 -0
  126. package/src/features/expand-all/types.ts +13 -0
  127. package/src/features/hotkeys-core/feature.ts +110 -0
  128. package/src/features/hotkeys-core/types.ts +36 -0
  129. package/src/features/main/types.ts +48 -0
  130. package/src/features/renaming/feature.ts +105 -0
  131. package/src/features/renaming/types.ts +28 -0
  132. package/src/features/search/feature.ts +158 -0
  133. package/src/features/search/types.ts +40 -0
  134. package/src/features/selection/feature.ts +157 -0
  135. package/src/features/selection/types.ts +28 -0
  136. package/src/features/sync-data-loader/feature.ts +41 -0
  137. package/src/features/sync-data-loader/types.ts +20 -0
  138. package/src/features/tree/feature.ts +326 -0
  139. package/src/features/tree/types.ts +78 -0
  140. package/src/index.ts +26 -0
  141. package/src/mddocs-entry.ts +26 -0
  142. package/src/types/core.ts +183 -0
  143. package/src/types/deep-merge.ts +31 -0
  144. package/src/utilities/create-on-drop-handler.ts +14 -0
  145. package/src/utilities/insert-items-at-target.ts +30 -0
  146. package/src/utilities/remove-items-from-parents.ts +21 -0
  147. package/src/utils.ts +68 -0
  148. package/tsconfig.json +7 -0
  149. package/typedoc.json +4 -0
@@ -0,0 +1,117 @@
1
+ import { ItemInstance, TreeInstance } from "../../types/core";
2
+ import {
3
+ DndState,
4
+ DragAndDropFeatureDef,
5
+ DropTarget,
6
+ DropTargetPosition,
7
+ } from "./types";
8
+
9
+ export const getDragCode = ({ item, childIndex }: DropTarget<any>) =>
10
+ `${item.getId()}__${childIndex ?? "none"}`;
11
+
12
+ export const getDropOffset = (e: any, item: ItemInstance<any>): number => {
13
+ const bb = item.getElement()?.getBoundingClientRect();
14
+ return bb ? (e.pageY - bb.top) / bb.height : 0.5;
15
+ };
16
+
17
+ export const canDrop = (
18
+ dataTransfer: DataTransfer | null,
19
+ target: DropTarget<any>,
20
+ tree: TreeInstance<any>
21
+ ) => {
22
+ const draggedItems = tree.getState().dnd?.draggedItems;
23
+ const config = tree.getConfig();
24
+
25
+ if (draggedItems && !(config.canDrop?.(draggedItems, target) ?? true)) {
26
+ return false;
27
+ }
28
+
29
+ if (
30
+ !draggedItems &&
31
+ dataTransfer &&
32
+ !config.canDropForeignDragObject?.(dataTransfer, target)
33
+ ) {
34
+ return false;
35
+ }
36
+
37
+ return true;
38
+ };
39
+
40
+ const getDropTargetPosition = (
41
+ offset: number,
42
+ topLinePercentage: number,
43
+ bottomLinePercentage: number
44
+ ) => {
45
+ if (offset < topLinePercentage) {
46
+ return DropTargetPosition.Top;
47
+ }
48
+ if (offset > bottomLinePercentage) {
49
+ return DropTargetPosition.Bottom;
50
+ }
51
+ return DropTargetPosition.Item;
52
+ };
53
+
54
+ export const getDropTarget = (
55
+ e: any,
56
+ item: ItemInstance<any>,
57
+ tree: TreeInstance<any>,
58
+ canDropInbetween = tree.getConfig().canDropInbetween
59
+ ): DropTarget<any> => {
60
+ const config = tree.getConfig();
61
+ const draggedItems = tree.getState().dnd?.draggedItems ?? [];
62
+ const itemTarget = { item, childIndex: null, insertionIndex: null };
63
+ const parentTarget = {
64
+ item: item.getParent(),
65
+ childIndex: null,
66
+ insertionIndex: null,
67
+ };
68
+
69
+ if (!canDropInbetween) {
70
+ if (!canDrop(e.dataTransfer, parentTarget, tree)) {
71
+ return getDropTarget(e, item.getParent(), tree, false);
72
+ }
73
+ return itemTarget;
74
+ }
75
+
76
+ const canDropInside = canDrop(e.dataTransfer, itemTarget, tree);
77
+
78
+ const offset = getDropOffset(e, item);
79
+
80
+ const pos = canDropInside
81
+ ? getDropTargetPosition(
82
+ offset,
83
+ config.topLinePercentage ?? 0.3,
84
+ config.bottomLinePercentage ?? 0.7
85
+ )
86
+ : getDropTargetPosition(offset, 0.5, 0.5);
87
+
88
+ if (pos === DropTargetPosition.Item) {
89
+ return itemTarget;
90
+ }
91
+
92
+ if (!canDrop(e.dataTransfer, parentTarget, tree)) {
93
+ return getDropTarget(e, item.getParent(), tree, false);
94
+ }
95
+
96
+ const childIndex =
97
+ item.getIndexInParent() + (pos === DropTargetPosition.Top ? 0 : 1);
98
+
99
+ const numberOfDragItemsBeforeTarget = item
100
+ .getParent()
101
+ .getChildren()
102
+ .slice(0, childIndex)
103
+ .reduce(
104
+ (counter, child) =>
105
+ child && draggedItems?.some((i) => i.getId() === child.getId())
106
+ ? ++counter
107
+ : counter,
108
+ 0
109
+ );
110
+
111
+ return {
112
+ item: item.getParent(),
113
+ childIndex,
114
+ // TODO performance could be improved by computing this only when dragcode changed
115
+ insertionIndex: childIndex - numberOfDragItemsBeforeTarget,
116
+ };
117
+ };
@@ -0,0 +1,63 @@
1
+ import { FeatureImplementation } from "../../types/core";
2
+ import { ExpandAllFeatureDef } from "./types";
3
+ import { MainFeatureDef } from "../main/types";
4
+ import { TreeFeatureDef } from "../tree/types";
5
+ import { SyncDataLoaderFeatureDef } from "../sync-data-loader/types";
6
+ import { poll } from "../../utils";
7
+
8
+ export const expandAllFeature: FeatureImplementation<
9
+ any,
10
+ ExpandAllFeatureDef,
11
+ | MainFeatureDef
12
+ | TreeFeatureDef<any>
13
+ | SyncDataLoaderFeatureDef<any>
14
+ | ExpandAllFeatureDef
15
+ > = {
16
+ key: "expand-all",
17
+
18
+ createTreeInstance: (prev, tree) => ({
19
+ ...prev,
20
+
21
+ expandAll: async (cancelToken) => {
22
+ await Promise.all(
23
+ tree.getItems().map((item) => item.expandAll(cancelToken))
24
+ );
25
+ },
26
+
27
+ collapseAll: () => {
28
+ tree.applySubStateUpdate("expandedItems", []);
29
+ tree.rebuildTree();
30
+ },
31
+ }),
32
+
33
+ createItemInstance: (prev, item, tree) => ({
34
+ ...prev,
35
+
36
+ expandAll: async (cancelToken) => {
37
+ if (cancelToken?.current) {
38
+ return;
39
+ }
40
+ if (!item.isFolder()) {
41
+ return;
42
+ }
43
+
44
+ item.expand();
45
+ await poll(() => !tree.getState().loadingItems.includes(item.getId()));
46
+ await Promise.all(
47
+ item.getChildren().map(async (child) => {
48
+ await poll(
49
+ () => !tree.getState().loadingItems.includes(child.getId())
50
+ );
51
+ await child?.expandAll(cancelToken);
52
+ })
53
+ );
54
+ },
55
+
56
+ collapseAll: () => {
57
+ for (const child of item.getChildren()) {
58
+ child?.collapseAll();
59
+ }
60
+ item.collapse();
61
+ },
62
+ }),
63
+ };
@@ -0,0 +1,13 @@
1
+ export type ExpandAllFeatureDef = {
2
+ state: {};
3
+ config: {};
4
+ treeInstance: {
5
+ expandAll: (cancelToken?: { current: boolean }) => Promise<void>;
6
+ collapseAll: () => void;
7
+ };
8
+ itemInstance: {
9
+ expandAll: (cancelToken?: { current: boolean }) => Promise<void>;
10
+ collapseAll: () => void;
11
+ };
12
+ hotkeys: never;
13
+ };
@@ -0,0 +1,110 @@
1
+ import {
2
+ FeatureImplementation,
3
+ HotkeysConfig,
4
+ TreeInstance,
5
+ } from "../../types/core";
6
+ import {
7
+ HotkeyConfig,
8
+ HotkeysCoreDataRef,
9
+ HotkeysCoreFeatureDef,
10
+ } from "./types";
11
+ import { MainFeatureDef } from "../main/types";
12
+
13
+ const specialKeys: Record<string, RegExp> = {
14
+ Letter: /^[a-z]$/,
15
+ LetterOrNumber: /^[a-z0-9]$/,
16
+ };
17
+
18
+ const testHotkeyMatch = (
19
+ pressedKeys: Set<string>,
20
+ tree: TreeInstance<any>,
21
+ hotkey: HotkeyConfig<any>
22
+ ) => {
23
+ const supposedKeys = hotkey.hotkey.split("+");
24
+ const doKeysMatch = supposedKeys.every((key) =>
25
+ key in specialKeys
26
+ ? [...pressedKeys].some((pressedKey) => specialKeys[key].test(pressedKey))
27
+ : pressedKeys.has(key)
28
+ );
29
+ const isEnabled = !hotkey.isEnabled || hotkey.isEnabled(tree);
30
+ const equalCounts = pressedKeys.size === supposedKeys.length;
31
+ return doKeysMatch && isEnabled && equalCounts;
32
+ };
33
+
34
+ const findHotkeyMatch = (
35
+ pressedKeys: Set<string>,
36
+ tree: TreeInstance<any>,
37
+ config1: HotkeysConfig<any, any>,
38
+ config2: HotkeysConfig<any, any>
39
+ ) => {
40
+ return Object.entries({ ...config1, ...config2 }).find(([, hotkey]) =>
41
+ testHotkeyMatch(pressedKeys, tree, hotkey)
42
+ )?.[0];
43
+ };
44
+
45
+ export const hotkeysCoreFeature: FeatureImplementation<
46
+ any,
47
+ HotkeysCoreFeatureDef<any>,
48
+ MainFeatureDef | HotkeysCoreFeatureDef<any>
49
+ > = {
50
+ key: "hotkeys-core",
51
+
52
+ onTreeMount: (tree, element) => {
53
+ const data = tree.getDataRef<HotkeysCoreDataRef>();
54
+ const keydown = (e: KeyboardEvent) => {
55
+ data.current.pressedKeys ??= new Set();
56
+ const newMatch = !data.current.pressedKeys.has(e.key);
57
+ data.current.pressedKeys.add(e.key);
58
+
59
+ const hotkeyName = findHotkeyMatch(
60
+ data.current.pressedKeys,
61
+ tree as any,
62
+ tree.getHotkeyPresets(),
63
+ tree.getConfig().hotkeys as HotkeysConfig<any>
64
+ );
65
+
66
+ if (!hotkeyName) return;
67
+
68
+ const hotkeyConfig: HotkeyConfig<any> = {
69
+ ...tree.getHotkeyPresets()[hotkeyName],
70
+ ...tree.getConfig().hotkeys?.[hotkeyName],
71
+ };
72
+
73
+ if (!hotkeyConfig) return;
74
+ if (
75
+ !hotkeyConfig.allowWhenInputFocused &&
76
+ e.target instanceof HTMLInputElement
77
+ )
78
+ return;
79
+ if (!hotkeyConfig.canRepeat && !newMatch) return;
80
+ if (hotkeyConfig.preventDefault) e.preventDefault();
81
+
82
+ hotkeyConfig.handler(e, tree as any);
83
+ };
84
+
85
+ const keyup = (e: KeyboardEvent) => {
86
+ data.current.pressedKeys ??= new Set();
87
+ data.current.pressedKeys.delete(e.key);
88
+ };
89
+
90
+ // keyup is registered on document, because some hotkeys shift
91
+ // the focus away from the tree (i.e. search)
92
+ // and then we wouldn't get the keyup event anymore
93
+ element.addEventListener("keydown", keydown);
94
+ document.addEventListener("keyup", keyup);
95
+ data.current.keydownHandler = keydown;
96
+ data.current.keyupHandler = keyup;
97
+ },
98
+
99
+ onTreeUnmount: (tree, element) => {
100
+ const data = tree.getDataRef<HotkeysCoreDataRef>();
101
+ if (data.current.keyupHandler) {
102
+ document.removeEventListener("keyup", data.current.keyupHandler);
103
+ delete data.current.keyupHandler;
104
+ }
105
+ if (data.current.keydownHandler) {
106
+ element.removeEventListener("keydown", data.current.keydownHandler);
107
+ delete data.current.keydownHandler;
108
+ }
109
+ },
110
+ };
@@ -0,0 +1,36 @@
1
+ import {
2
+ CustomHotkeysConfig,
3
+ ItemInstance,
4
+ TreeInstance,
5
+ } from "../../types/core";
6
+
7
+ export interface HotkeyConfig<T> {
8
+ hotkey: string;
9
+ canRepeat?: boolean;
10
+ allowWhenInputFocused?: boolean;
11
+ isEnabled?: (tree: TreeInstance<T>) => boolean;
12
+ preventDefault?: boolean;
13
+ handler: (e: KeyboardEvent, tree: TreeInstance<T>) => void;
14
+ }
15
+
16
+ export type HotkeysCoreDataRef = {
17
+ keydownHandler?: (e: KeyboardEvent) => void;
18
+ keyupHandler?: (e: KeyboardEvent) => void;
19
+ pressedKeys: Set<string>;
20
+ };
21
+
22
+ export type HotkeysCoreFeatureDef<T> = {
23
+ state: {};
24
+ config: {
25
+ hotkeys?: CustomHotkeysConfig<T>;
26
+ onTreeHotkey?: (name: string, element: HTMLElement) => void;
27
+ onItemHotkey?: (
28
+ name: string,
29
+ item: ItemInstance<T>,
30
+ element: HTMLElement
31
+ ) => void;
32
+ };
33
+ treeInstance: {};
34
+ itemInstance: {};
35
+ hotkeys: never;
36
+ };
@@ -0,0 +1,48 @@
1
+ import {
2
+ FeatureImplementation,
3
+ HotkeysConfig,
4
+ ItemInstance,
5
+ SetStateFn,
6
+ TreeConfig,
7
+ TreeState,
8
+ Updater,
9
+ } from "../../types/core";
10
+ import { ItemMeta } from "../tree/types";
11
+
12
+ export type MainFeatureDef<T = any> = {
13
+ state: {};
14
+ config: {
15
+ features?: FeatureImplementation<any>[];
16
+ initialState?: Partial<TreeState<T>>;
17
+ state?: Partial<TreeState<T>>;
18
+ setState?: SetStateFn<TreeState<T>>;
19
+ };
20
+ treeInstance: {
21
+ /** @internal */
22
+ applySubStateUpdate: <K extends keyof TreeState<any>>(
23
+ stateName: K,
24
+ updater: Updater<TreeState<T>[K]>
25
+ ) => void;
26
+ setState: SetStateFn<TreeState<T>>;
27
+ getState: () => TreeState<T>;
28
+ setConfig: SetStateFn<TreeConfig<T>>;
29
+ getConfig: () => TreeConfig<T>;
30
+ getItemInstance: (itemId: string) => ItemInstance<T>;
31
+ getItems: () => ItemInstance<T>[];
32
+ registerElement: (element: HTMLElement | null) => void;
33
+ getElement: () => HTMLElement | undefined | null;
34
+ /** @internal */
35
+ getDataRef: <D>() => { current: D };
36
+ /* @internal */
37
+ getHotkeyPresets: () => HotkeysConfig<T>;
38
+ rebuildTree: () => void;
39
+ };
40
+ itemInstance: {
41
+ registerElement: (element: HTMLElement | null) => void;
42
+ getItemMeta: () => ItemMeta;
43
+ getElement: () => HTMLElement | undefined | null;
44
+ /** @internal */
45
+ getDataRef: <D>() => { current: D };
46
+ };
47
+ hotkeys: never;
48
+ };
@@ -0,0 +1,105 @@
1
+ import { FeatureImplementation, ItemInstance } from "../../types/core";
2
+ import { RenamingFeatureDef } from "./types";
3
+ import { MainFeatureDef } from "../main/types";
4
+ import { TreeFeatureDef } from "../tree/types";
5
+ import { makeStateUpdater } from "../../utils";
6
+
7
+ export const renamingFeature: FeatureImplementation<
8
+ any,
9
+ RenamingFeatureDef<any>,
10
+ MainFeatureDef | TreeFeatureDef<any> | RenamingFeatureDef<any>
11
+ > = {
12
+ key: "renaming",
13
+
14
+ getDefaultConfig: (defaultConfig, tree) => ({
15
+ setRenamingItem: makeStateUpdater("renamingItem", tree),
16
+ setRenamingValue: makeStateUpdater("renamingValue", tree),
17
+ canRename: () => true,
18
+ ...defaultConfig,
19
+ }),
20
+
21
+ stateHandlerNames: {
22
+ renamingItem: "setRenamingItem",
23
+ renamingValue: "setRenamingValue",
24
+ },
25
+
26
+ createTreeInstance: (prev, instance) => ({
27
+ ...prev,
28
+
29
+ startRenamingItem: (itemId) => {
30
+ const item = instance.getItemInstance(itemId);
31
+
32
+ if (!item.canRename()) {
33
+ return;
34
+ }
35
+
36
+ instance.applySubStateUpdate("renamingItem", itemId);
37
+ instance.applySubStateUpdate("renamingValue", item.getItemName());
38
+ },
39
+
40
+ getRenamingItem: () => {
41
+ const itemId = instance.getState().renamingItem;
42
+ return itemId ? instance.getItemInstance(itemId) : null;
43
+ },
44
+
45
+ getRenamingValue: () => instance.getState().renamingValue || "",
46
+
47
+ abortRenaming: () => {
48
+ instance.applySubStateUpdate("renamingItem", null);
49
+ },
50
+
51
+ completeRenaming: () => {
52
+ const config = instance.getConfig();
53
+ const item = instance.getRenamingItem();
54
+ if (item) {
55
+ config.onRename?.(item, instance.getState().renamingValue || "");
56
+ }
57
+ instance.applySubStateUpdate("renamingItem", null);
58
+ },
59
+
60
+ isRenamingItem: () => !!instance.getState().renamingItem,
61
+ }),
62
+
63
+ createItemInstance: (prev, instance, tree) => ({
64
+ ...prev,
65
+ getRenameInputProps: () => ({
66
+ onBlur: () => tree.abortRenaming(),
67
+ value: tree.getRenamingValue(),
68
+ onChange: (e) => {
69
+ tree.applySubStateUpdate("renamingValue", e.target.value);
70
+ },
71
+ }),
72
+
73
+ canRename: () =>
74
+ tree.getConfig().canRename?.(instance as ItemInstance<any>) ?? true,
75
+
76
+ isRenaming: () => instance.getId() === tree.getState().renamingItem,
77
+ }),
78
+
79
+ hotkeys: {
80
+ renameItem: {
81
+ hotkey: "F2",
82
+ handler: (e, tree) => {
83
+ tree.startRenamingItem(tree.getFocusedItem().getId());
84
+ },
85
+ },
86
+
87
+ abortRenaming: {
88
+ hotkey: "Escape",
89
+ allowWhenInputFocused: true,
90
+ isEnabled: (tree) => tree.isRenamingItem(),
91
+ handler: (e, tree) => {
92
+ tree.abortRenaming();
93
+ },
94
+ },
95
+
96
+ completeRenaming: {
97
+ hotkey: "Enter",
98
+ allowWhenInputFocused: true,
99
+ isEnabled: (tree) => tree.isRenamingItem(),
100
+ handler: (e, tree) => {
101
+ tree.completeRenaming();
102
+ },
103
+ },
104
+ },
105
+ };
@@ -0,0 +1,28 @@
1
+ import { ItemInstance, SetStateFn } from "../../types/core";
2
+
3
+ export type RenamingFeatureDef<T> = {
4
+ state: {
5
+ renamingItem?: string | null;
6
+ renamingValue?: string;
7
+ };
8
+ config: {
9
+ setRenamingItem?: SetStateFn<string | null>;
10
+ setRenamingValue?: SetStateFn<string | undefined>;
11
+ canRename?: (item: ItemInstance<T>) => boolean;
12
+ onRename?: (item: ItemInstance<T>, value: string) => void;
13
+ };
14
+ treeInstance: {
15
+ startRenamingItem: (itemId: string) => void;
16
+ getRenamingItem: () => ItemInstance<T> | null;
17
+ getRenamingValue: () => string;
18
+ abortRenaming: () => void;
19
+ completeRenaming: () => void;
20
+ isRenamingItem: () => boolean;
21
+ };
22
+ itemInstance: {
23
+ getRenameInputProps: () => any;
24
+ canRename: () => boolean;
25
+ isRenaming: () => boolean;
26
+ };
27
+ hotkeys: "renameItem" | "abortRenaming" | "completeRenaming";
28
+ };
@@ -0,0 +1,158 @@
1
+ import { FeatureImplementation } from "../../types/core";
2
+ import { SearchFeatureDataRef, SearchFeatureDef } from "./types";
3
+ import { MainFeatureDef } from "../main/types";
4
+ import { TreeFeatureDef } from "../tree/types";
5
+ import { makeStateUpdater, memo } from "../../utils";
6
+
7
+ export const searchFeature: FeatureImplementation<
8
+ any,
9
+ SearchFeatureDef<any>,
10
+ MainFeatureDef | TreeFeatureDef<any> | SearchFeatureDef<any>
11
+ > = {
12
+ key: "search",
13
+
14
+ getInitialState: (initialState) => ({
15
+ search: null,
16
+ ...initialState,
17
+ }),
18
+
19
+ getDefaultConfig: (defaultConfig, tree) => ({
20
+ setSearch: makeStateUpdater("search", tree),
21
+ isSearchMatchingItem: (search, item) =>
22
+ search.length > 0 &&
23
+ item.getItemName().toLowerCase().includes(search.toLowerCase()),
24
+ ...defaultConfig,
25
+ }),
26
+
27
+ stateHandlerNames: {
28
+ search: "setSearch",
29
+ },
30
+
31
+ createTreeInstance: (prev, instance) => ({
32
+ ...prev,
33
+
34
+ setSearch: (search) => {
35
+ instance.applySubStateUpdate("search", search);
36
+ instance
37
+ .getItems()
38
+ .find((item) =>
39
+ instance
40
+ .getConfig()
41
+ .isSearchMatchingItem?.(instance.getSearchValue(), item)
42
+ )
43
+ ?.setFocused();
44
+ },
45
+ openSearch: (initialValue = "") => {
46
+ instance.setSearch(initialValue);
47
+ setTimeout(() => {
48
+ instance
49
+ .getDataRef<SearchFeatureDataRef>()
50
+ .current.searchInput?.focus();
51
+ });
52
+ },
53
+ closeSearch: () => {
54
+ instance.setSearch(null);
55
+ instance.updateDomFocus();
56
+ },
57
+ isSearchOpen: () => instance.getState().search !== null,
58
+ getSearchValue: () => instance.getState().search || "",
59
+ registerSearchInputElement: (element) => {
60
+ const dataRef = instance.getDataRef<SearchFeatureDataRef>();
61
+ dataRef.current.searchInput = element;
62
+ if (element && dataRef.current.keydownHandler) {
63
+ element.addEventListener("keydown", dataRef.current.keydownHandler);
64
+ }
65
+ },
66
+ getSearchInputElement: () =>
67
+ instance.getDataRef<SearchFeatureDataRef>().current.searchInput ?? null,
68
+
69
+ getSearchInputElementProps: () => ({
70
+ value: instance.getSearchValue(),
71
+ onChange: (e: any) => instance.setSearch(e.target.value),
72
+ onBlur: () => instance.closeSearch(),
73
+ }),
74
+
75
+ getSearchMatchingItems: memo(
76
+ (search, items) =>
77
+ items.filter(
78
+ (item) =>
79
+ search && instance.getConfig().isSearchMatchingItem?.(search, item)
80
+ ),
81
+ () => [instance.getSearchValue(), instance.getItems()]
82
+ ),
83
+ }),
84
+
85
+ createItemInstance: (prev, item, tree) => ({
86
+ ...prev,
87
+ isMatchingSearch: () =>
88
+ tree.getSearchMatchingItems().some((i) => i.getId() === item.getId()),
89
+ }),
90
+
91
+ hotkeys: {
92
+ openSearch: {
93
+ hotkey: "LetterOrNumber",
94
+ preventDefault: true, // TODO make true default
95
+ isEnabled: (tree) => !tree.isSearchOpen(),
96
+ handler: (e, tree) => {
97
+ e.stopPropagation();
98
+ tree.openSearch(e.key);
99
+ },
100
+ },
101
+
102
+ closeSearch: {
103
+ // TODO allow multiple, i.e. Enter
104
+ hotkey: "Escape",
105
+ allowWhenInputFocused: true,
106
+ isEnabled: (tree) => tree.isSearchOpen(),
107
+ handler: (e, tree) => {
108
+ tree.closeSearch();
109
+ },
110
+ },
111
+
112
+ submitSearch: {
113
+ hotkey: "Enter",
114
+ allowWhenInputFocused: true,
115
+ isEnabled: (tree) => tree.isSearchOpen(),
116
+ handler: (e, tree) => {
117
+ tree.closeSearch();
118
+ tree.setSelectedItems([tree.getFocusedItem().getId()]);
119
+ },
120
+ },
121
+
122
+ nextSearchItem: {
123
+ hotkey: "ArrowDown",
124
+ allowWhenInputFocused: true,
125
+ canRepeat: true,
126
+ isEnabled: (tree) => tree.isSearchOpen(),
127
+ handler: (e, tree) => {
128
+ const focusItem = tree
129
+ .getSearchMatchingItems()
130
+ .find(
131
+ (item) =>
132
+ item.getItemMeta().index >
133
+ tree.getFocusedItem().getItemMeta().index
134
+ );
135
+ focusItem?.setFocused();
136
+ focusItem?.scrollTo({ block: "nearest", inline: "nearest" });
137
+ },
138
+ },
139
+
140
+ previousSearchItem: {
141
+ hotkey: "ArrowUp",
142
+ allowWhenInputFocused: true,
143
+ canRepeat: true,
144
+ isEnabled: (tree) => tree.isSearchOpen(),
145
+ handler: (e, tree) => {
146
+ const focusItem = [...tree.getSearchMatchingItems()]
147
+ .reverse()
148
+ .find(
149
+ (item) =>
150
+ item.getItemMeta().index <
151
+ tree.getFocusedItem().getItemMeta().index
152
+ );
153
+ focusItem?.setFocused();
154
+ focusItem?.scrollTo({ block: "nearest", inline: "nearest" });
155
+ },
156
+ },
157
+ },
158
+ };