@headless-tree/core 1.0.0 → 1.0.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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @headless-tree/core
2
2
 
3
+ ## 1.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - c9f9932: fixed tree.focusNextItem() and tree.focusPreviousItem() throwing if no item is currently focused
8
+ - 6ed84b4: recursive item references are filtered out when rendering (#89)
9
+ - 4bef2f2: fixed a bug where hotkeys involving shift may not work properly depending on the order of shift and other key inputs (#98)
10
+
3
11
  ## 1.0.0
4
12
 
5
13
  ### Minor Changes
@@ -8,7 +8,7 @@ const specialKeys = {
8
8
  Space: /^ $/,
9
9
  };
10
10
  const testHotkeyMatch = (pressedKeys, tree, hotkey) => {
11
- const supposedKeys = hotkey.hotkey.split("+");
11
+ const supposedKeys = hotkey.hotkey.toLowerCase().split("+");
12
12
  const doKeysMatch = supposedKeys.every((key) => key in specialKeys
13
13
  ? [...pressedKeys].some((pressedKey) => specialKeys[key].test(pressedKey))
14
14
  : pressedKeys.has(key));
@@ -27,10 +27,10 @@ exports.hotkeysCoreFeature = {
27
27
  const keydown = (e) => {
28
28
  var _a, _b, _c, _d;
29
29
  var _e;
30
+ const key = e.key.toLowerCase();
30
31
  (_a = (_e = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_e.pressedKeys = new Set());
31
- const newMatch = !data.current.pressedKeys.has(e.key);
32
- data.current.pressedKeys.add(e.key);
33
- console.log("HOTKEYS", data.current.pressedKeys);
32
+ const newMatch = !data.current.pressedKeys.has(key);
33
+ data.current.pressedKeys.add(key);
34
34
  const hotkeyName = findHotkeyMatch(data.current.pressedKeys, tree, tree.getHotkeyPresets(), tree.getConfig().hotkeys);
35
35
  if (!hotkeyName)
36
36
  return;
@@ -51,7 +51,7 @@ exports.hotkeysCoreFeature = {
51
51
  var _a;
52
52
  var _b;
53
53
  (_a = (_b = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_b.pressedKeys = new Set());
54
- data.current.pressedKeys.delete(e.key);
54
+ data.current.pressedKeys.delete(e.key.toLowerCase());
55
55
  };
56
56
  // keyup is registered on document, because some hotkeys shift
57
57
  // the focus away from the tree (i.e. search)
@@ -28,9 +28,9 @@ exports.selectionFeature = {
28
28
  const { selectedItems } = tree.getState();
29
29
  tree.setSelectedItems(selectedItems.filter((id) => id !== itemId));
30
30
  },
31
- isSelected: ({ tree, item }) => {
31
+ isSelected: ({ tree, itemId }) => {
32
32
  const { selectedItems } = tree.getState();
33
- return selectedItems.includes(item.getItemMeta().itemId);
33
+ return selectedItems.includes(itemId);
34
34
  },
35
35
  selectUpTo: ({ tree, item }, ctrl) => {
36
36
  const indexA = item.getItemMeta().index;
@@ -11,6 +11,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.treeFeature = void 0;
13
13
  const utils_1 = require("../../utils");
14
+ const errors_1 = require("../../utilities/errors");
14
15
  exports.treeFeature = {
15
16
  key: "tree",
16
17
  getInitialState: (initialState) => (Object.assign({ expandedItems: [], focusedItem: null }, initialState)),
@@ -25,13 +26,17 @@ exports.treeFeature = {
25
26
  const { expandedItems } = tree.getState();
26
27
  const flatItems = [];
27
28
  const expandedItemsSet = new Set(expandedItems); // TODO support setting state expandedItems as set instead of array
28
- const recursiveAdd = (itemId, parentId, level, setSize, posInSet) => {
29
+ const recursiveAdd = (itemId, path, level, setSize, posInSet) => {
29
30
  var _a;
31
+ if (path.includes(itemId)) {
32
+ (0, errors_1.logWarning)(`Circular reference for ${path.join(".")}`);
33
+ return;
34
+ }
30
35
  flatItems.push({
31
36
  itemId,
32
37
  level,
33
38
  index: flatItems.length,
34
- parentId,
39
+ parentId: path.at(-1),
35
40
  setSize,
36
41
  posInSet,
37
42
  });
@@ -39,14 +44,14 @@ exports.treeFeature = {
39
44
  const children = (_a = tree.retrieveChildrenIds(itemId)) !== null && _a !== void 0 ? _a : [];
40
45
  let i = 0;
41
46
  for (const childId of children) {
42
- recursiveAdd(childId, itemId, level + 1, children.length, i++);
47
+ recursiveAdd(childId, path.concat(itemId), level + 1, children.length, i++);
43
48
  }
44
49
  }
45
50
  };
46
51
  const children = tree.retrieveChildrenIds(rootItemId);
47
52
  let i = 0;
48
53
  for (const itemId of children) {
49
- recursiveAdd(itemId, rootItemId, 0, children.length, i++);
54
+ recursiveAdd(itemId, [rootItemId], 0, children.length, i++);
50
55
  }
51
56
  return flatItems;
52
57
  },
@@ -56,14 +61,18 @@ exports.treeFeature = {
56
61
  },
57
62
  focusNextItem: ({ tree }) => {
58
63
  var _a;
59
- const { index } = tree.getFocusedItem().getItemMeta();
60
- const nextIndex = Math.min(index + 1, tree.getItems().length - 1);
64
+ const focused = tree.getFocusedItem().getItemMeta();
65
+ if (!focused)
66
+ return;
67
+ const nextIndex = Math.min(focused.index + 1, tree.getItems().length - 1);
61
68
  (_a = tree.getItems()[nextIndex]) === null || _a === void 0 ? void 0 : _a.setFocused();
62
69
  },
63
70
  focusPreviousItem: ({ tree }) => {
64
71
  var _a;
65
- const { index } = tree.getFocusedItem().getItemMeta();
66
- const nextIndex = Math.max(index - 1, 0);
72
+ const focused = tree.getFocusedItem().getItemMeta();
73
+ if (!focused)
74
+ return;
75
+ const nextIndex = Math.max(focused.index - 1, 0);
67
76
  (_a = tree.getItems()[nextIndex]) === null || _a === void 0 ? void 0 : _a.setFocused();
68
77
  },
69
78
  updateDomFocus: ({ tree }) => {
@@ -1 +1,2 @@
1
1
  export declare const throwError: (message: string) => Error;
2
+ export declare const logWarning: (message: string) => void;
@@ -1,5 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.throwError = void 0;
4
- const throwError = (message) => Error(`Headless Tree: ${message}`);
3
+ exports.logWarning = exports.throwError = void 0;
4
+ const prefix = "Headless Tree: ";
5
+ const throwError = (message) => Error(prefix + message);
5
6
  exports.throwError = throwError;
7
+ // eslint-disable-next-line no-console
8
+ const logWarning = (message) => console.warn(prefix + message);
9
+ exports.logWarning = logWarning;
@@ -5,7 +5,7 @@ const specialKeys = {
5
5
  Space: /^ $/,
6
6
  };
7
7
  const testHotkeyMatch = (pressedKeys, tree, hotkey) => {
8
- const supposedKeys = hotkey.hotkey.split("+");
8
+ const supposedKeys = hotkey.hotkey.toLowerCase().split("+");
9
9
  const doKeysMatch = supposedKeys.every((key) => key in specialKeys
10
10
  ? [...pressedKeys].some((pressedKey) => specialKeys[key].test(pressedKey))
11
11
  : pressedKeys.has(key));
@@ -24,10 +24,10 @@ export const hotkeysCoreFeature = {
24
24
  const keydown = (e) => {
25
25
  var _a, _b, _c, _d;
26
26
  var _e;
27
+ const key = e.key.toLowerCase();
27
28
  (_a = (_e = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_e.pressedKeys = new Set());
28
- const newMatch = !data.current.pressedKeys.has(e.key);
29
- data.current.pressedKeys.add(e.key);
30
- console.log("HOTKEYS", data.current.pressedKeys);
29
+ const newMatch = !data.current.pressedKeys.has(key);
30
+ data.current.pressedKeys.add(key);
31
31
  const hotkeyName = findHotkeyMatch(data.current.pressedKeys, tree, tree.getHotkeyPresets(), tree.getConfig().hotkeys);
32
32
  if (!hotkeyName)
33
33
  return;
@@ -48,7 +48,7 @@ export const hotkeysCoreFeature = {
48
48
  var _a;
49
49
  var _b;
50
50
  (_a = (_b = data.current).pressedKeys) !== null && _a !== void 0 ? _a : (_b.pressedKeys = new Set());
51
- data.current.pressedKeys.delete(e.key);
51
+ data.current.pressedKeys.delete(e.key.toLowerCase());
52
52
  };
53
53
  // keyup is registered on document, because some hotkeys shift
54
54
  // the focus away from the tree (i.e. search)
@@ -25,9 +25,9 @@ export const selectionFeature = {
25
25
  const { selectedItems } = tree.getState();
26
26
  tree.setSelectedItems(selectedItems.filter((id) => id !== itemId));
27
27
  },
28
- isSelected: ({ tree, item }) => {
28
+ isSelected: ({ tree, itemId }) => {
29
29
  const { selectedItems } = tree.getState();
30
- return selectedItems.includes(item.getItemMeta().itemId);
30
+ return selectedItems.includes(itemId);
31
31
  },
32
32
  selectUpTo: ({ tree, item }, ctrl) => {
33
33
  const indexA = item.getItemMeta().index;
@@ -8,6 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { makeStateUpdater, poll } from "../../utils";
11
+ import { logWarning } from "../../utilities/errors";
11
12
  export const treeFeature = {
12
13
  key: "tree",
13
14
  getInitialState: (initialState) => (Object.assign({ expandedItems: [], focusedItem: null }, initialState)),
@@ -22,13 +23,17 @@ export const treeFeature = {
22
23
  const { expandedItems } = tree.getState();
23
24
  const flatItems = [];
24
25
  const expandedItemsSet = new Set(expandedItems); // TODO support setting state expandedItems as set instead of array
25
- const recursiveAdd = (itemId, parentId, level, setSize, posInSet) => {
26
+ const recursiveAdd = (itemId, path, level, setSize, posInSet) => {
26
27
  var _a;
28
+ if (path.includes(itemId)) {
29
+ logWarning(`Circular reference for ${path.join(".")}`);
30
+ return;
31
+ }
27
32
  flatItems.push({
28
33
  itemId,
29
34
  level,
30
35
  index: flatItems.length,
31
- parentId,
36
+ parentId: path.at(-1),
32
37
  setSize,
33
38
  posInSet,
34
39
  });
@@ -36,14 +41,14 @@ export const treeFeature = {
36
41
  const children = (_a = tree.retrieveChildrenIds(itemId)) !== null && _a !== void 0 ? _a : [];
37
42
  let i = 0;
38
43
  for (const childId of children) {
39
- recursiveAdd(childId, itemId, level + 1, children.length, i++);
44
+ recursiveAdd(childId, path.concat(itemId), level + 1, children.length, i++);
40
45
  }
41
46
  }
42
47
  };
43
48
  const children = tree.retrieveChildrenIds(rootItemId);
44
49
  let i = 0;
45
50
  for (const itemId of children) {
46
- recursiveAdd(itemId, rootItemId, 0, children.length, i++);
51
+ recursiveAdd(itemId, [rootItemId], 0, children.length, i++);
47
52
  }
48
53
  return flatItems;
49
54
  },
@@ -53,14 +58,18 @@ export const treeFeature = {
53
58
  },
54
59
  focusNextItem: ({ tree }) => {
55
60
  var _a;
56
- const { index } = tree.getFocusedItem().getItemMeta();
57
- const nextIndex = Math.min(index + 1, tree.getItems().length - 1);
61
+ const focused = tree.getFocusedItem().getItemMeta();
62
+ if (!focused)
63
+ return;
64
+ const nextIndex = Math.min(focused.index + 1, tree.getItems().length - 1);
58
65
  (_a = tree.getItems()[nextIndex]) === null || _a === void 0 ? void 0 : _a.setFocused();
59
66
  },
60
67
  focusPreviousItem: ({ tree }) => {
61
68
  var _a;
62
- const { index } = tree.getFocusedItem().getItemMeta();
63
- const nextIndex = Math.max(index - 1, 0);
69
+ const focused = tree.getFocusedItem().getItemMeta();
70
+ if (!focused)
71
+ return;
72
+ const nextIndex = Math.max(focused.index - 1, 0);
64
73
  (_a = tree.getItems()[nextIndex]) === null || _a === void 0 ? void 0 : _a.setFocused();
65
74
  },
66
75
  updateDomFocus: ({ tree }) => {
@@ -1 +1,2 @@
1
1
  export declare const throwError: (message: string) => Error;
2
+ export declare const logWarning: (message: string) => void;
@@ -1 +1,4 @@
1
- export const throwError = (message) => Error(`Headless Tree: ${message}`);
1
+ const prefix = "Headless Tree: ";
2
+ export const throwError = (message) => Error(prefix + message);
3
+ // eslint-disable-next-line no-console
4
+ export const logWarning = (message) => console.warn(prefix + message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@headless-tree/core",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "main": "lib/cjs/index.js",
6
6
  "module": "lib/esm/index.js",
package/readme.md ADDED
@@ -0,0 +1,157 @@
1
+ ![Headless Tree](./packages/docs/static/img/banner-github.png)
2
+
3
+ [![Documentation](https://img.shields.io/badge/docs-1e1f22?style=flat)](https://headless-tree.lukasbach.com/)
4
+ [![Chat on Discord](https://img.shields.io/badge/discord-4c57d9?style=flat&logo=discord&logoColor=ffffff)](https://discord.gg/KuZ6EezzVw)
5
+ [![Follow on BLuesky](https://img.shields.io/badge/bluesky-0285FF?style=flat&logo=bluesky&logoColor=ffffff)](https://bsky.app/profile/lukasbach.bsky.social)
6
+ [![Support on Github Sponsors](https://img.shields.io/badge/sponsor-EA4AAA?style=flat&logo=githubsponsors&logoColor=ffffff)](https://github.com/sponsors/lukasbach)
7
+ [![Follow on Github](https://img.shields.io/badge/follow-181717?style=flat&logo=github&logoColor=ffffff)](https://github.com/lukasbach)
8
+ [![NPM Core package](https://img.shields.io/badge/core-CB3837?style=flat&logo=npm&logoColor=ffffff)](https://www.npmjs.com/package/@headless-tree/core)
9
+ [![NPM React package](https://img.shields.io/badge/react-CB3837?style=flat&logo=npm&logoColor=ffffff)](https://www.npmjs.com/package/@headless-tree/react)
10
+
11
+
12
+ Super-easy integration of complex tree components into React. Supports ordered
13
+ and unordered drag-and-drop, extensive keybindings, search, renaming and more.
14
+ Fully customizable and accessible. Headless Tree is the official successor for
15
+ [react-complex-tree](https://github.com/lukasbach/react-complex-tree).
16
+
17
+ It aims to bring the many features of complex tree views, like multi-select,
18
+ drag-and-drop, keyboard navigation, tree search, renaming and more, while
19
+ being unopinionated about the styling and rendering of the tree itself.
20
+ Accessibility is ensured by default, and the integration is extremely
21
+ simple and flexible.
22
+
23
+ The interface gives you a flat list of tree nodes
24
+ that you can easily render yourself, which keeps the complexity of the
25
+ code low and allows you to customize the tree to your needs. This flat
26
+ structure also allows you to virtualize the tree with any virtualization
27
+ library you want. The library automatically provides the necessary
28
+ aria tags to emulate a nested tree structure, so that accessibility
29
+ requirements are met despite the flat structure.
30
+
31
+ Dive into [the Get Started page](https://headless-tree.lukasbach.com/getstarted)
32
+ to find out how to use Headless Tree, or have a look at
33
+ [the samples on the Headless Tree Homepage](https://headless-tree.lukasbach.com/#demogrid)
34
+ to get an idea of what you can do with it.
35
+
36
+ > [!TIP]
37
+ > Headless Tree is now available as Beta! The library is mostly stable and
38
+ > production ready, and will be generally released within two months, once
39
+ > I have collected feedback and fixed any bugs that might arise. I've written
40
+ > [a blog post](https://medium.com/@lukasbach/headless-tree-and-the-future-of-react-complex-tree-fc920700e82a)
41
+ > about the details of the change, and the future of the library.
42
+ >
43
+ > Join
44
+ > [the Discord](https://discord.gg/KuZ6EezzVw) to get involved, and
45
+ > [follow on Bluesky](https://bsky.app/profile/lukasbach.bsky.social) to
46
+ > stay up to date.
47
+
48
+ ## Features
49
+
50
+ - [Simple Interface](https://headless-tree.lukasbach.com/?demo=0#demogrid): Easy integration in React with full customizability of DOM
51
+ - [Drag and Drop](https://headless-tree.lukasbach.com/?demo=1#demogrid): Powerful ordered drag-and-drop, that can interact with external drag events
52
+ - [Scalable](https://headless-tree.lukasbach.com/?demo=2#demogrid): Headless Tree remains performant even with large trees
53
+ - [Virtualization Support](https://headless-tree.lukasbach.com/?demo=3#demogrid): Compatible with common virtualization library to support 100k+ items
54
+ - [Hotkeys!](https://headless-tree.lukasbach.com/?demo=4#demogrid): Lots of hotkeys, fully customizable
55
+ - [Search Support](https://headless-tree.lukasbach.com/?demo=5#demogrid): Typeahead anywhere in the tree to quickly search the entire tree
56
+ - [Rename items](https://headless-tree.lukasbach.com/?demo=6#demogrid): Optionally allow users to rename items inside the tree
57
+ - [Manage State](https://headless-tree.lukasbach.com/?demo=7#demogrid): Let Headless Tree manage tree state internally, or manage any part of it yourself
58
+ - [Customize Behavior](https://headless-tree.lukasbach.com/?demo=8#demogrid): Easily overwrite internal behavior like requiring double clicks on items to expand
59
+ - [Customize Logic](https://headless-tree.lukasbach.com/?demo=9#demogrid): Overwrite or expand any internal behavior of Headless Tree
60
+ - [Async Data Support](https://headless-tree.lukasbach.com/?demo=10#demogrid): Use synchronous or asynchronous data sources for your tree. Headless Tree comes with optional caching for async data
61
+ - Free of dependencies
62
+ - Or check out [this comprehensive playground](https://headless-tree.lukasbach.com/?demo=11#demogrid) that has most of the capabilities enabled.
63
+
64
+ ## Bundle Size
65
+
66
+ Headless Tree exports individual features in a tree-shaking-friendly
67
+ way, allowing you to only include what you need to keep your bundle size
68
+ small. Listed bundle sizes are based on min+gzipped bundles, and are
69
+ based on the Bundlephobia report as of Headless Tree v0.0.15.
70
+
71
+ | Feature | Bundle Size |
72
+ |------------------------|-------------|
73
+ | Tree Core | 3.1kB |
74
+ | Sync Data Loader | 0.8kB |
75
+ | Async Data Loader | 1.4kB |
76
+ | Selections | 1.1kB |
77
+ | Drag and Drop | 2.8kB |
78
+ | Keyboard Drag and Drop | 2.7kB |
79
+ | Hotkeys | 0.8kB |
80
+ | Tree Search | 1.3kB |
81
+ | Renaming | 0.9kB |
82
+ | Expand All | 0.7kB |
83
+ | React Bindings | 0.4kB |
84
+
85
+ Total bundle size is 9.5kB plus 0.4kB for the React bindings. Note that
86
+ the sum of features is bigger than the total bundle size, because several
87
+ features share code. Tree-shaking will ensure that the minimum amount of
88
+ code is included in your bundle.
89
+
90
+ ## Get Started
91
+
92
+ > [!TIP]
93
+ > You can find a comprehensive [get-started guide](https://headless-tree.lukasbach.com/getstarted)
94
+ > on the documentation homepage. The following gives a brief overview.
95
+
96
+ Install Headless Tree via npm:
97
+
98
+ ```bash
99
+ npm install @headless-tree/core @headless-tree/react
100
+ ```
101
+
102
+ In your react component, call the `useTree` hook from `@headless-tree/react` with the configuration of
103
+ your tree:
104
+
105
+ ```tsx
106
+ import {
107
+ hotkeysCoreFeature,
108
+ selectionFeature,
109
+ syncDataLoaderFeature,
110
+ } from "@headless-tree/core";
111
+ import { useTree } from "@headless-tree/react";
112
+
113
+ const tree = useTree<string>({
114
+ initialState: { expandedItems: ["folder-1"] },
115
+ rootItemId: "folder",
116
+ getItemName: (item) => item.getItemData(),
117
+ isItemFolder: (item) => !item.getItemData().endsWith("item"),
118
+ dataLoader: {
119
+ getItem: (itemId) => itemId,
120
+ getChildren: (itemId) => [
121
+ `${itemId}-folder`,
122
+ `${itemId}-1-item`,
123
+ `${itemId}-2-item`,
124
+ ],
125
+ },
126
+ indent: 20,
127
+ features: [syncDataLoaderFeature, selectionFeature, hotkeysCoreFeature],
128
+ });
129
+ ```
130
+
131
+ Then, render your tree based on the tree instance returned from the hook:
132
+
133
+ ```tsx
134
+ <div {...tree.getContainerProps()} className="tree">
135
+ {tree.getItems().map((item) => (
136
+ <button
137
+ {...item.getProps()}
138
+ key={item.getId()}
139
+ style={{ paddingLeft: `${item.getItemMeta().level * 20}px` }}
140
+ >
141
+ <div
142
+ className={cx("treeitem", {
143
+ focused: item.isFocused(),
144
+ expanded: item.isExpanded(),
145
+ selected: item.isSelected(),
146
+ folder: item.isFolder(),
147
+ })}
148
+ >
149
+ {item.getItemName()}
150
+ </div>
151
+ </button>
152
+ ))}
153
+ </div>
154
+ ```
155
+
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.
@@ -17,7 +17,7 @@ const testHotkeyMatch = (
17
17
  tree: TreeInstance<any>,
18
18
  hotkey: HotkeyConfig<any>,
19
19
  ) => {
20
- const supposedKeys = hotkey.hotkey.split("+");
20
+ const supposedKeys = hotkey.hotkey.toLowerCase().split("+");
21
21
  const doKeysMatch = supposedKeys.every((key) =>
22
22
  key in specialKeys
23
23
  ? [...pressedKeys].some((pressedKey) => specialKeys[key].test(pressedKey))
@@ -45,10 +45,10 @@ export const hotkeysCoreFeature: FeatureImplementation = {
45
45
  onTreeMount: (tree, element) => {
46
46
  const data = tree.getDataRef<HotkeysCoreDataRef>();
47
47
  const keydown = (e: KeyboardEvent) => {
48
+ const key = e.key.toLowerCase();
48
49
  data.current.pressedKeys ??= new Set();
49
- const newMatch = !data.current.pressedKeys.has(e.key);
50
- data.current.pressedKeys.add(e.key);
51
- console.log("HOTKEYS", data.current.pressedKeys);
50
+ const newMatch = !data.current.pressedKeys.has(key);
51
+ data.current.pressedKeys.add(key);
52
52
 
53
53
  const hotkeyName = findHotkeyMatch(
54
54
  data.current.pressedKeys,
@@ -79,7 +79,7 @@ export const hotkeysCoreFeature: FeatureImplementation = {
79
79
 
80
80
  const keyup = (e: KeyboardEvent) => {
81
81
  data.current.pressedKeys ??= new Set();
82
- data.current.pressedKeys.delete(e.key);
82
+ data.current.pressedKeys.delete(e.key.toLowerCase());
83
83
  };
84
84
 
85
85
  // keyup is registered on document, because some hotkeys shift
@@ -22,7 +22,7 @@ export type SearchFeatureDef<T> = {
22
22
  closeSearch: () => void;
23
23
  isSearchOpen: () => boolean;
24
24
  getSearchValue: () => string;
25
- registerSearchInputElement: (element: HTMLInputElement | null) => void;
25
+ registerSearchInputElement: (element: HTMLInputElement | null) => void; // TODO remove
26
26
  getSearchInputElement: () => HTMLInputElement | null;
27
27
  getSearchInputElementProps: () => any;
28
28
  getSearchMatchingItems: () => ItemInstance<T>[];
@@ -43,9 +43,9 @@ export const selectionFeature: FeatureImplementation = {
43
43
  tree.setSelectedItems(selectedItems.filter((id) => id !== itemId));
44
44
  },
45
45
 
46
- isSelected: ({ tree, item }) => {
46
+ isSelected: ({ tree, itemId }) => {
47
47
  const { selectedItems } = tree.getState();
48
- return selectedItems.includes(item.getItemMeta().itemId);
48
+ return selectedItems.includes(itemId);
49
49
  },
50
50
 
51
51
  selectUpTo: ({ tree, item }, ctrl: boolean) => {
@@ -1,6 +1,7 @@
1
1
  import { FeatureImplementation, ItemInstance } from "../../types/core";
2
2
  import { ItemMeta } from "./types";
3
3
  import { makeStateUpdater, poll } from "../../utils";
4
+ import { logWarning } from "../../utilities/errors";
4
5
 
5
6
  export const treeFeature: FeatureImplementation<any> = {
6
7
  key: "tree",
@@ -31,16 +32,21 @@ export const treeFeature: FeatureImplementation<any> = {
31
32
 
32
33
  const recursiveAdd = (
33
34
  itemId: string,
34
- parentId: string,
35
+ path: string[],
35
36
  level: number,
36
37
  setSize: number,
37
38
  posInSet: number,
38
39
  ) => {
40
+ if (path.includes(itemId)) {
41
+ logWarning(`Circular reference for ${path.join(".")}`);
42
+ return;
43
+ }
44
+
39
45
  flatItems.push({
40
46
  itemId,
41
47
  level,
42
48
  index: flatItems.length,
43
- parentId,
49
+ parentId: path.at(-1) as string,
44
50
  setSize,
45
51
  posInSet,
46
52
  });
@@ -49,7 +55,13 @@ export const treeFeature: FeatureImplementation<any> = {
49
55
  const children = tree.retrieveChildrenIds(itemId) ?? [];
50
56
  let i = 0;
51
57
  for (const childId of children) {
52
- recursiveAdd(childId, itemId, level + 1, children.length, i++);
58
+ recursiveAdd(
59
+ childId,
60
+ path.concat(itemId),
61
+ level + 1,
62
+ children.length,
63
+ i++,
64
+ );
53
65
  }
54
66
  }
55
67
  };
@@ -57,7 +69,7 @@ export const treeFeature: FeatureImplementation<any> = {
57
69
  const children = tree.retrieveChildrenIds(rootItemId);
58
70
  let i = 0;
59
71
  for (const itemId of children) {
60
- recursiveAdd(itemId, rootItemId, 0, children.length, i++);
72
+ recursiveAdd(itemId, [rootItemId], 0, children.length, i++);
61
73
  }
62
74
 
63
75
  return flatItems;
@@ -71,14 +83,16 @@ export const treeFeature: FeatureImplementation<any> = {
71
83
  },
72
84
 
73
85
  focusNextItem: ({ tree }) => {
74
- const { index } = tree.getFocusedItem().getItemMeta();
75
- const nextIndex = Math.min(index + 1, tree.getItems().length - 1);
86
+ const focused = tree.getFocusedItem().getItemMeta();
87
+ if (!focused) return;
88
+ const nextIndex = Math.min(focused.index + 1, tree.getItems().length - 1);
76
89
  tree.getItems()[nextIndex]?.setFocused();
77
90
  },
78
91
 
79
92
  focusPreviousItem: ({ tree }) => {
80
- const { index } = tree.getFocusedItem().getItemMeta();
81
- const nextIndex = Math.max(index - 1, 0);
93
+ const focused = tree.getFocusedItem().getItemMeta();
94
+ if (!focused) return;
95
+ const nextIndex = Math.max(focused.index - 1, 0);
82
96
  tree.getItems()[nextIndex]?.setFocused();
83
97
  },
84
98
 
@@ -1,2 +1,6 @@
1
- export const throwError = (message: string) =>
2
- Error(`Headless Tree: ${message}`);
1
+ const prefix = "Headless Tree: ";
2
+
3
+ export const throwError = (message: string) => Error(prefix + message);
4
+
5
+ // eslint-disable-next-line no-console
6
+ export const logWarning = (message: string) => console.warn(prefix + message);