kleinigkeiten
This commit is contained in:
+881
@@ -0,0 +1,881 @@
|
||||

|
||||
|
||||
<h1>React Arborist</h1>
|
||||
|
||||
[See the Demos](https://react-arborist.netlify.app/)
|
||||
|
||||
The tree view is ubiquitous in software applications. This library provides the React ecosystem with a complete solution to build the equivalent of a VSCode sidebar, Mac Finder, Windows Explorer, or Sketch/Figma layers panel.
|
||||
|
||||
Here is a Gmail sidebar clone built with react-arborist.
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/3460638/197306119-59fe59e6-50ae-4bc2-8cb9-3faa2bc52cd2.gif" width="270px" alt="Gmail sidebar clone built with react-arborist" />
|
||||
|
||||
## Features
|
||||
|
||||
- Drag and drop sorting
|
||||
- Open/close folders
|
||||
- Inline renaming
|
||||
- Virtualized rendering
|
||||
- Custom styling
|
||||
- Keyboard navigation
|
||||
- Aria attributes
|
||||
- Tree filtering
|
||||
- Selection synchronization
|
||||
- Callbacks (onScroll, onActivate, onSelect)
|
||||
- Controlled or uncontrolled trees
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
yarn add react-arborist
|
||||
```
|
||||
|
||||
```
|
||||
npm install react-arborist
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Assume our data is this:
|
||||
|
||||
```js
|
||||
const data = [
|
||||
{ id: "1", name: "Unread" },
|
||||
{ id: "2", name: "Threads" },
|
||||
{
|
||||
id: "3",
|
||||
name: "Chat Rooms",
|
||||
children: [
|
||||
{ id: "c1", name: "General" },
|
||||
{ id: "c2", name: "Random" },
|
||||
{ id: "c3", name: "Open Source Projects" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "Direct Messages",
|
||||
children: [
|
||||
{ id: "d1", name: "Alice" },
|
||||
{ id: "d2", name: "Bob" },
|
||||
{ id: "d3", name: "Charlie" },
|
||||
],
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### The Simplest Tree
|
||||
|
||||
Use all the defaults. The _initialData_ prop makes the tree an uncontrolled component. Create, move, rename, and delete will be handled internally.
|
||||
|
||||
```jsx
|
||||
import { Tree } from 'react-arborist';
|
||||
|
||||
function App() {
|
||||
return <Tree initialData={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
<img width="214" alt="image" src="https://user-images.githubusercontent.com/3460638/198098015-d7dc6400-6391-4094-9f66-0f56a99433e9.png">
|
||||
|
||||
[Demo](https://codesandbox.io/s/the-simplest-tree-7tbedw)
|
||||
|
||||
### Customize the Appearance
|
||||
|
||||
We provide our own dimensions and our own `Node` component.
|
||||
|
||||
```jsx
|
||||
function App() {
|
||||
return (
|
||||
<Tree
|
||||
initialData={data}
|
||||
openByDefault={false}
|
||||
width={600}
|
||||
height={1000}
|
||||
indent={24}
|
||||
rowHeight={36}
|
||||
overscanCount={1}
|
||||
paddingTop={30}
|
||||
paddingBottom={10}
|
||||
padding={25 /* sets both */}
|
||||
>
|
||||
{Node}
|
||||
</Tree>
|
||||
);
|
||||
}
|
||||
|
||||
function Node({ node, style, dragHandle }) {
|
||||
/* This node instance can do many things. See the API reference. */
|
||||
return (
|
||||
<div style={style} ref={dragHandle}>
|
||||
{node.isLeaf ? "🍁" : "🗀"}
|
||||
{node.data.name}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
<img width="166" alt="image" src="https://user-images.githubusercontent.com/3460638/198100281-594a492d-2ea0-4ff0-883d-1dd79dbb5acd.png">
|
||||
|
||||
[Demo](https://codesandbox.io/s/customize-appearance-f4g15v?file=/src/App.tsx)
|
||||
|
||||
### Control the Tree data
|
||||
|
||||
Here we use the _data_ prop to make the tree a controlled component. We must handle all the data modifications ourselves using the props below.
|
||||
|
||||
```jsx
|
||||
function App() {
|
||||
/* Handle the data modifications outside the tree component */
|
||||
const onCreate = ({ parentId, index, type }) => {};
|
||||
const onRename = ({ id, name }) => {};
|
||||
const onMove = ({ dragIds, parentId, index }) => {};
|
||||
const onDelete = ({ ids }) => {};
|
||||
|
||||
return (
|
||||
<Tree
|
||||
data={data}
|
||||
onCreate={onCreate}
|
||||
onRename={onRename}
|
||||
onMove={onMove}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### The `onMove` index
|
||||
|
||||
`onMove`'s `index` is a **pre-removal slot**: it counts positions in the destination parent's child list _as shown on screen_, with the dragged rows still in place. If your handler removes the dragged rows before inserting them — the natural way to reorder an array — every dragged row that started before the drop slot shifts your target one place to the left, so dragging a row to just below itself would jump it past its neighbor (issue [#247](https://github.com/jameskerr/react-arborist/issues/247)).
|
||||
|
||||
`SimpleTree` / `useSimpleTree` insert before removing, so they need no adjustment. If you manage the data yourself, use the exported `adjustMoveIndex` helper:
|
||||
|
||||
```jsx
|
||||
import { adjustMoveIndex } from "react-arborist";
|
||||
|
||||
const onMove = ({ dragIds, index }) => {
|
||||
const to = adjustMoveIndex({ index, dragIds, siblingIds: data.map((d) => d.id) });
|
||||
const dragged = data.filter((d) => dragIds.includes(d.id));
|
||||
const rest = data.filter((d) => !dragIds.includes(d.id));
|
||||
rest.splice(to, 0, ...dragged);
|
||||
setData(rest);
|
||||
};
|
||||
```
|
||||
|
||||
Pass the destination parent's current child ids in display order (for a root-level move, that's your top-level data's ids). Ids that aren't among them — rows moving in from another parent — don't shift anything.
|
||||
|
||||
### Tree Filtering
|
||||
|
||||
Providing a non-empty _searchTerm_ will only show nodes that match. If a child matches, all its parents are shown too, so the tree keeps its structure. Internal nodes are opened when filtering. You can provide your own _searchMatch_ function, or use the default, which searches a node's own data, excluding its children. The default is deliberately loose: it JSON-stringifies the data's values and looks for the term anywhere in the result, so a term can also hit the keys of a nested object. Pass _searchMatch_ when you want to match specific fields.
|
||||
|
||||
```jsx
|
||||
function App() {
|
||||
const term = useSearchTermString()
|
||||
<Tree
|
||||
data={data}
|
||||
searchTerm={term}
|
||||
searchMatch={
|
||||
(node, term) => node.data.name.toLowerCase().includes(term.toLowerCase())
|
||||
}
|
||||
/>
|
||||
}
|
||||
```
|
||||
|
||||
### Sync the Selection
|
||||
|
||||
It's common to open something elsewhere in the app, but have the tree reflect the new selection.
|
||||
|
||||
Passing an id to the _selection_ prop will select and scroll to that node whenever that id changes.
|
||||
|
||||
```jsx
|
||||
function App() {
|
||||
const chatId = useCurrentChatId();
|
||||
|
||||
/*
|
||||
Whenever the currentChatRoomId changes,
|
||||
the tree will automatically select it and scroll to it.
|
||||
*/
|
||||
|
||||
return <Tree initialData={data} selection={chatId} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Use the Tree Api Instance
|
||||
|
||||
You can access the Tree Api in the parent component by giving a ref to the tree.
|
||||
|
||||
```jsx
|
||||
function App() {
|
||||
const treeRef = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
const tree = treeRef.current;
|
||||
tree.selectAll();
|
||||
/* See the Tree API reference for all you can do with it. */
|
||||
}, []);
|
||||
|
||||
return <Tree initialData={data} ref={treeRef} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Data with Different Property Names
|
||||
|
||||
The _idAccessor_ and _childrenAccessor_ props allow you to specify the children and id fields in your data.
|
||||
|
||||
```jsx
|
||||
function App() {
|
||||
const data = [
|
||||
{
|
||||
category: "Food",
|
||||
subCategories: [{ category: "Restaurants" }, { category: "Groceries" }],
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Tree
|
||||
data={data}
|
||||
/* An accessor can provide a string property name */
|
||||
idAccessor="category"
|
||||
/* or a function with the data as the argument */
|
||||
childrenAccessor={(d) => d.subCategories}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Rendering
|
||||
|
||||
Render every single piece of the tree yourself. See the API reference for the props passed to each renderer.
|
||||
|
||||
```jsx
|
||||
function App() {
|
||||
return (
|
||||
<Tree
|
||||
data={data}
|
||||
/* The outer most element in the list */
|
||||
renderRow={MyRow}
|
||||
/* The "ghost" element that follows the mouse as you drag */
|
||||
renderDragPreview={MyDragPreview}
|
||||
/* The line that shows where an element will be dropped */
|
||||
renderCursor={MyCursor}
|
||||
>
|
||||
{/* The inner element that shows the indentation and data */}
|
||||
{MyNode}
|
||||
</Tree>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Dynamic sizing
|
||||
|
||||
You can add a ref to it with this package [Pmndrs/react-use-measure](https://github.com/pmndrs/react-use-measure)
|
||||
|
||||
That hook will measure the boundaries (for instance width, height, top, left) of a view you reference. Then you pass the width and the height to the Tree.
|
||||
|
||||
```js
|
||||
import useMeasure from "react-use-measure";
|
||||
|
||||
const [ref, bounds] = useMeasure();
|
||||
|
||||
<div className="parent" ref={ref}>
|
||||
<Tree height={bounds.height} width={bounds.width} />
|
||||
</div>
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- Components
|
||||
- [Tree Component Props](#tree-component-props)
|
||||
- [Row Component Props](#row-component-props)
|
||||
- [Node Component Props](#node-component-props)
|
||||
- [DragPreview Component Props](#dragpreview-component-props)
|
||||
- [Cursor Component Props](#cursor-component-props)
|
||||
- Interfaces
|
||||
- [Node API](#node-api-reference)
|
||||
- [Tree API](#tree-api-reference)
|
||||
- Utilities
|
||||
- [getTreeLinePrefix](#gettreelineprefix)
|
||||
|
||||
## Tree Component Props
|
||||
|
||||
These are all the props you can pass to the Tree component.
|
||||
|
||||
```ts
|
||||
interface TreeProps<T> {
|
||||
/* Data Options */
|
||||
data?: readonly T[];
|
||||
initialData?: readonly T[];
|
||||
|
||||
/* Data Handlers */
|
||||
onCreate?: handlers.CreateHandler<T>;
|
||||
onMove?: handlers.MoveHandler<T>;
|
||||
onRename?: handlers.RenameHandler<T>;
|
||||
onDelete?: handlers.DeleteHandler<T>;
|
||||
|
||||
/* Renderers*/
|
||||
children?: ElementType<renderers.NodeRendererProps<T>>;
|
||||
renderRow?: ElementType<renderers.RowRendererProps<T>>;
|
||||
renderDragPreview?: ElementType<renderers.DragPreviewProps>;
|
||||
renderCursor?: ElementType<renderers.CursorProps>;
|
||||
renderContainer?: ElementType<{}>;
|
||||
|
||||
/* Sizes */
|
||||
rowHeight?: number;
|
||||
overscanCount?: number;
|
||||
width?: number | string;
|
||||
height?: number;
|
||||
indent?: number;
|
||||
paddingTop?: number;
|
||||
paddingBottom?: number;
|
||||
padding?: number;
|
||||
|
||||
/* Config */
|
||||
childrenAccessor?: string | ((d: T) => readonly T[] | null);
|
||||
idAccessor?: string | ((d: T) => string);
|
||||
openByDefault?: boolean;
|
||||
selectionFollowsFocus?: boolean;
|
||||
disableMultiSelection?: boolean;
|
||||
disableDeselectOnClick?: boolean;
|
||||
disableSelect?: string | boolean | BoolFunc<T>;
|
||||
disableEdit?: string | boolean | BoolFunc<T>;
|
||||
disableDrag?: string | boolean | BoolFunc<T>;
|
||||
disableDrop?:
|
||||
| string
|
||||
| boolean
|
||||
| ((args: {
|
||||
parentNode: NodeApi<T>;
|
||||
dragNodes: NodeApi<T>[];
|
||||
index: number;
|
||||
}) => boolean);
|
||||
|
||||
/* Event Handlers */
|
||||
onActivate?: (node: NodeApi<T>) => void;
|
||||
onSelect?: (nodes: NodeApi<T>[]) => void;
|
||||
onScroll?: (props: ListOnScrollProps) => void;
|
||||
onToggle?: (id: string) => void;
|
||||
onFocus?: (node: NodeApi<T>) => void;
|
||||
|
||||
/* Selection */
|
||||
selection?: string;
|
||||
|
||||
/* Open State */
|
||||
initialOpenState?: OpenMap;
|
||||
|
||||
/* Search */
|
||||
searchTerm?: string;
|
||||
searchMatch?: (node: NodeApi<T>, searchTerm: string) => boolean;
|
||||
|
||||
/* Extra */
|
||||
className?: string | undefined;
|
||||
rowClassName?: string | undefined;
|
||||
|
||||
/* Tree container mouse events — fire for clicks / right-clicks anywhere in
|
||||
the tree (see "Handling Clicks on the Tree Container" below). */
|
||||
onClick?: MouseEventHandler;
|
||||
onContextMenu?: MouseEventHandler;
|
||||
|
||||
dndRootElement?: globalThis.Node | null;
|
||||
dndBackend?: Extract<
|
||||
DndProviderProps<unknown, unknown>,
|
||||
{ backend: unknown }
|
||||
>["backend"];
|
||||
dndManager?: ReturnType<typeof useDragDropManager>;
|
||||
dragType?: string | ((node: NodeApi<T>) => string);
|
||||
}
|
||||
```
|
||||
|
||||
### Handling Clicks on the Tree Container
|
||||
|
||||
`onClick` and `onContextMenu` are attached to the tree's outer container, so they fire for mouse events anywhere in the tree — clicks that bubble up from a row, as well as clicks on the empty space below the last row. Use them for interactions that aren't tied to a single node, such as clearing the selection or opening your own context menu for the tree as a whole.
|
||||
|
||||
To act only when the empty area was clicked (and not a row), check whether the event started inside a row — rows carry `role="treeitem"`:
|
||||
|
||||
```tsx
|
||||
<Tree
|
||||
data={data}
|
||||
onClick={(e) => {
|
||||
const onRow = (e.target as Element).closest('[role="treeitem"]');
|
||||
if (!onRow) {
|
||||
// clicked the empty area, not a row
|
||||
}
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault(); // suppress the native menu
|
||||
// open a custom context menu for the tree
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
Clicking empty space already clears the selection by default; pass the [`disableDeselectOnClick`](#tree-component-props) prop to keep the selection instead.
|
||||
|
||||
### Dragging Nodes to External Drop Targets
|
||||
|
||||
The react-dnd drag item created for each row carries the dragged node's `data`, so a drop target rendered outside the tree can read it:
|
||||
|
||||
```tsx
|
||||
const [, drop] = useDrop(() => ({
|
||||
accept: "NODE",
|
||||
drop: (item) => console.log(item.data), // the dragged node's data
|
||||
}));
|
||||
```
|
||||
|
||||
The tree and your external target must share one react-dnd backend. Wrap both in a single `DndProvider` and pass its manager to the tree via the `dndManager` prop. By default rows advertise the `"NODE"` item type; set the `dragType` prop (a fixed string, or a function of the node) to advertise a custom type instead. Note that the tree's own drop targets only accept `"NODE"`, so a row given a custom `dragType` is no longer reorderable within the tree.
|
||||
|
||||
## Row Component Props
|
||||
|
||||
The _\<RowRenderer\>_ is responsible for attaching the drop ref, the row style (top, height) and the aria-attributes. The default should work fine for most use cases, but it can be replaced by your own component if you need. See the _renderRow_ prop in the _\<Tree\>_ component.
|
||||
|
||||
```ts
|
||||
type RowRendererProps<T> = {
|
||||
node: NodeApi<T>;
|
||||
innerRef: (el: HTMLDivElement | null) => void;
|
||||
attrs: HTMLAttributes<any>;
|
||||
children: ReactElement;
|
||||
};
|
||||
```
|
||||
|
||||
## Node Component Props
|
||||
|
||||
The _\<NodeRenderer\>_ is responsible for attaching the drag ref, the node style (padding for indentation), the visual look of the node, the edit input of the node, and anything else you can dream up.
|
||||
|
||||
There is a default renderer, but it's only there as a placeholder to get started. You'll want to create your own component for this. It is passed as the _\<Tree\>_ components only child.
|
||||
|
||||
```ts
|
||||
export type NodeRendererProps<T> = {
|
||||
style: CSSProperties;
|
||||
node: NodeApi<T>;
|
||||
tree: TreeApi<T>;
|
||||
dragHandle?: (el: HTMLDivElement | null) => void;
|
||||
preview?: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
## DragPreview Component Props
|
||||
|
||||
The _\<DragPreview\>_ is responsible for showing a "ghost" version of the node being dragged. The default is a semi-transparent version of the NodeRenderer and should work fine for most people. To customize it, pass your new component to the _renderDragPreview_ prop.
|
||||
|
||||
```ts
|
||||
type DragPreviewProps = {
|
||||
offset: XYCoord | null;
|
||||
mouse: XYCoord | null;
|
||||
id: string | null;
|
||||
dragIds: string[];
|
||||
isDragging: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
## Cursor Component Props
|
||||
|
||||
The _\<Cursor\>_ is responsible for showing a line that indicates where the node will move to when it's dropped. The default is a blue line with circle on the left side. You may want to customize this. Pass your own component to the _renderCursor_ prop.
|
||||
|
||||
```ts
|
||||
export type CursorProps = {
|
||||
top: number;
|
||||
left: number;
|
||||
indent: number;
|
||||
};
|
||||
```
|
||||
|
||||
## Node API Reference
|
||||
|
||||
### State Properties
|
||||
|
||||
All these properties on the node instance return booleans related to the state of the node.
|
||||
|
||||
_node_.**isRoot**
|
||||
|
||||
Returns true if this is the root node. The root node is added internally by react-arborist and not shown in the UI.
|
||||
|
||||
_node_.**isLeaf**
|
||||
|
||||
Returns true if the children property is not an array.
|
||||
|
||||
_node_.**isInternal**
|
||||
|
||||
Returns true if the children property is an array.
|
||||
|
||||
_node_.**isOpen**
|
||||
|
||||
Returns true if node is internal and in an open state.
|
||||
|
||||
_node_.**isEditing**
|
||||
|
||||
Returns true if this node is currently being edited. Use this property in the NodeRenderer to render the rename form.
|
||||
|
||||
_node_.**isSelected**
|
||||
|
||||
Returns true if node is selected.
|
||||
|
||||
_node_.**isSelectedStart**
|
||||
|
||||
Returns true if node is the first of a contiguous group of selected nodes. Useful for styling.
|
||||
|
||||
_node_.**isSelectedEnd**
|
||||
|
||||
Returns true if node is the last of a contiguous group of selected nodes. Useful for styling.
|
||||
|
||||
_node_.**isOnlySelection**
|
||||
|
||||
Returns true if node is the only node selected in the tree.
|
||||
|
||||
_node_.**isFocused**
|
||||
|
||||
Returns true if node is focused.
|
||||
|
||||
_node_.**isDragging**
|
||||
|
||||
Returns true if node is being dragged.
|
||||
|
||||
_node_.**willReceiveDrop**
|
||||
|
||||
Returns true if node is internal and the user is hovering a dragged node over it.
|
||||
|
||||
_node_.**state**
|
||||
|
||||
Returns an object with all the above properties as keys and boolean values. Useful for adding class names to an element with a library like [clsx](https://github.com/lukeed/clsx) or [classnames](https://github.com/JedWatson/classnames).
|
||||
|
||||
```ts
|
||||
type NodeState = {
|
||||
isEditing: boolean;
|
||||
isDragging: boolean;
|
||||
isSelected: boolean;
|
||||
isSelectedStart: boolean;
|
||||
isSelectedEnd: boolean;
|
||||
isFocused: boolean;
|
||||
isOpen: boolean;
|
||||
isClosed: boolean;
|
||||
isLeaf: boolean;
|
||||
isInternal: boolean;
|
||||
willReceiveDrop: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
### Accessors
|
||||
|
||||
_node_.**childIndex**
|
||||
|
||||
Returns the node's index in relation to its siblings.
|
||||
|
||||
_node_.**next**
|
||||
|
||||
Returns the next visible node. The node directly under this node in the tree component. Returns null if none exist.
|
||||
|
||||
_node_.**prev**
|
||||
|
||||
Returns the previous visible node. The node directly above this node in the tree component. Returns null if none exist.
|
||||
|
||||
_node_.**nextSibling**
|
||||
|
||||
Returns the next sibling in the data of this node. Returns null if none exist.
|
||||
|
||||
### Selection Methods
|
||||
|
||||
_node_.**select**()
|
||||
|
||||
Select only this node.
|
||||
|
||||
_node_.**deselect**()
|
||||
|
||||
Deselect this node. Other nodes may still be selected.
|
||||
|
||||
_node_.**selectMulti**()
|
||||
|
||||
Select this node while maintaining all other selections.
|
||||
|
||||
_node_.**selectContiguous**()
|
||||
|
||||
Deselect all nodes from the anchor node to the last selected node, the select all nodes from the anchor node to this node. The anchor changes to the focused node after calling _select()_ or _selectMulti()_.
|
||||
|
||||
### Activation Methods
|
||||
|
||||
_node_.**activate**()
|
||||
|
||||
Runs the Tree props' onActivate callback passing in this node.
|
||||
|
||||
_node_.**focus**()
|
||||
|
||||
Focus this node.
|
||||
|
||||
### Open/Close Methods
|
||||
|
||||
_node_.**open**()
|
||||
|
||||
Opens the node if it is an internal node.
|
||||
|
||||
_node_.**close**()
|
||||
|
||||
Closes the node if it is an internal node.
|
||||
|
||||
_node_.**toggle**()
|
||||
|
||||
Toggles the open/closed state of the node if it is an internal node.
|
||||
|
||||
_node_.**openParents**()
|
||||
|
||||
Opens all the parents of this node.
|
||||
|
||||
_node_.**edit**()
|
||||
|
||||
Moves this node into the editing state. Calling node._isEditing_ will return true.
|
||||
|
||||
_node_.**submit**(_newName_)
|
||||
|
||||
Submits _newName_ string to the _onRename_ handler. Moves this node out of the editing state.
|
||||
|
||||
_node_.**reset**()
|
||||
|
||||
Moves this node out of the editing state without submitting a new name.
|
||||
|
||||
### Event Handlers
|
||||
|
||||
_node_.**handleClick**(_event_)
|
||||
|
||||
Useful for using the standard selection methods when a node is clicked. If the meta key is down, call _multiSelect()_. If the shift key is down, call _selectContiguous()_. Otherwise, call _select()_ and _activate()_.
|
||||
|
||||
## Tree API Reference
|
||||
|
||||
The tree api reference is stable across re-renders. It always has the most recent state and props.
|
||||
|
||||
### Node Accessors
|
||||
|
||||
_tree_.**get**(_id_) : _NodeApi | null_
|
||||
|
||||
Get node by id from the _visibleNodes_ array.
|
||||
|
||||
_tree_.**at**(_index_) : _NodeApi | null_
|
||||
|
||||
Get node by index from the _visibleNodes_ array.
|
||||
|
||||
_tree_.**visibleNodes** : _NodeApi[]_
|
||||
|
||||
Returns an array of the visible nodes.
|
||||
|
||||
_tree_.**firstNode** : _NodeApi | null_
|
||||
|
||||
The first node in the _visibleNodes_ array.
|
||||
|
||||
_tree_.**lastNode** : _NodeApi | null_
|
||||
|
||||
The last node in the _visibleNodes_ array.
|
||||
|
||||
_tree_.**focusedNode** : _NodeApi | null_
|
||||
|
||||
The currently focused node.
|
||||
|
||||
_tree_.**mostRecentNode** : _NodeApi | null_
|
||||
|
||||
The most recently selected node.
|
||||
|
||||
_tree_.**nextNode** : _NodeApi | null_
|
||||
|
||||
The node directly after the _focusedNode_ in the _visibleNodes_ array.
|
||||
|
||||
_tree_.**prevNode** : _NodeApi | null_
|
||||
|
||||
The node directly before the _focusedNode_ in the _visibleNodes_ array.
|
||||
|
||||
### Focus Methods
|
||||
|
||||
_tree_.**hasFocus** : _boolean_
|
||||
|
||||
Returns true if the the tree has focus somewhere within it.
|
||||
|
||||
_tree_.**focus**(_id_)
|
||||
|
||||
Focus on the node with _id_.
|
||||
|
||||
_tree_.**isFocused**(_id_) : _boolean_
|
||||
|
||||
Check if the node with _id_ is focused.
|
||||
|
||||
_tree_.**pageUp**()
|
||||
|
||||
Move focus up one page.
|
||||
|
||||
_tree_.**pageDown**()
|
||||
|
||||
Move focus down one page.
|
||||
|
||||
### Selection Methods
|
||||
|
||||
_tree_.**selectedIds** : _Set\<string\>_
|
||||
|
||||
Returns a set of ids that are selected.
|
||||
|
||||
_tree_.**selectedNodes** : _NodeApi[]_
|
||||
|
||||
Returns an array of nodes that are selected.
|
||||
|
||||
_tree_.**hasNoSelection** : boolean
|
||||
|
||||
Returns true if nothing is selected in the tree.
|
||||
|
||||
_tree_.**hasSingleSelection** : boolean
|
||||
|
||||
Returns true if there is only one selection.
|
||||
|
||||
_tree_.**hasMultipleSelections** : boolean
|
||||
|
||||
Returns true if there is more than one selection.
|
||||
|
||||
_tree_.**isSelected**(_id_) : _boolean_
|
||||
|
||||
Returns true if the node with _id_ is selected.
|
||||
|
||||
_tree_.**select**(_id_, _[opts]_)
|
||||
|
||||
Select only the node with _id_. Accepts an optional options object: `{ align?: "auto" | "smart" | "center" | "end" | "start"; focus?: boolean }`. `align` is forwarded to the scroll behavior; passing `focus: false` suppresses the focus change and the `onFocus` callback.
|
||||
|
||||
_tree_.**deselect**(_id_)
|
||||
|
||||
Deselect the node with _id_.
|
||||
|
||||
_tree_.**selectMulti**(_id_, _[opts]_)
|
||||
|
||||
Add to the selection the node with _id_. Accepts the same options object as `select`.
|
||||
|
||||
_tree_.**selectContiguous**(_id_)
|
||||
|
||||
Deselected nodes between the anchor and the last selected node, then select the nodes between the anchor and the node with _id_.
|
||||
|
||||
_tree_.**deselectAll**()
|
||||
|
||||
Deselect all nodes.
|
||||
|
||||
_tree_.**selectAll**()
|
||||
|
||||
Select all nodes.
|
||||
|
||||
### Visibility
|
||||
|
||||
_tree_.**open**(_id_)
|
||||
|
||||
Open the node with _id_.
|
||||
|
||||
_tree_.**close**(_id_)
|
||||
|
||||
Close the node with _id_.
|
||||
|
||||
_tree_.**toggle**(_id_)
|
||||
|
||||
Toggle the open state of the node with _id_.
|
||||
|
||||
_tree_.**openParents**(_id_)
|
||||
|
||||
Open all parents of the node with _id_.
|
||||
|
||||
_tree_.**openSiblings**(_id_)
|
||||
|
||||
Open all siblings of the node with _id_.
|
||||
|
||||
_tree_.**openAll**()
|
||||
|
||||
Open all internal nodes.
|
||||
|
||||
_tree_.**closeAll**()
|
||||
|
||||
Close all internal nodes.
|
||||
|
||||
_tree_.**isOpen**(_id_) : _boolean_
|
||||
|
||||
Returns true if the node with _id_ is open.
|
||||
|
||||
### Drag and Drop
|
||||
|
||||
_tree_.**isDragging**(_id_) : _boolean_
|
||||
|
||||
Returns true if the node with _id_ is being dragged.
|
||||
|
||||
_tree_.**willReceiveDrop**(_id_) : _boolean_
|
||||
|
||||
Returns true if the node with _id_ is internal and is under the dragged node.
|
||||
|
||||
### Scrolling
|
||||
|
||||
_tree_.**scrollTo**(_id_, _[align]_)
|
||||
|
||||
Scroll to the node with _id_. If this node is not visible, this method will open all its parents. The align argument can be _"auto" | "smart" | "center" | "end" | "start"_.
|
||||
|
||||
_tree_.**scrollToOffset**(_offset_)
|
||||
|
||||
Scroll the list vertically to an exact pixel _offset_ from the top — the offset-based counterpart to _scrollTo_, useful for saving and restoring a scroll position. Negative or non-finite values are clamped to the top, and react-window clamps the upper bound to the scrollable range.
|
||||
|
||||
_tree_.**scrollOffset** : _number_
|
||||
|
||||
Returns the list's current vertical scroll offset, in pixels from the top. Pairs with _scrollToOffset_ to persist and restore the scroll position.
|
||||
|
||||
### Properties
|
||||
|
||||
_tree_.**isEditing** : _boolean_
|
||||
|
||||
Returns true if the tree is editing a node.
|
||||
|
||||
_tree_.**isFiltered** : _boolean_
|
||||
|
||||
Returns true if the _searchTerm_ prop is not an empty string when trimmed.
|
||||
|
||||
_tree_.**filteredCount** : _number_
|
||||
|
||||
Returns how many nodes match the current _searchTerm_, counted across the whole tree regardless of which folders are open. Returns 0 when there is no active search. Useful for rendering a match count or a "no results" message (`isFiltered && filteredCount === 0`). Ancestors that appear in the filtered list only to keep the tree's structure intact are not counted. Matches are determined by the same predicate as filtering, so a _searchMatch_ prop changes what counts.
|
||||
|
||||
_tree_.**props** : _TreeProps_
|
||||
|
||||
Returns all the props that were passed to the _\<Tree\>_ component.
|
||||
|
||||
_tree_.**root** : _NodeApi_
|
||||
|
||||
Returns the root _NodeApi_ instance. Its children are the Node representations of the _data_ prop array.
|
||||
|
||||
## Utilities
|
||||
|
||||
### getTreeLinePrefix
|
||||
|
||||
Generates a tree-line prefix string (using Unix `tree`-style box-drawing characters like `├`, `└`, `│`) for a given node. Useful when you want ASCII/Unicode connector lines in a custom node renderer.
|
||||
|
||||
```ts
|
||||
function getTreeLinePrefix(
|
||||
node: NodeApi<any>,
|
||||
chars?: Partial<TreeLineChars>
|
||||
): string;
|
||||
|
||||
type TreeLineChars = {
|
||||
last: string; // default: "└ "
|
||||
middle: string; // default: "├ "
|
||||
pipe: string; // default: "│ "
|
||||
blank: string; // default: "\u3000 "
|
||||
};
|
||||
```
|
||||
|
||||
Wrap the prefix in a monospace span so the connectors line up:
|
||||
|
||||
```tsx
|
||||
import { Tree, getTreeLinePrefix } from "react-arborist";
|
||||
|
||||
function Node({ node, style }) {
|
||||
return (
|
||||
<div style={style}>
|
||||
<span style={{ fontFamily: "monospace" }}>{getTreeLinePrefix(node)}</span>
|
||||
{node.data.name}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Pass a partial `chars` object to override any of the default characters (e.g. for an ASCII-only style).
|
||||
|
||||
## Contributing
|
||||
|
||||
The package ships with Jest tests under `modules/react-arborist/src/**/*.test.ts?(x)`.
|
||||
From the package directory you can run:
|
||||
|
||||
```bash
|
||||
cd modules/react-arborist
|
||||
yarn test
|
||||
```
|
||||
|
||||
Good starting points:
|
||||
|
||||
- `src/interfaces/tree-api.test.ts` for pure API behavior
|
||||
- `src/components/provider.test.tsx` for rendered tree behavior
|
||||
- `src/dnd/drag-hook.test.ts` for drag-and-drop behavior
|
||||
|
||||
## Author
|
||||
|
||||
[James Kerr](https://twitter.com/specialCaseDev) at [Brim Data](https://brimdata.io) for the [Zui desktop app](https://www.youtube.com/watch?v=I2y663n8d2A).
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare function Cursor(): import("react/jsx-runtime").JSX.Element | null;
|
||||
Generated
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Cursor = Cursor;
|
||||
const jsx_runtime_1 = require("react/jsx-runtime");
|
||||
const context_1 = require("../context");
|
||||
function Cursor() {
|
||||
var _a, _b;
|
||||
const tree = (0, context_1.useTreeApi)();
|
||||
const state = (0, context_1.useDndContext)();
|
||||
const cursor = state.cursor;
|
||||
if (!cursor || cursor.type !== "line")
|
||||
return null;
|
||||
const indent = tree.indent;
|
||||
const top = tree.rowTopPosition(cursor.index) + ((_b = (_a = tree.props.padding) !== null && _a !== void 0 ? _a : tree.props.paddingTop) !== null && _b !== void 0 ? _b : 0);
|
||||
const left = indent * cursor.level;
|
||||
const Cursor = tree.renderCursor;
|
||||
return (0, jsx_runtime_1.jsx)(Cursor, { top, left, indent });
|
||||
}
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* All these keyboard shortcuts seem like they should be configurable.
|
||||
* Each operation should be a given a name and separated from
|
||||
* the event handler. Future clean up welcome.
|
||||
*/
|
||||
export declare function DefaultContainer(): import("react/jsx-runtime").JSX.Element;
|
||||
Generated
Vendored
+278
@@ -0,0 +1,278 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.DefaultContainer = DefaultContainer;
|
||||
const jsx_runtime_1 = require("react/jsx-runtime");
|
||||
const react_window_1 = require("react-window");
|
||||
const context_1 = require("../context");
|
||||
const utils_1 = require("../utils");
|
||||
const list_outer_element_1 = require("./list-outer-element");
|
||||
const list_inner_element_1 = require("./list-inner-element");
|
||||
const row_container_1 = require("./row-container");
|
||||
let focusSearchTerm = "";
|
||||
let timeoutId = null;
|
||||
/**
|
||||
* All these keyboard shortcuts seem like they should be configurable.
|
||||
* Each operation should be a given a name and separated from
|
||||
* the event handler. Future clean up welcome.
|
||||
*/
|
||||
function DefaultContainer() {
|
||||
(0, context_1.useDataUpdates)();
|
||||
const tree = (0, context_1.useTreeApi)();
|
||||
return ((0, jsx_runtime_1.jsx)("div", { role: "tree", "aria-label": tree.props["aria-label"], "aria-labelledby": tree.props["aria-labelledby"], "aria-multiselectable": !tree.props.disableMultiSelection || undefined, style: {
|
||||
height: tree.height,
|
||||
width: tree.width,
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
}, onContextMenu: tree.props.onContextMenu, onClick: tree.props.onClick, tabIndex: 0, onFocus: (e) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget)) {
|
||||
tree.onFocus();
|
||||
}
|
||||
}, onBlur: (e) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget)) {
|
||||
tree.onBlur();
|
||||
}
|
||||
}, onKeyDown: (e) => {
|
||||
var _a;
|
||||
if (tree.isEditing) {
|
||||
return;
|
||||
}
|
||||
// Don't hijack keystrokes typed into a nested form field or editable
|
||||
// element (e.g. an <input> inside a modal rendered within the tree).
|
||||
// Their events bubble up to this handler, where shortcuts like Space
|
||||
// would otherwise call preventDefault and swallow the character. Rows
|
||||
// are plain divs, so navigation is unaffected. (#257)
|
||||
const target = e.target;
|
||||
if (target instanceof Element &&
|
||||
target !== e.currentTarget &&
|
||||
target.closest("input, textarea, select, [contenteditable]:not([contenteditable='false'])")) {
|
||||
return;
|
||||
}
|
||||
if (e.key === "Backspace") {
|
||||
if (!tree.props.onDelete)
|
||||
return;
|
||||
const ids = Array.from(tree.selectedIds);
|
||||
if (ids.length > 1) {
|
||||
let nextFocus = tree.mostRecentNode;
|
||||
while (nextFocus && nextFocus.isSelected) {
|
||||
nextFocus = nextFocus.nextSibling;
|
||||
}
|
||||
if (!nextFocus)
|
||||
nextFocus = tree.lastNode;
|
||||
tree.focus(nextFocus, { scroll: false });
|
||||
tree.delete(Array.from(ids));
|
||||
}
|
||||
else {
|
||||
const node = tree.focusedNode;
|
||||
if (node) {
|
||||
const sib = node.nextSibling;
|
||||
const parent = node.parent;
|
||||
tree.focus(sib || parent, { scroll: false });
|
||||
tree.delete(node);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key === "Tab" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
(0, utils_1.focusNextElement)(e.currentTarget);
|
||||
return;
|
||||
}
|
||||
if (e.key === "Tab" && e.shiftKey) {
|
||||
e.preventDefault();
|
||||
(0, utils_1.focusPrevElement)(e.currentTarget);
|
||||
return;
|
||||
}
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
const next = tree.nextNode;
|
||||
if (e.metaKey) {
|
||||
tree.select(tree.focusedNode);
|
||||
tree.activate(tree.focusedNode);
|
||||
return;
|
||||
}
|
||||
else if (!e.shiftKey || tree.props.disableMultiSelection) {
|
||||
tree.focus(next);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (!next)
|
||||
return;
|
||||
const current = tree.focusedNode;
|
||||
if (!current) {
|
||||
tree.focus(tree.firstNode);
|
||||
}
|
||||
else if (current.isSelected) {
|
||||
tree.selectContiguous(next);
|
||||
}
|
||||
else {
|
||||
tree.selectMulti(next);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
const prev = tree.prevNode;
|
||||
if (!e.shiftKey || tree.props.disableMultiSelection) {
|
||||
tree.focus(prev);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
if (!prev)
|
||||
return;
|
||||
const current = tree.focusedNode;
|
||||
if (!current) {
|
||||
tree.focus(tree.lastNode); // ?
|
||||
}
|
||||
else if (current.isSelected) {
|
||||
tree.selectContiguous(prev);
|
||||
}
|
||||
else {
|
||||
tree.selectMulti(prev);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (e.key === "ArrowRight") {
|
||||
const node = tree.focusedNode;
|
||||
if (!node)
|
||||
return;
|
||||
if (node.isInternal && node.isOpen) {
|
||||
tree.focus(tree.nextNode);
|
||||
}
|
||||
else if (node.isInternal)
|
||||
tree.open(node.id);
|
||||
return;
|
||||
}
|
||||
if (e.key === "ArrowLeft") {
|
||||
const node = tree.focusedNode;
|
||||
if (!node || node.isRoot)
|
||||
return;
|
||||
if (node.isInternal && node.isOpen)
|
||||
tree.close(node.id);
|
||||
else if (!((_a = node.parent) === null || _a === void 0 ? void 0 : _a.isRoot)) {
|
||||
tree.focus(node.parent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key === "a" && (e.metaKey || e.ctrlKey) && !tree.props.disableMultiSelection) {
|
||||
e.preventDefault();
|
||||
tree.selectAll();
|
||||
return;
|
||||
}
|
||||
if (e.key === "a" && !e.metaKey && !e.ctrlKey && tree.props.onCreate) {
|
||||
tree.createLeaf();
|
||||
return;
|
||||
}
|
||||
if (e.key === "A" && !e.metaKey && !e.ctrlKey) {
|
||||
if (!tree.props.onCreate)
|
||||
return;
|
||||
tree.createInternal();
|
||||
return;
|
||||
}
|
||||
if (e.key === "Home") {
|
||||
// add shift keys
|
||||
e.preventDefault();
|
||||
tree.focus(tree.firstNode);
|
||||
return;
|
||||
}
|
||||
if (e.key === "End") {
|
||||
// add shift keys
|
||||
e.preventDefault();
|
||||
tree.focus(tree.lastNode);
|
||||
return;
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
const node = tree.focusedNode;
|
||||
if (!node)
|
||||
return;
|
||||
if (!node.isEditable || !tree.props.onRename)
|
||||
return;
|
||||
setTimeout(() => {
|
||||
if (node)
|
||||
tree.edit(node);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (e.key === " ") {
|
||||
e.preventDefault();
|
||||
const node = tree.focusedNode;
|
||||
if (!node)
|
||||
return;
|
||||
if (node.isLeaf) {
|
||||
node.select();
|
||||
node.activate();
|
||||
}
|
||||
else {
|
||||
node.toggle();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key === "*") {
|
||||
const node = tree.focusedNode;
|
||||
if (!node)
|
||||
return;
|
||||
tree.openSiblings(node);
|
||||
return;
|
||||
}
|
||||
if (e.key === "PageUp") {
|
||||
e.preventDefault();
|
||||
tree.pageUp();
|
||||
return;
|
||||
}
|
||||
if (e.key === "PageDown") {
|
||||
e.preventDefault();
|
||||
tree.pageDown();
|
||||
}
|
||||
// If they type a sequence of characters
|
||||
// collect them. Reset them after a timeout.
|
||||
// Use it to search the tree for a node, then focus it.
|
||||
// Clean this up a bit later
|
||||
clearTimeout(timeoutId);
|
||||
focusSearchTerm += e.key;
|
||||
timeoutId = setTimeout(() => {
|
||||
focusSearchTerm = "";
|
||||
}, 600);
|
||||
const node = tree.visibleNodes.find((n) => {
|
||||
// @ts-ignore
|
||||
const name = n.data.name;
|
||||
if (typeof name === "string") {
|
||||
return name.toLowerCase().startsWith(focusSearchTerm);
|
||||
}
|
||||
else
|
||||
return false;
|
||||
});
|
||||
if (node)
|
||||
tree.focus(node.id);
|
||||
}, children: (0, jsx_runtime_1.jsx)(List, {}) }));
|
||||
}
|
||||
/**
|
||||
* Fixed-height trees (numeric rowHeight) render a FixedSizeList, preserving the
|
||||
* original O(1) layout and avoiding VariableSizeList's measurement cache. Only
|
||||
* the function form, which needs per-row heights, uses VariableSizeList.
|
||||
*/
|
||||
function List() {
|
||||
var _a, _b;
|
||||
const tree = (0, context_1.useTreeApi)();
|
||||
const commonProps = {
|
||||
className: tree.props.className,
|
||||
outerRef: tree.listEl,
|
||||
itemCount: tree.visibleNodes.length,
|
||||
height: tree.height,
|
||||
width: tree.width,
|
||||
overscanCount: tree.overscanCount,
|
||||
itemKey: (index) => { var _a; return ((_a = tree.visibleNodes[index]) === null || _a === void 0 ? void 0 : _a.id) || index; },
|
||||
outerElementType: (_a = tree.props.outerElementType) !== null && _a !== void 0 ? _a : list_outer_element_1.ListOuterElement,
|
||||
innerElementType: (_b = tree.props.innerElementType) !== null && _b !== void 0 ? _b : list_inner_element_1.ListInnerElement,
|
||||
onScroll: tree.props.onScroll,
|
||||
onItemsRendered: tree.onItemsRendered.bind(tree),
|
||||
};
|
||||
if (typeof tree.props.rowHeight === "function") {
|
||||
return (
|
||||
// @ts-ignore
|
||||
(0, jsx_runtime_1.jsx)(react_window_1.VariableSizeList, Object.assign({}, commonProps, { itemSize: tree.rowHeightAt, ref: tree.list, children: row_container_1.RowContainer })));
|
||||
}
|
||||
return (
|
||||
// @ts-ignore
|
||||
(0, jsx_runtime_1.jsx)(react_window_1.FixedSizeList, Object.assign({}, commonProps, { itemSize: tree.rowHeight, ref: tree.list, children: row_container_1.RowContainer })));
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Generated
Vendored
+134
@@ -0,0 +1,134 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const jsx_runtime_1 = require("react/jsx-runtime");
|
||||
const react_1 = require("react");
|
||||
const react_2 = require("@testing-library/react");
|
||||
const tree_1 = require("./tree");
|
||||
const data = [
|
||||
{
|
||||
id: "1",
|
||||
name: "root",
|
||||
children: [
|
||||
{ id: "2", name: "a" },
|
||||
{ id: "3", name: "b", children: [{ id: "4", name: "c" }] },
|
||||
],
|
||||
},
|
||||
];
|
||||
/* Selecting a row kicks off tree.scrollTo(), whose promise resolves on a
|
||||
microtask after fireEvent's synchronous act() scope has exited — the
|
||||
resulting List scrollToItem() update would otherwise warn about not being
|
||||
wrapped in act(). Awaiting an async act flushes that trailing update. */
|
||||
function click(el, init) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
yield (0, react_2.act)(() => __awaiter(this, void 0, void 0, function* () {
|
||||
react_2.fireEvent.click(el, init);
|
||||
}));
|
||||
});
|
||||
}
|
||||
/* #303: multi-select should respond to Ctrl+Click (Windows) as well as
|
||||
Cmd/Meta+Click (macOS). */
|
||||
test("Ctrl+Click adds a row to the selection (#303)", () => __awaiter(void 0, void 0, void 0, function* () {
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, openByDefault: true }));
|
||||
const [, a, b] = react_2.screen.getAllByRole("treeitem");
|
||||
yield click(a);
|
||||
expect(a.getAttribute("aria-selected")).toBe("true");
|
||||
yield click(b, { ctrlKey: true });
|
||||
expect(a.getAttribute("aria-selected")).toBe("true");
|
||||
expect(b.getAttribute("aria-selected")).toBe("true");
|
||||
}));
|
||||
test("Ctrl+Click toggles an already-selected row off (#303)", () => __awaiter(void 0, void 0, void 0, function* () {
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, openByDefault: true }));
|
||||
const [, a, b] = react_2.screen.getAllByRole("treeitem");
|
||||
yield click(a);
|
||||
yield click(b, { ctrlKey: true });
|
||||
yield click(b, { ctrlKey: true });
|
||||
expect(a.getAttribute("aria-selected")).toBe("true");
|
||||
expect(b.getAttribute("aria-selected")).toBe("false");
|
||||
}));
|
||||
test("Ctrl+Click falls through to a plain select when multi-select is disabled (#303)", () => __awaiter(void 0, void 0, void 0, function* () {
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, openByDefault: true, disableMultiSelection: true }));
|
||||
const [, a, b] = react_2.screen.getAllByRole("treeitem");
|
||||
yield click(a);
|
||||
yield click(b, { ctrlKey: true });
|
||||
expect(a.getAttribute("aria-selected")).toBe("false");
|
||||
expect(b.getAttribute("aria-selected")).toBe("true");
|
||||
}));
|
||||
/* #10: a row's background/selection highlight must span the full scrollable
|
||||
width, not stop at the viewport edge, when content overflows horizontally. */
|
||||
test("rows get min-width: max-content so the highlight spans overflow (#10)", () => {
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, openByDefault: true }));
|
||||
for (const row of react_2.screen.getAllByRole("treeitem")) {
|
||||
expect(row.style.minWidth).toBe("max-content");
|
||||
}
|
||||
});
|
||||
/* #325: forward an accessible name and multiselectable state onto the
|
||||
role="tree" element. */
|
||||
test("forwards aria-label to the role=tree element (#325)", () => {
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, "aria-label": "File explorer" }));
|
||||
expect(react_2.screen.getByRole("tree").getAttribute("aria-label")).toBe("File explorer");
|
||||
});
|
||||
test("forwards aria-labelledby to the role=tree element (#325)", () => {
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, "aria-labelledby": "heading-id" }));
|
||||
expect(react_2.screen.getByRole("tree").getAttribute("aria-labelledby")).toBe("heading-id");
|
||||
});
|
||||
test("marks the tree aria-multiselectable by default (#325)", () => {
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data }));
|
||||
expect(react_2.screen.getByRole("tree").getAttribute("aria-multiselectable")).toBe("true");
|
||||
});
|
||||
test("omits aria-multiselectable when multi-select is disabled (#325)", () => {
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, disableMultiSelection: true }));
|
||||
expect(react_2.screen.getByRole("tree").hasAttribute("aria-multiselectable")).toBe(false);
|
||||
});
|
||||
/* #245, #308: clicking the empty area below the rows clears the selection by
|
||||
default; disableDeselectOnClick opts out of that. The deselect handler lives
|
||||
on the list's outer (scroll) element, which is wired to tree.listEl. */
|
||||
test("clicking empty tree space clears the selection by default (#245)", () => __awaiter(void 0, void 0, void 0, function* () {
|
||||
const ref = (0, react_1.createRef)();
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, openByDefault: true, ref: ref }));
|
||||
const [, a] = react_2.screen.getAllByRole("treeitem");
|
||||
yield click(a);
|
||||
expect(a.getAttribute("aria-selected")).toBe("true");
|
||||
yield click(ref.current.listEl.current);
|
||||
expect(a.getAttribute("aria-selected")).toBe("false");
|
||||
}));
|
||||
test("disableDeselectOnClick keeps the selection when empty space is clicked (#245, #308)", () => __awaiter(void 0, void 0, void 0, function* () {
|
||||
const ref = (0, react_1.createRef)();
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, openByDefault: true, disableDeselectOnClick: true, ref: ref }));
|
||||
const [, a] = react_2.screen.getAllByRole("treeitem");
|
||||
yield click(a);
|
||||
expect(a.getAttribute("aria-selected")).toBe("true");
|
||||
yield click(ref.current.listEl.current);
|
||||
expect(a.getAttribute("aria-selected")).toBe("true");
|
||||
}));
|
||||
/* #257: an <input> rendered inside the tree (e.g. in a modal) must still
|
||||
receive Space keystrokes. The container's keydown handler calls
|
||||
preventDefault on Space to toggle/select, which would otherwise swallow the
|
||||
character as the event bubbles up from the nested input. */
|
||||
test("does not preventDefault Space typed into a nested input (#257)", () => {
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, openByDefault: true, children: () => ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("input", { "aria-label": "modal-input" }) })) }));
|
||||
const [input] = react_2.screen.getAllByLabelText("modal-input");
|
||||
const notPrevented = react_2.fireEvent.keyDown(input, { key: " ", code: "Space" });
|
||||
// fireEvent returns false when a listener called preventDefault.
|
||||
expect(notPrevented).toBe(true);
|
||||
});
|
||||
test("does not preventDefault Space typed into a nested contenteditable (#257)", () => {
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, openByDefault: true, children: () => ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("div", { "aria-label": "editable", contentEditable: true, suppressContentEditableWarning: true }) })) }));
|
||||
const [editable] = react_2.screen.getAllByLabelText("editable");
|
||||
const notPrevented = react_2.fireEvent.keyDown(editable, { key: " ", code: "Space" });
|
||||
expect(notPrevented).toBe(true);
|
||||
});
|
||||
test("still calls preventDefault on Space typed on the tree container itself (#257)", () => {
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, openByDefault: true }));
|
||||
const tree = react_2.screen.getByRole("tree");
|
||||
const notPrevented = react_2.fireEvent.keyDown(tree, { key: " ", code: "Space" });
|
||||
expect(notPrevented).toBe(false);
|
||||
});
|
||||
buildtime/client/logical-editor/node_modules/react-arborist/dist/main/components/default-cursor.d.ts
Generated
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
import React from "react";
|
||||
import { CursorProps } from "../types/renderers";
|
||||
export declare const DefaultCursor: React.NamedExoticComponent<CursorProps>;
|
||||
Generated
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.DefaultCursor = void 0;
|
||||
const jsx_runtime_1 = require("react/jsx-runtime");
|
||||
const react_1 = __importDefault(require("react"));
|
||||
const placeholderStyle = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
zIndex: 1,
|
||||
};
|
||||
const lineStyle = {
|
||||
flex: 1,
|
||||
height: "2px",
|
||||
background: "#4B91E2",
|
||||
borderRadius: "1px",
|
||||
};
|
||||
const circleStyle = {
|
||||
width: "4px",
|
||||
height: "4px",
|
||||
boxShadow: "0 0 0 3px #4B91E2",
|
||||
borderRadius: "50%",
|
||||
};
|
||||
exports.DefaultCursor = react_1.default.memo(function DefaultCursor({ top, left, indent }) {
|
||||
const style = {
|
||||
position: "absolute",
|
||||
pointerEvents: "none",
|
||||
top: top - 2 + "px",
|
||||
left: left + "px",
|
||||
right: indent + "px",
|
||||
};
|
||||
return ((0, jsx_runtime_1.jsxs)("div", { style: Object.assign(Object.assign({}, placeholderStyle), style), children: [(0, jsx_runtime_1.jsx)("div", { style: Object.assign({}, circleStyle) }), (0, jsx_runtime_1.jsx)("div", { style: Object.assign({}, lineStyle) })] }));
|
||||
});
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { DragPreviewProps } from "../types/renderers";
|
||||
export declare function DefaultDragPreview({ offset, mouse, id, dragIds, isDragging }: DragPreviewProps): import("react/jsx-runtime").JSX.Element;
|
||||
Generated
Vendored
+56
@@ -0,0 +1,56 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.DefaultDragPreview = DefaultDragPreview;
|
||||
const jsx_runtime_1 = require("react/jsx-runtime");
|
||||
const react_1 = require("react");
|
||||
const context_1 = require("../context");
|
||||
const layerStyles = {
|
||||
position: "fixed",
|
||||
pointerEvents: "none",
|
||||
zIndex: 100,
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
};
|
||||
const getStyle = (offset) => {
|
||||
if (!offset)
|
||||
return { display: "none" };
|
||||
const { x, y } = offset;
|
||||
return { transform: `translate(${x}px, ${y}px)` };
|
||||
};
|
||||
const getCountStyle = (offset) => {
|
||||
if (!offset)
|
||||
return { display: "none" };
|
||||
const { x, y } = offset;
|
||||
return { transform: `translate(${x + 10}px, ${y + 10}px)` };
|
||||
};
|
||||
function DefaultDragPreview({ offset, mouse, id, dragIds, isDragging }) {
|
||||
return ((0, jsx_runtime_1.jsxs)(Overlay, { isDragging: isDragging, children: [(0, jsx_runtime_1.jsx)(Position, { offset: offset, children: (0, jsx_runtime_1.jsx)(PreviewNode, { id: id, dragIds: dragIds }) }), (0, jsx_runtime_1.jsx)(Count, { mouse: mouse, count: dragIds.length })] }));
|
||||
}
|
||||
const Overlay = (0, react_1.memo)(function Overlay(props) {
|
||||
if (!props.isDragging)
|
||||
return null;
|
||||
return (0, jsx_runtime_1.jsx)("div", { style: layerStyles, children: props.children });
|
||||
});
|
||||
function Position(props) {
|
||||
return ((0, jsx_runtime_1.jsx)("div", { className: "row preview", style: getStyle(props.offset), children: props.children }));
|
||||
}
|
||||
function Count(props) {
|
||||
const { count, mouse } = props;
|
||||
if (count > 1)
|
||||
return ((0, jsx_runtime_1.jsx)("div", { className: "selected-count", style: getCountStyle(mouse), children: count }));
|
||||
else
|
||||
return null;
|
||||
}
|
||||
const PreviewNode = (0, react_1.memo)(function PreviewNode(props) {
|
||||
const tree = (0, context_1.useTreeApi)();
|
||||
const node = tree.get(props.id);
|
||||
if (!node)
|
||||
return null;
|
||||
return ((0, jsx_runtime_1.jsx)(tree.renderNode, { preview: true, node: node, style: {
|
||||
paddingLeft: node.level * tree.indent,
|
||||
opacity: 0.2,
|
||||
background: "transparent",
|
||||
}, tree: tree }));
|
||||
});
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { NodeRendererProps } from "../types/renderers";
|
||||
export declare function DefaultNode<T>(props: NodeRendererProps<T>): import("react/jsx-runtime").JSX.Element;
|
||||
Generated
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.DefaultNode = DefaultNode;
|
||||
const jsx_runtime_1 = require("react/jsx-runtime");
|
||||
const react_1 = require("react");
|
||||
function DefaultNode(props) {
|
||||
return ((0, jsx_runtime_1.jsxs)("div", { ref: props.dragHandle, style: props.style, children: [(0, jsx_runtime_1.jsx)("span", { onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
props.node.toggle();
|
||||
}, children: props.node.isLeaf ? "🌳" : props.node.isOpen ? "🗁" : "🗀" }), " ", props.node.isEditing ? (0, jsx_runtime_1.jsx)(Edit, Object.assign({}, props)) : (0, jsx_runtime_1.jsx)(Show, Object.assign({}, props))] }));
|
||||
}
|
||||
function Show(props) {
|
||||
return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsx)("span", { children: props.node.data.name }) }));
|
||||
}
|
||||
function Edit({ node }) {
|
||||
const input = (0, react_1.useRef)();
|
||||
(0, react_1.useEffect)(() => {
|
||||
var _a, _b;
|
||||
(_a = input.current) === null || _a === void 0 ? void 0 : _a.focus();
|
||||
(_b = input.current) === null || _b === void 0 ? void 0 : _b.select();
|
||||
}, []);
|
||||
return ((0, jsx_runtime_1.jsx)("input", { ref: input,
|
||||
// @ts-ignore
|
||||
defaultValue: node.data.name, onBlur: () => node.reset(), onKeyDown: (e) => {
|
||||
var _a;
|
||||
if (e.key === "Escape")
|
||||
node.reset();
|
||||
if (e.key === "Enter")
|
||||
node.submit(((_a = input.current) === null || _a === void 0 ? void 0 : _a.value) || "");
|
||||
} }));
|
||||
}
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { RowRendererProps } from "../types/renderers";
|
||||
export declare function DefaultRow<T>({ node, attrs, innerRef, children }: RowRendererProps<T>): import("react/jsx-runtime").JSX.Element;
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.DefaultRow = DefaultRow;
|
||||
const jsx_runtime_1 = require("react/jsx-runtime");
|
||||
function DefaultRow({ node, attrs, innerRef, children }) {
|
||||
return ((0, jsx_runtime_1.jsx)("div", Object.assign({}, attrs, { ref: innerRef, onFocus: (e) => e.stopPropagation(), onClick: node.handleClick, children: children })));
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare function DragPreviewContainer(): import("react/jsx-runtime").JSX.Element;
|
||||
Generated
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.DragPreviewContainer = DragPreviewContainer;
|
||||
const jsx_runtime_1 = require("react/jsx-runtime");
|
||||
const react_dnd_1 = require("react-dnd");
|
||||
const context_1 = require("../context");
|
||||
const default_drag_preview_1 = require("./default-drag-preview");
|
||||
function DragPreviewContainer() {
|
||||
const tree = (0, context_1.useTreeApi)();
|
||||
const { offset, mouse, item, isDragging } = (0, react_dnd_1.useDragLayer)((m) => {
|
||||
return {
|
||||
offset: m.getSourceClientOffset(),
|
||||
mouse: m.getClientOffset(),
|
||||
item: m.getItem(),
|
||||
isDragging: m.isDragging(),
|
||||
};
|
||||
});
|
||||
const DragPreview = tree.props.renderDragPreview || default_drag_preview_1.DefaultDragPreview;
|
||||
return ((0, jsx_runtime_1.jsx)(DragPreview, { offset: offset, mouse: mouse, id: (item === null || item === void 0 ? void 0 : item.id) || null, dragIds: (item === null || item === void 0 ? void 0 : item.dragIds) || [], isDragging: isDragging }));
|
||||
}
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import React from "react";
|
||||
export declare const ListInnerElement: React.ForwardRefExoticComponent<Omit<any, "ref"> & React.RefAttributes<any>>;
|
||||
Generated
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
"use strict";
|
||||
var __rest = (this && this.__rest) || function (s, e) {
|
||||
var t = {};
|
||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
||||
t[p] = s[p];
|
||||
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
||||
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
||||
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
||||
t[p[i]] = s[p[i]];
|
||||
}
|
||||
return t;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ListInnerElement = void 0;
|
||||
const jsx_runtime_1 = require("react/jsx-runtime");
|
||||
const react_1 = require("react");
|
||||
const context_1 = require("../context");
|
||||
exports.ListInnerElement = (0, react_1.forwardRef)(function InnerElement(_a, ref) {
|
||||
var _b, _c, _d, _e;
|
||||
var { style } = _a, rest = __rest(_a, ["style"]);
|
||||
const tree = (0, context_1.useTreeApi)();
|
||||
const paddingTop = (_c = (_b = tree.props.padding) !== null && _b !== void 0 ? _b : tree.props.paddingTop) !== null && _c !== void 0 ? _c : 0;
|
||||
const paddingBottom = (_e = (_d = tree.props.padding) !== null && _d !== void 0 ? _d : tree.props.paddingBottom) !== null && _e !== void 0 ? _e : 0;
|
||||
return ((0, jsx_runtime_1.jsx)("div", Object.assign({ ref: ref, style: Object.assign(Object.assign({}, style), { height: `${parseFloat(style.height) + paddingTop + paddingBottom}px` }) }, rest)));
|
||||
});
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export declare const ListOuterElement: import("react").ForwardRefExoticComponent<Omit<import("react").HTMLProps<HTMLDivElement>, "ref"> & import("react").RefAttributes<unknown>>;
|
||||
export declare const DropContainer: () => import("react/jsx-runtime").JSX.Element;
|
||||
Generated
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
"use strict";
|
||||
var __rest = (this && this.__rest) || function (s, e) {
|
||||
var t = {};
|
||||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
||||
t[p] = s[p];
|
||||
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
||||
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
||||
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
||||
t[p[i]] = s[p[i]];
|
||||
}
|
||||
return t;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.DropContainer = exports.ListOuterElement = void 0;
|
||||
const jsx_runtime_1 = require("react/jsx-runtime");
|
||||
const react_1 = require("react");
|
||||
const context_1 = require("../context");
|
||||
const cursor_1 = require("./cursor");
|
||||
exports.ListOuterElement = (0, react_1.forwardRef)(function Outer(props, ref) {
|
||||
const { children } = props, rest = __rest(props, ["children"]);
|
||||
const tree = (0, context_1.useTreeApi)();
|
||||
return ((0, jsx_runtime_1.jsxs)("div", Object.assign({
|
||||
// @ts-ignore
|
||||
ref: ref }, rest, { onClick: (e) => {
|
||||
if (tree.props.disableDeselectOnClick)
|
||||
return;
|
||||
if (e.currentTarget === e.target)
|
||||
tree.deselectAll();
|
||||
}, children: [(0, jsx_runtime_1.jsx)(exports.DropContainer, {}), children] })));
|
||||
});
|
||||
const DropContainer = () => {
|
||||
const tree = (0, context_1.useTreeApi)();
|
||||
return ((0, jsx_runtime_1.jsx)("div", { style: {
|
||||
height: tree.rowTopPosition(tree.visibleNodes.length),
|
||||
width: "100%",
|
||||
position: "absolute",
|
||||
left: "0",
|
||||
right: "0",
|
||||
}, children: (0, jsx_runtime_1.jsx)(cursor_1.Cursor, {}) }));
|
||||
};
|
||||
exports.DropContainer = DropContainer;
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import { ReactElement } from "react";
|
||||
export declare function OuterDrop(props: {
|
||||
children: ReactElement;
|
||||
}): ReactElement<any, string | import("react").JSXElementConstructor<any>>;
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.OuterDrop = OuterDrop;
|
||||
const outer_drop_hook_1 = require("../dnd/outer-drop-hook");
|
||||
function OuterDrop(props) {
|
||||
(0, outer_drop_hook_1.useOuterDrop)();
|
||||
return props.children;
|
||||
}
|
||||
Generated
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
import { ReactNode } from "react";
|
||||
import { TreeApi } from "../interfaces/tree-api";
|
||||
import { TreeProps } from "../types/tree-props";
|
||||
type Props<T> = {
|
||||
treeProps: TreeProps<T>;
|
||||
imperativeHandle: React.Ref<TreeApi<T> | undefined>;
|
||||
children: ReactNode;
|
||||
};
|
||||
export declare function TreeProvider<T>({ treeProps, imperativeHandle, children }: Props<T>): import("react/jsx-runtime").JSX.Element;
|
||||
export {};
|
||||
Generated
Vendored
+65
@@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.TreeProvider = TreeProvider;
|
||||
const jsx_runtime_1 = require("react/jsx-runtime");
|
||||
const react_1 = require("react");
|
||||
const shim_1 = require("use-sync-external-store/shim");
|
||||
const context_1 = require("../context");
|
||||
const tree_api_1 = require("../interfaces/tree-api");
|
||||
const initial_1 = require("../state/initial");
|
||||
const root_reducer_1 = require("../state/root-reducer");
|
||||
const react_dnd_html5_backend_1 = require("react-dnd-html5-backend");
|
||||
const react_dnd_1 = require("react-dnd");
|
||||
const redux_1 = require("redux");
|
||||
const open_slice_1 = require("../state/open-slice");
|
||||
const SERVER_STATE = (0, initial_1.initialState)();
|
||||
function TreeProvider({ treeProps, imperativeHandle, children }) {
|
||||
const list = (0, react_1.useRef)(null);
|
||||
const listEl = (0, react_1.useRef)(null);
|
||||
const store = (0, react_1.useRef)(
|
||||
// @ts-ignore
|
||||
(0, redux_1.createStore)(root_reducer_1.rootReducer, (0, initial_1.initialState)(treeProps)));
|
||||
const state = (0, shim_1.useSyncExternalStore)(store.current.subscribe, store.current.getState, () => SERVER_STATE);
|
||||
/* The tree api object is stable. */
|
||||
const api = (0, react_1.useMemo)(() => {
|
||||
return new tree_api_1.TreeApi(store.current, treeProps, list, listEl);
|
||||
}, []);
|
||||
/* Make sure the tree instance stays in sync */
|
||||
const updateCount = (0, react_1.useRef)(0);
|
||||
(0, react_1.useMemo)(() => {
|
||||
updateCount.current += 1;
|
||||
api.update(treeProps);
|
||||
}, Object.values(treeProps));
|
||||
/* Rebuild visible nodes when open state changes, without clobbering
|
||||
props set imperatively via api.update(). Bumping updateCount keeps
|
||||
DataUpdates consumers (e.g. DefaultContainer) in sync. */
|
||||
(0, react_1.useMemo)(() => {
|
||||
updateCount.current += 1;
|
||||
api.update(api.props);
|
||||
}, [state.nodes.open]);
|
||||
/* Expose the tree api */
|
||||
(0, react_1.useImperativeHandle)(imperativeHandle, () => api);
|
||||
/* Change selection based on props */
|
||||
(0, react_1.useEffect)(() => {
|
||||
if (api.props.selection) {
|
||||
api.select(api.props.selection, { focus: false });
|
||||
}
|
||||
else {
|
||||
api.deselectAll();
|
||||
}
|
||||
}, [api.props.selection]);
|
||||
/* Clear visability for filtered nodes */
|
||||
(0, react_1.useEffect)(() => {
|
||||
if (!api.props.searchTerm) {
|
||||
store.current.dispatch(open_slice_1.actions.clear(true));
|
||||
}
|
||||
}, [api.props.searchTerm]);
|
||||
return ((0, jsx_runtime_1.jsx)(context_1.TreeApiContext.Provider, { value: api, children: (0, jsx_runtime_1.jsx)(context_1.DataUpdatesContext.Provider, { value: updateCount.current, children: (0, jsx_runtime_1.jsx)(context_1.NodesContext.Provider, { value: state.nodes, children: (0, jsx_runtime_1.jsx)(context_1.DndContext.Provider, { value: state.dnd, children: (0, jsx_runtime_1.jsx)(react_dnd_1.DndProvider, Object.assign({}, (treeProps.dndManager
|
||||
? { manager: treeProps.dndManager }
|
||||
: {
|
||||
backend: treeProps.dndBackend || react_dnd_html5_backend_1.HTML5Backend,
|
||||
options: {
|
||||
rootElement: api.props.dndRootElement || undefined,
|
||||
},
|
||||
}), { children: children })) }) }) }) }));
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Generated
Vendored
+103
@@ -0,0 +1,103 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const jsx_runtime_1 = require("react/jsx-runtime");
|
||||
const react_1 = require("react");
|
||||
const react_2 = require("@testing-library/react");
|
||||
const react_window_1 = require("react-window");
|
||||
const tree_1 = require("./tree");
|
||||
const data = [
|
||||
{
|
||||
id: "1",
|
||||
name: "root",
|
||||
children: [
|
||||
{ id: "2", name: "a" },
|
||||
{ id: "3", name: "b", children: [{ id: "4", name: "c" }] },
|
||||
],
|
||||
},
|
||||
];
|
||||
test("imperative tree.update() props survive node toggles (#228)", () => {
|
||||
const ref = (0, react_1.createRef)();
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, ref: ref, rowHeight: 24, openByDefault: false }));
|
||||
const api = ref.current;
|
||||
expect(api.rowHeight).toBe(24);
|
||||
(0, react_2.act)(() => {
|
||||
api.update(Object.assign(Object.assign({}, api.props), { rowHeight: 48 }));
|
||||
});
|
||||
expect(api.rowHeight).toBe(48);
|
||||
/* Opening a node dispatches a redux action that changes state.nodes.open.
|
||||
Before #337, the open-state effect re-ran api.update(treeProps), reverting
|
||||
rowHeight to 24. */
|
||||
(0, react_2.act)(() => {
|
||||
api.open("1");
|
||||
});
|
||||
expect(api.rowHeight).toBe(48);
|
||||
});
|
||||
/* Backwards compatibility: switching FixedSizeList -> VariableSizeList must not
|
||||
change layout for a numeric rowHeight. With openByDefault, all four nodes
|
||||
(1 > 2, 3 > 4) are visible in DFS order. */
|
||||
test("numeric rowHeight positions rows at index * height (#238 back-compat)", () => {
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, rowHeight: 24, openByDefault: true }));
|
||||
const rows = react_2.screen.getAllByRole("treeitem");
|
||||
expect(rows).toHaveLength(4);
|
||||
rows.forEach((row, i) => {
|
||||
expect(row.style.height).toBe("24px");
|
||||
expect(row.style.top).toBe(`${i * 24}px`);
|
||||
});
|
||||
});
|
||||
test("function rowHeight gives each row its own height and cumulative top (#238)", () => {
|
||||
const heights = { "1": 40, "2": 20, "3": 30, "4": 10 };
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, rowHeight: (node) => heights[node.id], openByDefault: true }));
|
||||
const rows = react_2.screen.getAllByRole("treeitem");
|
||||
expect(rows).toHaveLength(4);
|
||||
const expected = [40, 20, 30, 10];
|
||||
let top = 0;
|
||||
rows.forEach((row, i) => {
|
||||
expect(row.style.height).toBe(`${expected[i]}px`);
|
||||
expect(row.style.top).toBe(`${top}px`);
|
||||
top += expected[i];
|
||||
});
|
||||
});
|
||||
test("mutations tell the list to recompute heights (#238)", () => {
|
||||
const ref = (0, react_1.createRef)();
|
||||
/* Only variable-height mode renders a VariableSizeList with a measurement
|
||||
cache to recompute, so use a function rowHeight here. */
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, ref: ref, rowHeight: () => 24, openByDefault: true }));
|
||||
const api = ref.current;
|
||||
const reset = jest.spyOn(api.list.current, "resetAfterIndex");
|
||||
(0, react_2.act)(() => api.close("1"));
|
||||
expect(reset).toHaveBeenCalled();
|
||||
reset.mockClear();
|
||||
(0, react_2.act)(() => api.open("1"));
|
||||
expect(reset).toHaveBeenCalled();
|
||||
});
|
||||
/* react-window caches measurements by index and never invalidates them itself.
|
||||
When data changes via props in variable-height mode, those cached sizes belong
|
||||
to the wrong rows, so update() must drop the cache. It runs during render, so
|
||||
it uses the shouldForceUpdate=false variant. */
|
||||
test("changing data in variable-height mode resets the list cache (#238)", () => {
|
||||
const ref = (0, react_1.createRef)();
|
||||
const rowHeight = (node) => (node.isInternal ? 40 : 20);
|
||||
const { rerender } = (0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, ref: ref, rowHeight: rowHeight, openByDefault: true }));
|
||||
const reset = jest.spyOn(ref.current.list.current, "resetAfterIndex");
|
||||
const nextData = [{ id: "9", name: "fresh" }, ...data];
|
||||
(0, react_2.act)(() => {
|
||||
rerender((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: nextData, ref: ref, rowHeight: rowHeight, openByDefault: true }));
|
||||
});
|
||||
expect(reset).toHaveBeenCalledWith(0, false);
|
||||
});
|
||||
/* The numeric path must stay on FixedSizeList: it has constant item sizes, so
|
||||
there is no measurement cache to go stale and none of VariableSizeList's
|
||||
overhead. A FixedSizeList has no resetAfterIndex method at all. */
|
||||
test("numeric rowHeight renders a cache-free FixedSizeList (#238)", () => {
|
||||
const ref = (0, react_1.createRef)();
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, ref: ref, rowHeight: 24, openByDefault: true }));
|
||||
const list = ref.current.list.current;
|
||||
expect(list).toBeInstanceOf(react_window_1.FixedSizeList);
|
||||
expect("resetAfterIndex" in list).toBe(false);
|
||||
});
|
||||
/* The function path uses VariableSizeList so per-row heights are possible. */
|
||||
test("function rowHeight renders a VariableSizeList (#238)", () => {
|
||||
const ref = (0, react_1.createRef)();
|
||||
(0, react_2.render)((0, jsx_runtime_1.jsx)(tree_1.Tree, { data: data, ref: ref, rowHeight: () => 24, openByDefault: true }));
|
||||
expect(ref.current.list.current).toBeInstanceOf(react_window_1.VariableSizeList);
|
||||
});
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
type Props = {
|
||||
style: React.CSSProperties;
|
||||
index: number;
|
||||
};
|
||||
export declare const RowContainer: React.MemoExoticComponent<(<T>({ index, style }: Props) => import("react/jsx-runtime").JSX.Element)>;
|
||||
export {};
|
||||
Generated
Vendored
+90
@@ -0,0 +1,90 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.RowContainer = void 0;
|
||||
const jsx_runtime_1 = require("react/jsx-runtime");
|
||||
const react_1 = __importStar(require("react"));
|
||||
const context_1 = require("../context");
|
||||
const drag_hook_1 = require("../dnd/drag-hook");
|
||||
const drop_hook_1 = require("../dnd/drop-hook");
|
||||
const use_fresh_node_1 = require("../hooks/use-fresh-node");
|
||||
exports.RowContainer = react_1.default.memo(function RowContainer({ index, style }) {
|
||||
/* When will the <Row> will re-render.
|
||||
*
|
||||
* The row component is memo'd so it will only render
|
||||
* when a new instance of the NodeApi class is passed
|
||||
* to it.
|
||||
*
|
||||
* The TreeApi instance is stable. It does not
|
||||
* change when the internal state changes.
|
||||
*
|
||||
* The TreeApi has all the references to the nodes.
|
||||
* We need to clone the nodes when their state
|
||||
* changes. The node class contains no state itself,
|
||||
* It always checks the tree for state. The tree's
|
||||
* state will always be up to date.
|
||||
*/
|
||||
(0, context_1.useDataUpdates)(); // Re-render when tree props or visability changes
|
||||
const _ = (0, context_1.useNodesContext)(); // So that we re-render appropriately
|
||||
const tree = (0, context_1.useTreeApi)(); // Tree already has the fresh state
|
||||
const node = (0, use_fresh_node_1.useFreshNode)(index);
|
||||
const el = (0, react_1.useRef)(null);
|
||||
const dragRef = (0, drag_hook_1.useDragHook)(node);
|
||||
const dropRef = (0, drop_hook_1.useDropHook)(el, node);
|
||||
const innerRef = (0, react_1.useCallback)((n) => {
|
||||
el.current = n;
|
||||
dropRef(n);
|
||||
}, [dropRef]);
|
||||
const indent = tree.indent * node.level;
|
||||
const nodeStyle = (0, react_1.useMemo)(() => ({ paddingLeft: indent }), [indent]);
|
||||
const rowStyle = (0, react_1.useMemo)(() => {
|
||||
var _a, _b;
|
||||
return (Object.assign(Object.assign({}, style), { top: parseFloat(style.top) + ((_b = (_a = tree.props.padding) !== null && _a !== void 0 ? _a : tree.props.paddingTop) !== null && _b !== void 0 ? _b : 0),
|
||||
// react-window gives the row width: 100% of the viewport. When a deeply
|
||||
// nested (or long) node overflows horizontally, that clips the row's
|
||||
// background/selection highlight at the viewport edge. min-width:
|
||||
// max-content lets the row grow with its content so the highlight spans
|
||||
// the full scrollable width (#10).
|
||||
minWidth: "max-content" }));
|
||||
}, [style, tree.props.padding, tree.props.paddingTop]);
|
||||
const rowAttrs = {
|
||||
role: "treeitem",
|
||||
"aria-level": node.level + 1,
|
||||
"aria-selected": node.isSelected,
|
||||
"aria-expanded": node.isOpen,
|
||||
style: rowStyle,
|
||||
tabIndex: -1,
|
||||
className: tree.props.rowClassName,
|
||||
};
|
||||
(0, react_1.useEffect)(() => {
|
||||
var _a;
|
||||
if (!node.isEditing && node.isFocused) {
|
||||
(_a = el.current) === null || _a === void 0 ? void 0 : _a.focus({ preventScroll: true });
|
||||
}
|
||||
}, [node.isEditing, node.isFocused, el.current]);
|
||||
const Node = tree.renderNode;
|
||||
const Row = tree.renderRow;
|
||||
return ((0, jsx_runtime_1.jsx)(Row, { node: node, innerRef: innerRef, attrs: rowAttrs, children: (0, jsx_runtime_1.jsx)(Node, { node: node, tree: tree, style: nodeStyle, dragHandle: dragRef }) }));
|
||||
});
|
||||
buildtime/client/logical-editor/node_modules/react-arborist/dist/main/components/tree-container.d.ts
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare function TreeContainer(): import("react/jsx-runtime").JSX.Element;
|
||||
Generated
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.TreeContainer = TreeContainer;
|
||||
const jsx_runtime_1 = require("react/jsx-runtime");
|
||||
const context_1 = require("../context");
|
||||
const default_container_1 = require("./default-container");
|
||||
function TreeContainer() {
|
||||
const tree = (0, context_1.useTreeApi)();
|
||||
const Container = tree.props.renderContainer || default_container_1.DefaultContainer;
|
||||
return ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsx)(Container, {}) }));
|
||||
}
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import { TreeApi } from "../interfaces/tree-api";
|
||||
import { TreeProps } from "../types/tree-props";
|
||||
declare function TreeComponent<T>(props: TreeProps<T>, ref: React.Ref<TreeApi<T> | undefined>): import("react/jsx-runtime").JSX.Element;
|
||||
export declare const Tree: <T>(props: TreeProps<T> & {
|
||||
ref?: React.ForwardedRef<TreeApi<T> | undefined>;
|
||||
}) => ReturnType<typeof TreeComponent>;
|
||||
export {};
|
||||
Generated
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.Tree = void 0;
|
||||
const jsx_runtime_1 = require("react/jsx-runtime");
|
||||
const react_1 = require("react");
|
||||
const provider_1 = require("./provider");
|
||||
const outer_drop_1 = require("./outer-drop");
|
||||
const tree_container_1 = require("./tree-container");
|
||||
const drag_preview_container_1 = require("./drag-preview-container");
|
||||
const use_validated_props_1 = require("../hooks/use-validated-props");
|
||||
function TreeComponent(props, ref) {
|
||||
const treeProps = (0, use_validated_props_1.useValidatedProps)(props);
|
||||
return ((0, jsx_runtime_1.jsxs)(provider_1.TreeProvider, { treeProps: treeProps, imperativeHandle: ref, children: [(0, jsx_runtime_1.jsx)(outer_drop_1.OuterDrop, { children: (0, jsx_runtime_1.jsx)(tree_container_1.TreeContainer, {}) }), (0, jsx_runtime_1.jsx)(drag_preview_container_1.DragPreviewContainer, {})] }));
|
||||
}
|
||||
exports.Tree = (0, react_1.forwardRef)(TreeComponent);
|
||||
Generated
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { TreeApi } from "./interfaces/tree-api";
|
||||
export declare const TreeApiContext: React.Context<TreeApi<any> | null>;
|
||||
export declare function useTreeApi<T>(): TreeApi<T>;
|
||||
export declare const NodesContext: React.Context<{
|
||||
focus: import("./state/focus-slice").FocusState;
|
||||
edit: import("./state/edit-slice").EditState;
|
||||
open: import("./state/open-slice").OpenSlice;
|
||||
selection: import("./state/selection-slice").SelectionState;
|
||||
drag: import("./state/drag-slice").DragSlice;
|
||||
} | null>;
|
||||
export declare function useNodesContext(): {
|
||||
focus: import("./state/focus-slice").FocusState;
|
||||
edit: import("./state/edit-slice").EditState;
|
||||
open: import("./state/open-slice").OpenSlice;
|
||||
selection: import("./state/selection-slice").SelectionState;
|
||||
drag: import("./state/drag-slice").DragSlice;
|
||||
};
|
||||
export declare const DndContext: React.Context<import("./state/dnd-slice").DndState | null>;
|
||||
export declare function useDndContext(): import("./state/dnd-slice").DndState;
|
||||
export declare const DataUpdatesContext: React.Context<number>;
|
||||
export declare function useDataUpdates(): void;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.DataUpdatesContext = exports.DndContext = exports.NodesContext = exports.TreeApiContext = void 0;
|
||||
exports.useTreeApi = useTreeApi;
|
||||
exports.useNodesContext = useNodesContext;
|
||||
exports.useDndContext = useDndContext;
|
||||
exports.useDataUpdates = useDataUpdates;
|
||||
const react_1 = require("react");
|
||||
exports.TreeApiContext = (0, react_1.createContext)(null);
|
||||
function useTreeApi() {
|
||||
const value = (0, react_1.useContext)(exports.TreeApiContext);
|
||||
if (value === null)
|
||||
throw new Error("No Tree Api Provided");
|
||||
return value;
|
||||
}
|
||||
exports.NodesContext = (0, react_1.createContext)(null);
|
||||
function useNodesContext() {
|
||||
const value = (0, react_1.useContext)(exports.NodesContext);
|
||||
if (value === null)
|
||||
throw new Error("Provide a NodesContext");
|
||||
return value;
|
||||
}
|
||||
exports.DndContext = (0, react_1.createContext)(null);
|
||||
function useDndContext() {
|
||||
const value = (0, react_1.useContext)(exports.DndContext);
|
||||
if (value === null)
|
||||
throw new Error("Provide a DnDContext");
|
||||
return value;
|
||||
}
|
||||
exports.DataUpdatesContext = (0, react_1.createContext)(0);
|
||||
function useDataUpdates() {
|
||||
(0, react_1.useContext)(exports.DataUpdatesContext);
|
||||
}
|
||||
Generated
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Convert the `index` that `onMove` reports into the index to use when your
|
||||
* handler removes the dragged rows *before* inserting them.
|
||||
*
|
||||
* `onMove`'s `index` is a pre-removal slot: it counts positions in the
|
||||
* destination parent's child list as shown on screen, with the dragged rows
|
||||
* still in place. The built-in `SimpleTree.move` / `useSimpleTree` handle this
|
||||
* by inserting before removing, so the index needs no adjustment there. But a
|
||||
* hand-written handler that splices the dragged rows out first — the natural
|
||||
* way to reorder an array — shifts its target one place left for every dragged
|
||||
* row that started before the drop slot. Without correction, dragging a row to
|
||||
* just below itself would jump it past its neighbor (issue #247).
|
||||
*
|
||||
* Pass the destination parent's current child ids in display order (for a
|
||||
* root-level move, that's your top-level data's ids). Ids that aren't among
|
||||
* them — i.e. rows moving in from another parent — don't shift anything.
|
||||
*
|
||||
* @example
|
||||
* const onMove = ({ dragIds, index }) => {
|
||||
* const to = adjustMoveIndex({ index, dragIds, siblingIds: data.map((d) => d.id) });
|
||||
* const dragged = data.filter((d) => dragIds.includes(d.id));
|
||||
* const rest = data.filter((d) => !dragIds.includes(d.id));
|
||||
* rest.splice(to, 0, ...dragged);
|
||||
* setData(rest);
|
||||
* };
|
||||
*/
|
||||
export declare function adjustMoveIndex(args: {
|
||||
index: number;
|
||||
dragIds: string[];
|
||||
siblingIds: string[];
|
||||
}): number;
|
||||
Generated
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.adjustMoveIndex = adjustMoveIndex;
|
||||
/**
|
||||
* Convert the `index` that `onMove` reports into the index to use when your
|
||||
* handler removes the dragged rows *before* inserting them.
|
||||
*
|
||||
* `onMove`'s `index` is a pre-removal slot: it counts positions in the
|
||||
* destination parent's child list as shown on screen, with the dragged rows
|
||||
* still in place. The built-in `SimpleTree.move` / `useSimpleTree` handle this
|
||||
* by inserting before removing, so the index needs no adjustment there. But a
|
||||
* hand-written handler that splices the dragged rows out first — the natural
|
||||
* way to reorder an array — shifts its target one place left for every dragged
|
||||
* row that started before the drop slot. Without correction, dragging a row to
|
||||
* just below itself would jump it past its neighbor (issue #247).
|
||||
*
|
||||
* Pass the destination parent's current child ids in display order (for a
|
||||
* root-level move, that's your top-level data's ids). Ids that aren't among
|
||||
* them — i.e. rows moving in from another parent — don't shift anything.
|
||||
*
|
||||
* @example
|
||||
* const onMove = ({ dragIds, index }) => {
|
||||
* const to = adjustMoveIndex({ index, dragIds, siblingIds: data.map((d) => d.id) });
|
||||
* const dragged = data.filter((d) => dragIds.includes(d.id));
|
||||
* const rest = data.filter((d) => !dragIds.includes(d.id));
|
||||
* rest.splice(to, 0, ...dragged);
|
||||
* setData(rest);
|
||||
* };
|
||||
*/
|
||||
function adjustMoveIndex(args) {
|
||||
const { index, dragIds, siblingIds } = args;
|
||||
const dragging = new Set(dragIds);
|
||||
// `index` is a slot in [0, siblingIds.length]; clamp defensively so an
|
||||
// out-of-range value yields a usable post-removal insertion index on its own
|
||||
// rather than relying on the caller's splice to clamp it.
|
||||
const slot = Math.max(0, Math.min(index, siblingIds.length));
|
||||
let shift = 0;
|
||||
for (let i = 0; i < slot; i++) {
|
||||
if (dragging.has(siblingIds[i]))
|
||||
shift++;
|
||||
}
|
||||
return slot - shift;
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
buildtime/client/logical-editor/node_modules/react-arborist/dist/main/data/adjust-move-index.test.js
Generated
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const adjust_move_index_1 = require("./adjust-move-index");
|
||||
/* Apply an onMove the naive remove-then-insert way, using the adjusted index,
|
||||
and return the resulting id order. Mirrors what a custom onMove does with a
|
||||
flat data array. */
|
||||
function reorder(ids, dragIds, index) {
|
||||
const to = (0, adjust_move_index_1.adjustMoveIndex)({ index, dragIds, siblingIds: ids });
|
||||
const dragged = ids.filter((id) => dragIds.includes(id));
|
||||
const rest = ids.filter((id) => !dragIds.includes(id));
|
||||
rest.splice(to, 0, ...dragged);
|
||||
return rest;
|
||||
}
|
||||
describe("adjustMoveIndex (#247)", () => {
|
||||
const list = ["a", "b", "c"];
|
||||
test("dropping a row just below itself keeps it in place", () => {
|
||||
// computeDrop reports index 1 for the a|b gap; the raw value would move "a"
|
||||
// past "b". Adjusting brings it back to 0.
|
||||
expect((0, adjust_move_index_1.adjustMoveIndex)({ index: 1, dragIds: ["a"], siblingIds: list })).toBe(0);
|
||||
expect(reorder(list, ["a"], 1)).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
test("dropping a row just above itself keeps it in place", () => {
|
||||
// The b|c gap reports index 2 when dragging "b" down onto it.
|
||||
expect((0, adjust_move_index_1.adjustMoveIndex)({ index: 2, dragIds: ["b"], siblingIds: list })).toBe(1);
|
||||
expect(reorder(list, ["b"], 2)).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
test("moving a row to the very bottom lands it last", () => {
|
||||
expect((0, adjust_move_index_1.adjustMoveIndex)({ index: 3, dragIds: ["a"], siblingIds: list })).toBe(2);
|
||||
expect(reorder(list, ["a"], 3)).toEqual(["b", "c", "a"]);
|
||||
});
|
||||
test("moving a row upward past earlier rows needs no shift", () => {
|
||||
// "c" (index 2) dropped into the a|b gap (index 1): nothing dragged sits
|
||||
// before the slot, so the index is unchanged.
|
||||
expect((0, adjust_move_index_1.adjustMoveIndex)({ index: 1, dragIds: ["c"], siblingIds: list })).toBe(1);
|
||||
expect(reorder(list, ["c"], 1)).toEqual(["a", "c", "b"]);
|
||||
});
|
||||
test("a row moving in from another parent is not shifted", () => {
|
||||
// The dragged id isn't among the destination siblings, so no removal
|
||||
// happens in this list and the index is used as-is.
|
||||
expect((0, adjust_move_index_1.adjustMoveIndex)({ index: 1, dragIds: ["x"], siblingIds: list })).toBe(1);
|
||||
});
|
||||
test("multiple dragged rows before the slot each shift the index", () => {
|
||||
const four = ["a", "b", "c", "d"];
|
||||
// Drag "a" and "b" (indices 0 and 1) into the c|d gap (index 3).
|
||||
expect((0, adjust_move_index_1.adjustMoveIndex)({ index: 3, dragIds: ["a", "b"], siblingIds: four })).toBe(1);
|
||||
expect(reorder(four, ["a", "b"], 3)).toEqual(["c", "a", "b", "d"]);
|
||||
});
|
||||
test("only dragged rows before the slot count", () => {
|
||||
const four = ["a", "b", "c", "d"];
|
||||
// Drag "a" (index 0) and "d" (index 3) to the b|c gap (index 2): only "a"
|
||||
// is before the slot, so shift by 1.
|
||||
expect((0, adjust_move_index_1.adjustMoveIndex)({ index: 2, dragIds: ["a", "d"], siblingIds: four })).toBe(1);
|
||||
expect(reorder(four, ["a", "d"], 2)).toEqual(["b", "a", "d", "c"]);
|
||||
});
|
||||
test("an index past the end of the sibling list clamps to the end", () => {
|
||||
// 99 clamps to the list length (3); after removing the one dragged row that
|
||||
// precedes the slot, the insertion point is the end of the 2-item result.
|
||||
expect((0, adjust_move_index_1.adjustMoveIndex)({ index: 99, dragIds: ["a"], siblingIds: list })).toBe(2);
|
||||
expect(reorder(list, ["a"], 99)).toEqual(["b", "c", "a"]);
|
||||
});
|
||||
test("never returns a negative index", () => {
|
||||
expect((0, adjust_move_index_1.adjustMoveIndex)({ index: 0, dragIds: ["a"], siblingIds: list })).toBe(0);
|
||||
});
|
||||
});
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import { NodeApi } from "../interfaces/node-api";
|
||||
export declare const createIndex: <T>(nodes: NodeApi<T>[]) => {
|
||||
[id: string]: number;
|
||||
};
|
||||
Generated
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createIndex = void 0;
|
||||
const createIndex = (nodes) => {
|
||||
return nodes.reduce((map, node, index) => {
|
||||
map[node.id] = index;
|
||||
return map;
|
||||
}, {});
|
||||
};
|
||||
exports.createIndex = createIndex;
|
||||
Generated
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import { NodeApi } from "../interfaces/node-api";
|
||||
import { TreeApi } from "../interfaces/tree-api";
|
||||
export type ListResult<T> = {
|
||||
list: NodeApi<T>[];
|
||||
matchCount: number;
|
||||
};
|
||||
export declare function createList<T>(tree: TreeApi<T>): ListResult<T>;
|
||||
Generated
Vendored
+63
@@ -0,0 +1,63 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createList = createList;
|
||||
function createList(tree) {
|
||||
if (tree.isFiltered) {
|
||||
return flattenAndFilterTree(tree.root, tree.isMatch.bind(tree));
|
||||
}
|
||||
else {
|
||||
return { list: flattenTree(tree.root), matchCount: 0 };
|
||||
}
|
||||
}
|
||||
function flattenTree(root) {
|
||||
const list = [];
|
||||
function collect(node) {
|
||||
var _a;
|
||||
if (node.level >= 0) {
|
||||
list.push(node);
|
||||
}
|
||||
if (node.isOpen) {
|
||||
(_a = node.children) === null || _a === void 0 ? void 0 : _a.forEach(collect);
|
||||
}
|
||||
}
|
||||
collect(root);
|
||||
list.forEach(assignRowIndex);
|
||||
return list;
|
||||
}
|
||||
function flattenAndFilterTree(root, isMatch) {
|
||||
const matches = {};
|
||||
const list = [];
|
||||
let matchCount = 0;
|
||||
function markMatch(node) {
|
||||
const yes = !node.isRoot && isMatch(node);
|
||||
if (yes) {
|
||||
matchCount++;
|
||||
matches[node.id] = true;
|
||||
let parent = node.parent;
|
||||
while (parent) {
|
||||
matches[parent.id] = true;
|
||||
parent = parent.parent;
|
||||
}
|
||||
}
|
||||
if (node.children) {
|
||||
for (let child of node.children)
|
||||
markMatch(child);
|
||||
}
|
||||
}
|
||||
function collect(node) {
|
||||
var _a;
|
||||
if (node.level >= 0 && matches[node.id]) {
|
||||
list.push(node);
|
||||
}
|
||||
if (node.isOpen) {
|
||||
(_a = node.children) === null || _a === void 0 ? void 0 : _a.forEach(collect);
|
||||
}
|
||||
}
|
||||
markMatch(root);
|
||||
collect(root);
|
||||
list.forEach(assignRowIndex);
|
||||
return { list, matchCount };
|
||||
}
|
||||
function assignRowIndex(node, index) {
|
||||
node.rowIndex = index;
|
||||
}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import { NodeApi } from "../interfaces/node-api";
|
||||
import { TreeApi } from "../interfaces/tree-api";
|
||||
export declare const ROOT_ID = "__REACT_ARBORIST_INTERNAL_ROOT__";
|
||||
export declare function createRoot<T>(tree: TreeApi<T>): NodeApi<T>;
|
||||
Generated
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ROOT_ID = void 0;
|
||||
exports.createRoot = createRoot;
|
||||
const node_api_1 = require("../interfaces/node-api");
|
||||
exports.ROOT_ID = "__REACT_ARBORIST_INTERNAL_ROOT__";
|
||||
function createRoot(tree) {
|
||||
var _a;
|
||||
function visitSelfAndChildren(data, level, parent) {
|
||||
const id = tree.accessId(data);
|
||||
const node = new node_api_1.NodeApi({
|
||||
tree,
|
||||
data,
|
||||
level,
|
||||
parent,
|
||||
id,
|
||||
children: null,
|
||||
isDraggable: tree.isDraggable(data),
|
||||
rowIndex: null,
|
||||
});
|
||||
const children = tree.accessChildren(data);
|
||||
if (children) {
|
||||
node.children = children.map((child) => visitSelfAndChildren(child, level + 1, node));
|
||||
}
|
||||
return node;
|
||||
}
|
||||
const root = new node_api_1.NodeApi({
|
||||
tree,
|
||||
id: exports.ROOT_ID,
|
||||
// @ts-ignore
|
||||
data: { id: exports.ROOT_ID },
|
||||
level: -1,
|
||||
parent: null,
|
||||
children: null,
|
||||
isDraggable: true,
|
||||
rowIndex: null,
|
||||
});
|
||||
const data = (_a = tree.props.data) !== null && _a !== void 0 ? _a : [];
|
||||
root.children = data.map((child) => {
|
||||
return visitSelfAndChildren(child, 0, root);
|
||||
});
|
||||
return root;
|
||||
}
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
export declare function makeTree(string: string): {
|
||||
id: string;
|
||||
name: string;
|
||||
isOpen: boolean;
|
||||
};
|
||||
Generated
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
"use strict";
|
||||
// A function that turns a string of text into a tree
|
||||
// Each line is a node
|
||||
// The number of spaces at the beginning indicate the level
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.makeTree = makeTree;
|
||||
function makeTree(string) {
|
||||
const root = { id: "ROOT", name: "ROOT", isOpen: true };
|
||||
let prevNode = root;
|
||||
let prevLevel = -1;
|
||||
let id = 1;
|
||||
string.split("\n").forEach((line) => {
|
||||
const name = line.trimStart();
|
||||
const level = line.length - name.length;
|
||||
const diff = level - prevLevel;
|
||||
const node = { id: (id++).toString(), name, isOpen: false };
|
||||
if (diff === 1) {
|
||||
// First child
|
||||
//@ts-ignore
|
||||
node.parent = prevNode;
|
||||
//@ts-ignore
|
||||
prevNode.children = [node];
|
||||
}
|
||||
else {
|
||||
// Find the parent and go up
|
||||
//@ts-ignore
|
||||
let parent = prevNode.parent;
|
||||
for (let i = diff; i < 0; i++) {
|
||||
parent = parent.parent;
|
||||
}
|
||||
//@ts-ignore
|
||||
node.parent = parent;
|
||||
parent.children.push(node);
|
||||
}
|
||||
prevNode = node;
|
||||
prevLevel = level;
|
||||
});
|
||||
return root;
|
||||
}
|
||||
Generated
Vendored
+50
@@ -0,0 +1,50 @@
|
||||
export type SimpleTreeOptions<T> = {
|
||||
idAccessor?: string | ((d: T) => string);
|
||||
childrenAccessor?: string | ((d: T) => readonly T[] | null | undefined);
|
||||
};
|
||||
type Accessors<T> = {
|
||||
getId: (data: T) => string;
|
||||
getChildren: (data: T) => readonly T[] | null | undefined;
|
||||
childrenKey: string;
|
||||
};
|
||||
export declare class SimpleTree<T> {
|
||||
root: SimpleNode<T>;
|
||||
private accessors;
|
||||
constructor(data: T[], options?: SimpleTreeOptions<T>);
|
||||
get data(): T[];
|
||||
create(args: {
|
||||
parentId: string | null;
|
||||
index: number;
|
||||
data: T;
|
||||
}): null | undefined;
|
||||
move(args: {
|
||||
id: string;
|
||||
parentId: string | null;
|
||||
index: number;
|
||||
}): void;
|
||||
update(args: {
|
||||
id: string;
|
||||
changes: Partial<T>;
|
||||
}): void;
|
||||
drop(args: {
|
||||
id: string;
|
||||
}): void;
|
||||
find(id: string, node?: SimpleNode<T>): SimpleNode<T> | null;
|
||||
}
|
||||
declare class SimpleNode<T> {
|
||||
data: T;
|
||||
parent: SimpleNode<T> | null;
|
||||
private accessors;
|
||||
id: string;
|
||||
children?: SimpleNode<T>[];
|
||||
constructor(data: T, parent: SimpleNode<T> | null, accessors: Accessors<T>, id?: string);
|
||||
hasParent(): this is this & {
|
||||
parent: SimpleNode<T>;
|
||||
};
|
||||
get childIndex(): number;
|
||||
addChild(data: T, index: number): void;
|
||||
removeChild(index: number): void;
|
||||
update(changes: Partial<T>): void;
|
||||
drop(): void;
|
||||
}
|
||||
export {};
|
||||
Generated
Vendored
+119
@@ -0,0 +1,119 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.SimpleTree = void 0;
|
||||
function resolveAccessors(options = {}) {
|
||||
var _a, _b;
|
||||
const id = (_a = options.idAccessor) !== null && _a !== void 0 ? _a : "id";
|
||||
const children = (_b = options.childrenAccessor) !== null && _b !== void 0 ? _b : "children";
|
||||
return {
|
||||
getId: typeof id === "function" ? id : (data) => data[id],
|
||||
getChildren: typeof children === "function" ? children : (data) => data[children],
|
||||
childrenKey: typeof children === "string" ? children : "children",
|
||||
};
|
||||
}
|
||||
class SimpleTree {
|
||||
constructor(data, options = {}) {
|
||||
this.accessors = resolveAccessors(options);
|
||||
this.root = createRoot(data, this.accessors);
|
||||
}
|
||||
get data() {
|
||||
var _a, _b;
|
||||
return (_b = (_a = this.root.children) === null || _a === void 0 ? void 0 : _a.map((node) => node.data)) !== null && _b !== void 0 ? _b : [];
|
||||
}
|
||||
create(args) {
|
||||
const parent = args.parentId ? this.find(args.parentId) : this.root;
|
||||
if (!parent)
|
||||
return null;
|
||||
parent.addChild(args.data, args.index);
|
||||
}
|
||||
move(args) {
|
||||
const src = this.find(args.id);
|
||||
const parent = args.parentId ? this.find(args.parentId) : this.root;
|
||||
if (!src || !parent)
|
||||
return;
|
||||
parent.addChild(src.data, args.index);
|
||||
src.drop();
|
||||
}
|
||||
update(args) {
|
||||
const node = this.find(args.id);
|
||||
if (node)
|
||||
node.update(args.changes);
|
||||
}
|
||||
drop(args) {
|
||||
const node = this.find(args.id);
|
||||
if (node)
|
||||
node.drop();
|
||||
}
|
||||
find(id, node = this.root) {
|
||||
if (!node)
|
||||
return null;
|
||||
if (node.id === id)
|
||||
return node;
|
||||
if (node.children) {
|
||||
for (let child of node.children) {
|
||||
const found = this.find(id, child);
|
||||
if (found)
|
||||
return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
exports.SimpleTree = SimpleTree;
|
||||
function createRoot(data, accessors) {
|
||||
// The synthetic root has no real data, so it gets an explicit id rather than
|
||||
// running the user's accessor on `{}` — a function accessor that reaches into
|
||||
// the data (e.g. `d => d.meta.id`) would otherwise throw during construction.
|
||||
const root = new SimpleNode({}, null, accessors, "ROOT");
|
||||
root.children = data.map((d) => createNode(d, root, accessors));
|
||||
return root;
|
||||
}
|
||||
function createNode(data, parent, accessors) {
|
||||
const node = new SimpleNode(data, parent, accessors);
|
||||
const children = accessors.getChildren(data);
|
||||
if (children)
|
||||
node.children = children.map((d) => createNode(d, node, accessors));
|
||||
return node;
|
||||
}
|
||||
class SimpleNode {
|
||||
constructor(data, parent, accessors, id) {
|
||||
this.data = data;
|
||||
this.parent = parent;
|
||||
this.accessors = accessors;
|
||||
this.id = id !== null && id !== void 0 ? id : accessors.getId(data);
|
||||
}
|
||||
hasParent() {
|
||||
return !!this.parent;
|
||||
}
|
||||
get childIndex() {
|
||||
return this.hasParent() ? this.parent.children.indexOf(this) : -1;
|
||||
}
|
||||
addChild(data, index) {
|
||||
var _a, _b;
|
||||
const node = createNode(data, this, this.accessors);
|
||||
this.children = (_a = this.children) !== null && _a !== void 0 ? _a : [];
|
||||
this.children.splice(index, 0, node);
|
||||
const key = this.accessors.childrenKey;
|
||||
const raw = this.data;
|
||||
raw[key] = (_b = raw[key]) !== null && _b !== void 0 ? _b : [];
|
||||
raw[key].splice(index, 0, data);
|
||||
}
|
||||
removeChild(index) {
|
||||
var _a, _b;
|
||||
(_a = this.children) === null || _a === void 0 ? void 0 : _a.splice(index, 1);
|
||||
const raw = this.data;
|
||||
(_b = raw[this.accessors.childrenKey]) === null || _b === void 0 ? void 0 : _b.splice(index, 1);
|
||||
}
|
||||
update(changes) {
|
||||
if (this.hasParent()) {
|
||||
const i = this.childIndex;
|
||||
this.parent.addChild(Object.assign(Object.assign({}, this.data), changes), i);
|
||||
this.drop();
|
||||
}
|
||||
}
|
||||
drop() {
|
||||
if (this.hasParent())
|
||||
this.parent.removeChild(this.childIndex);
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Generated
Vendored
+63
@@ -0,0 +1,63 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const simple_tree_1 = require("./simple-tree");
|
||||
describe("SimpleTree with default accessors", () => {
|
||||
const data = () => [
|
||||
{ id: "1", name: "a", children: [{ id: "1a", name: "a-child" }] },
|
||||
{ id: "2", name: "b" },
|
||||
];
|
||||
test("finds nodes by id, including nested ones", () => {
|
||||
var _a, _b;
|
||||
const tree = new simple_tree_1.SimpleTree(data());
|
||||
expect((_a = tree.find("2")) === null || _a === void 0 ? void 0 : _a.data.name).toBe("b");
|
||||
expect((_b = tree.find("1a")) === null || _b === void 0 ? void 0 : _b.data.name).toBe("a-child");
|
||||
});
|
||||
test("moves a node into a folder", () => {
|
||||
const tree = new simple_tree_1.SimpleTree(data());
|
||||
tree.move({ id: "2", parentId: "1", index: 1 });
|
||||
expect(tree.data[0].children.map((c) => c.id)).toEqual(["1a", "2"]);
|
||||
expect(tree.data).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
describe("SimpleTree honors custom accessors (issue #73, #170)", () => {
|
||||
// Custom keys: `uuid` for the id, `elements` for the children.
|
||||
const data = () => [
|
||||
{ uuid: "1", name: "a", elements: [{ uuid: "1a", name: "a-child" }] },
|
||||
{ uuid: "2", name: "b" },
|
||||
];
|
||||
function tree() {
|
||||
return new simple_tree_1.SimpleTree(data(), { idAccessor: "uuid", childrenAccessor: "elements" });
|
||||
}
|
||||
test("finds nodes by the custom id key, including nested ones", () => {
|
||||
var _a, _b;
|
||||
const t = tree();
|
||||
expect((_a = t.find("2")) === null || _a === void 0 ? void 0 : _a.data.name).toBe("b");
|
||||
expect((_b = t.find("1a")) === null || _b === void 0 ? void 0 : _b.data.name).toBe("a-child"); // read through `elements`
|
||||
});
|
||||
test("reorders a node, writing children back under the custom key (#170)", () => {
|
||||
const t = tree();
|
||||
t.move({ id: "2", parentId: "1", index: 1 });
|
||||
expect(t.data).toHaveLength(1);
|
||||
expect(t.data[0].elements.map((c) => c.uuid)).toEqual(["1a", "2"]);
|
||||
});
|
||||
test("moving a node with children into a childless node keeps its children (#73)", () => {
|
||||
const t = tree();
|
||||
// Put node "1" (which has children) inside node "2" (which has none).
|
||||
t.move({ id: "1", parentId: "2", index: 0 });
|
||||
expect(t.data.map((n) => n.uuid)).toEqual(["2"]);
|
||||
const moved = t.find("1");
|
||||
expect(moved === null || moved === void 0 ? void 0 : moved.data.elements.map((c) => c.uuid)).toEqual(["1a"]);
|
||||
});
|
||||
test("supports a function idAccessor", () => {
|
||||
var _a;
|
||||
const t = new simple_tree_1.SimpleTree(data(), { idAccessor: (d) => d.uuid, childrenAccessor: "elements" });
|
||||
expect((_a = t.find("1a")) === null || _a === void 0 ? void 0 : _a.data.name).toBe("a-child");
|
||||
t.move({ id: "2", parentId: "1", index: 1 });
|
||||
expect(t.data[0].elements.map((c) => c.uuid)).toEqual(["1a", "2"]);
|
||||
});
|
||||
test("a function idAccessor that reaches into the data doesn't throw on construction", () => {
|
||||
const nested = [{ meta: { id: "x" }, name: "x" }];
|
||||
// The synthetic root must not run this accessor on its empty data.
|
||||
expect(() => new simple_tree_1.SimpleTree(nested, { idAccessor: (d) => d.meta.id })).not.toThrow();
|
||||
});
|
||||
});
|
||||
Generated
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
import { XYCoord } from "react-dnd";
|
||||
import { NodeApi } from "../interfaces/node-api";
|
||||
import { DropResult } from "./drop-hook";
|
||||
type Args = {
|
||||
element: HTMLElement;
|
||||
offset: XYCoord;
|
||||
indent: number;
|
||||
node: NodeApi | null;
|
||||
prevNode: NodeApi | null;
|
||||
nextNode: NodeApi | null;
|
||||
};
|
||||
export type ComputedDrop = {
|
||||
drop: DropResult | null;
|
||||
cursor: Cursor | null;
|
||||
};
|
||||
declare function lineCursor(index: number, level: number): {
|
||||
type: "line";
|
||||
index: number;
|
||||
level: number;
|
||||
};
|
||||
declare function noCursor(): {
|
||||
type: "none";
|
||||
};
|
||||
declare function highlightCursor(id: string): {
|
||||
type: "highlight";
|
||||
id: string;
|
||||
};
|
||||
export type LineCursor = ReturnType<typeof lineCursor>;
|
||||
export type NoCursor = ReturnType<typeof noCursor>;
|
||||
export type HighlightCursor = ReturnType<typeof highlightCursor>;
|
||||
export type Cursor = LineCursor | NoCursor | HighlightCursor;
|
||||
/**
|
||||
* This is the most complex, tricky function in the whole repo.
|
||||
*/
|
||||
export declare function computeDrop(args: Args): ComputedDrop;
|
||||
export {};
|
||||
Generated
Vendored
+143
@@ -0,0 +1,143 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.computeDrop = computeDrop;
|
||||
const utils_1 = require("../utils");
|
||||
function measureHover(el, offset) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = offset.x - Math.round(rect.x);
|
||||
const y = offset.y - Math.round(rect.y);
|
||||
const height = rect.height;
|
||||
const inTopHalf = y < height / 2;
|
||||
const inBottomHalf = !inTopHalf;
|
||||
const pad = height / 4;
|
||||
const inMiddle = y > pad && y < height - pad;
|
||||
const atTop = !inMiddle && inTopHalf;
|
||||
const atBottom = !inMiddle && inBottomHalf;
|
||||
return { x, inTopHalf, inBottomHalf, inMiddle, atTop, atBottom };
|
||||
}
|
||||
function getNodesAroundCursor(node, prev, next, hover) {
|
||||
if (!node) {
|
||||
// We're hovering over the empty part of the list, not over an item,
|
||||
// Put the cursor below the last item which is "prev"
|
||||
return [prev, null];
|
||||
}
|
||||
if (node.isInternal) {
|
||||
if (hover.atTop) {
|
||||
return [prev, node];
|
||||
}
|
||||
else if (hover.inMiddle) {
|
||||
return [node, node];
|
||||
}
|
||||
else {
|
||||
return [node, next];
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (hover.inTopHalf) {
|
||||
return [prev, node];
|
||||
}
|
||||
else {
|
||||
return [node, next];
|
||||
}
|
||||
}
|
||||
}
|
||||
function dropAt(parentId, index) {
|
||||
return { parentId: parentId || null, index };
|
||||
}
|
||||
function lineCursor(index, level) {
|
||||
return {
|
||||
type: "line",
|
||||
index,
|
||||
level,
|
||||
};
|
||||
}
|
||||
function noCursor() {
|
||||
return {
|
||||
type: "none",
|
||||
};
|
||||
}
|
||||
function highlightCursor(id) {
|
||||
return {
|
||||
type: "highlight",
|
||||
id,
|
||||
};
|
||||
}
|
||||
function walkUpFrom(node, level) {
|
||||
var _a;
|
||||
let drop = node;
|
||||
while (drop.parent && drop.level > level) {
|
||||
drop = drop.parent;
|
||||
}
|
||||
const parentId = ((_a = drop.parent) === null || _a === void 0 ? void 0 : _a.id) || null;
|
||||
const index = (0, utils_1.indexOf)(drop) + 1;
|
||||
return { parentId, index };
|
||||
}
|
||||
/**
|
||||
* This is the most complex, tricky function in the whole repo.
|
||||
*/
|
||||
function computeDrop(args) {
|
||||
var _a;
|
||||
const hover = measureHover(args.element, args.offset);
|
||||
const indent = args.indent;
|
||||
const hoverLevel = Math.round(Math.max(0, hover.x - indent) / indent);
|
||||
const { node, nextNode, prevNode } = args;
|
||||
const [above, below] = getNodesAroundCursor(node, prevNode, nextNode, hover);
|
||||
/* Hovering over the middle of a folder */
|
||||
if (node && node.isInternal && hover.inMiddle) {
|
||||
return {
|
||||
drop: dropAt(node.id, null),
|
||||
cursor: highlightCursor(node.id),
|
||||
};
|
||||
}
|
||||
/*
|
||||
* Now we only need to care about the node above the cursor
|
||||
* ----------- -------
|
||||
*/
|
||||
/* There is no node above the cursor line */
|
||||
if (!above) {
|
||||
return {
|
||||
drop: dropAt((_a = below === null || below === void 0 ? void 0 : below.parent) === null || _a === void 0 ? void 0 : _a.id, 0),
|
||||
cursor: lineCursor(0, 0),
|
||||
};
|
||||
}
|
||||
/* The node above the cursor line is an item */
|
||||
if ((0, utils_1.isItem)(above)) {
|
||||
const level = (0, utils_1.bound)(hoverLevel, (below === null || below === void 0 ? void 0 : below.level) || 0, above.level);
|
||||
return {
|
||||
drop: walkUpFrom(above, level),
|
||||
cursor: lineCursor(above.rowIndex + 1, level),
|
||||
};
|
||||
}
|
||||
/* The node above the cursor line is a closed folder */
|
||||
if ((0, utils_1.isClosed)(above)) {
|
||||
const level = (0, utils_1.bound)(hoverLevel, (below === null || below === void 0 ? void 0 : below.level) || 0, above.level);
|
||||
return {
|
||||
drop: walkUpFrom(above, level),
|
||||
cursor: lineCursor(above.rowIndex + 1, level),
|
||||
};
|
||||
}
|
||||
/*
|
||||
* The node above the cursor is an open folder — empty or with children.
|
||||
* `below` is either the folder's first child (children) or its next sibling
|
||||
* (empty), but neither pins the slide: the deep end is the folder's first
|
||||
* child (above.level + 1) and the shallow end walks up the ancestor chain,
|
||||
* which walkUpFrom() clamps at the root. Sliding right drops as the first
|
||||
* child (the historical default); sliding left drops as a sibling or
|
||||
* grandsibling of the folder (#330).
|
||||
*/
|
||||
const level = (0, utils_1.bound)(hoverLevel, 0, above.level + 1);
|
||||
if (level > above.level) {
|
||||
/* Will be the first child of the folder */
|
||||
return {
|
||||
drop: dropAt(above.id, 0),
|
||||
cursor: lineCursor(above.rowIndex + 1, level),
|
||||
};
|
||||
}
|
||||
else {
|
||||
/* Will be a sibling or grandsibling of the folder */
|
||||
return {
|
||||
drop: walkUpFrom(above, level),
|
||||
cursor: lineCursor(above.rowIndex + 1, level),
|
||||
};
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Generated
Vendored
+119
@@ -0,0 +1,119 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const redux_1 = require("redux");
|
||||
const root_reducer_1 = require("../state/root-reducer");
|
||||
const create_root_1 = require("../data/create-root");
|
||||
const tree_api_1 = require("../interfaces/tree-api");
|
||||
const compute_drop_1 = require("./compute-drop");
|
||||
const ROW = 40; // height; top edge at y<10, middle 10..30, bottom edge y>30
|
||||
const INDENT = 24;
|
||||
function setupApi(props) {
|
||||
const store = (0, redux_1.createStore)(root_reducer_1.rootReducer);
|
||||
return new tree_api_1.TreeApi(store, Object.assign({ indent: INDENT, rowHeight: ROW }, props), { current: null }, {
|
||||
current: null,
|
||||
});
|
||||
}
|
||||
/*
|
||||
* Drive computeDrop against a hovered row. The row's rect is pinned at the
|
||||
* origin so `offset` doubles as the in-row coordinate:
|
||||
* - y picks the vertical band (5 = top edge, 20 = middle, 35 = bottom edge)
|
||||
* - x picks the horizontal drop level; hoverLevel = round((x - indent)/indent),
|
||||
* so x = (level + 1) * indent targets `level`, and a large x slides fully right.
|
||||
*/
|
||||
function hover(api, id, x, y) {
|
||||
const node = api.get(id);
|
||||
const element = {
|
||||
getBoundingClientRect: () => ({ x: 0, y: 0, height: ROW }),
|
||||
};
|
||||
return (0, compute_drop_1.computeDrop)({
|
||||
element,
|
||||
offset: { x, y },
|
||||
indent: api.indent,
|
||||
node,
|
||||
prevNode: node.prev,
|
||||
nextNode: node.next,
|
||||
});
|
||||
}
|
||||
const AT_TOP = 5;
|
||||
const MIDDLE = 20;
|
||||
const AT_BOTTOM = 35;
|
||||
const DEEP = 1000; // slides fully right
|
||||
const levelX = (level) => (level + 1) * INDENT;
|
||||
/*
|
||||
* Visible tree (all folders open by default):
|
||||
* a item, level 0, root index 0
|
||||
* F open folder, level 0, root index 1
|
||||
* G open folder, level 1, F index 0
|
||||
* C item, level 2, G index 0
|
||||
* E empty folder, level 0, root index 2
|
||||
* z item, level 0, root index 3
|
||||
*/
|
||||
const data = [
|
||||
{ id: "a" },
|
||||
{ id: "F", children: [{ id: "G", children: [{ id: "C" }] }] },
|
||||
{ id: "E", children: [] },
|
||||
{ id: "z" },
|
||||
];
|
||||
describe("computeDrop", () => {
|
||||
describe("existing branches (regression net)", () => {
|
||||
test("middle of a folder drops into it and highlights the row", () => {
|
||||
const api = setupApi({ data });
|
||||
const { drop, cursor } = hover(api, "F", levelX(0), MIDDLE);
|
||||
expect(drop).toEqual({ parentId: "F", index: null });
|
||||
expect(cursor).toEqual({ type: "highlight", id: "F" });
|
||||
});
|
||||
test("top of the first row drops at the top of the root", () => {
|
||||
const api = setupApi({ data });
|
||||
const { drop, cursor } = hover(api, "a", levelX(0), AT_TOP);
|
||||
expect(drop).toEqual({ parentId: create_root_1.ROOT_ID, index: 0 });
|
||||
expect(cursor).toEqual({ type: "line", index: 0, level: 0 });
|
||||
});
|
||||
test("below an item, sliding left walks up the ancestor chain", () => {
|
||||
const api = setupApi({ data });
|
||||
// Below C (level 2); its next row z is level 0, so the slide spans 0..2.
|
||||
const deep = hover(api, "C", DEEP, AT_BOTTOM);
|
||||
expect(deep.drop).toEqual({ parentId: "G", index: 1 }); // next sibling of C
|
||||
const shallow = hover(api, "C", levelX(0), AT_BOTTOM);
|
||||
expect(shallow.drop).toEqual({ parentId: create_root_1.ROOT_ID, index: 2 }); // after F, at root
|
||||
});
|
||||
test("below a closed folder behaves like an item", () => {
|
||||
const api = setupApi({ data });
|
||||
api.close("F", false); // rebuild the visible list so F's next row is E
|
||||
api.update(api.props);
|
||||
// With F closed, its next row is E (level 0). Sliding right pins to F's level.
|
||||
const deep = hover(api, "F", DEEP, AT_BOTTOM);
|
||||
expect(deep.drop).toEqual({ parentId: create_root_1.ROOT_ID, index: 2 }); // sibling after F
|
||||
});
|
||||
test("empty open folder: deep drops inside, shallow drops as a sibling", () => {
|
||||
const api = setupApi({ data });
|
||||
const deep = hover(api, "E", DEEP, AT_BOTTOM);
|
||||
expect(deep.drop).toEqual({ parentId: "E", index: 0 }); // first child of E
|
||||
const shallow = hover(api, "E", levelX(0), AT_BOTTOM);
|
||||
expect(shallow.drop).toEqual({ parentId: create_root_1.ROOT_ID, index: 3 }); // sibling after E
|
||||
});
|
||||
});
|
||||
describe("open folder with children (#330)", () => {
|
||||
test("sliding right still drops as the folder's first child", () => {
|
||||
const api = setupApi({ data });
|
||||
const { drop, cursor } = hover(api, "F", DEEP, AT_BOTTOM);
|
||||
expect(drop).toEqual({ parentId: "F", index: 0 });
|
||||
expect(cursor).toEqual({ type: "line", index: 2, level: 1 });
|
||||
});
|
||||
test("sliding left drops as the folder's next sibling", () => {
|
||||
const api = setupApi({ data });
|
||||
const { drop, cursor } = hover(api, "F", levelX(0), AT_BOTTOM);
|
||||
expect(drop).toEqual({ parentId: create_root_1.ROOT_ID, index: 2 }); // after F, at root
|
||||
expect(cursor).toEqual({ type: "line", index: 2, level: 0 });
|
||||
});
|
||||
test("a nested folder can drop as sibling or grandsibling, bounded by its ancestors", () => {
|
||||
const api = setupApi({ data });
|
||||
// Between G (level 1) and its child C (level 2).
|
||||
const child = hover(api, "G", DEEP, AT_BOTTOM);
|
||||
expect(child.drop).toEqual({ parentId: "G", index: 0 }); // first child of G
|
||||
const sibling = hover(api, "G", levelX(1), AT_BOTTOM);
|
||||
expect(sibling.drop).toEqual({ parentId: "F", index: 1 }); // after G, inside F
|
||||
const grandsibling = hover(api, "G", levelX(0), AT_BOTTOM);
|
||||
expect(grandsibling.drop).toEqual({ parentId: create_root_1.ROOT_ID, index: 2 }); // after F, at root
|
||||
});
|
||||
});
|
||||
});
|
||||
Generated
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
import { ConnectDragSource } from "react-dnd";
|
||||
import { NodeApi } from "../interfaces/node-api";
|
||||
import { TreeProps } from "../types/tree-props";
|
||||
export declare function dragTypeForNode<T>(dragType: TreeProps<T>["dragType"], node: NodeApi<T>): string;
|
||||
export declare function canDragNode<T>(node: NodeApi<T>): boolean;
|
||||
export declare function useDragHook<T>(node: NodeApi<T>): ConnectDragSource;
|
||||
Generated
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.dragTypeForNode = dragTypeForNode;
|
||||
exports.canDragNode = canDragNode;
|
||||
exports.useDragHook = useDragHook;
|
||||
const react_1 = require("react");
|
||||
const react_dnd_1 = require("react-dnd");
|
||||
const react_dnd_html5_backend_1 = require("react-dnd-html5-backend");
|
||||
const context_1 = require("../context");
|
||||
const dnd_slice_1 = require("../state/dnd-slice");
|
||||
/* The react-dnd item type a row's drag source broadcasts. The dragType prop
|
||||
can be a fixed string or a per-node function; it defaults to "NODE". */
|
||||
function dragTypeForNode(dragType, node) {
|
||||
if (typeof dragType === "function")
|
||||
return dragType(node);
|
||||
return dragType !== null && dragType !== void 0 ? dragType : "NODE";
|
||||
}
|
||||
/* A node can start a drag only when it's draggable and not currently being
|
||||
renamed. Without the editing guard, dragging inside the rename input would
|
||||
pick the row up and move it (issue #195). */
|
||||
function canDragNode(node) {
|
||||
return node.isDraggable && !node.isEditing;
|
||||
}
|
||||
function useDragHook(node) {
|
||||
const tree = (0, context_1.useTreeApi)();
|
||||
const ids = tree.selectedIds;
|
||||
const [_, ref, preview] = (0, react_dnd_1.useDrag)(() => ({
|
||||
canDrag: () => canDragNode(node),
|
||||
type: dragTypeForNode(tree.props.dragType, node),
|
||||
item: () => {
|
||||
// This is fired once at the beginning of a drag operation
|
||||
const dragIds = tree.isSelected(node.id) ? Array.from(ids) : [node.id];
|
||||
tree.dispatch(dnd_slice_1.actions.dragStart(node.id, dragIds));
|
||||
return { id: node.id, dragIds, data: node.data };
|
||||
},
|
||||
end: () => {
|
||||
tree.hideCursor();
|
||||
tree.redrawList();
|
||||
tree.dispatch(dnd_slice_1.actions.dragEnd());
|
||||
},
|
||||
}), [ids, node, tree.props.dragType]);
|
||||
(0, react_1.useEffect)(() => {
|
||||
preview((0, react_dnd_html5_backend_1.getEmptyImage)());
|
||||
}, [preview]);
|
||||
return ref;
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Generated
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const drag_hook_1 = require("./drag-hook");
|
||||
/* dragTypeForNode only reads node.data when dragType is a function, so a
|
||||
minimal stub stands in for a real NodeApi. */
|
||||
function nodeWith(data) {
|
||||
return { data };
|
||||
}
|
||||
test("defaults to the internal NODE type when dragType is undefined", () => {
|
||||
expect((0, drag_hook_1.dragTypeForNode)(undefined, nodeWith({ id: "a" }))).toBe("NODE");
|
||||
});
|
||||
test("uses a fixed string dragType for every node", () => {
|
||||
expect((0, drag_hook_1.dragTypeForNode)("FILE", nodeWith({ id: "a" }))).toBe("FILE");
|
||||
});
|
||||
test("resolves a per-node dragType function against the node", () => {
|
||||
const dragType = (node) => node.data.kind.toUpperCase();
|
||||
expect((0, drag_hook_1.dragTypeForNode)(dragType, nodeWith({ kind: "folder" }))).toBe("FOLDER");
|
||||
expect((0, drag_hook_1.dragTypeForNode)(dragType, nodeWith({ kind: "file" }))).toBe("FILE");
|
||||
});
|
||||
/* canDragNode only reads the isDraggable/isEditing flags, so a minimal stub
|
||||
stands in for a real NodeApi. */
|
||||
function draggableNode(flags) {
|
||||
return flags;
|
||||
}
|
||||
test("a draggable node that isn't being edited can drag", () => {
|
||||
expect((0, drag_hook_1.canDragNode)(draggableNode({ isDraggable: true, isEditing: false }))).toBe(true);
|
||||
});
|
||||
test("a non-draggable node can't drag", () => {
|
||||
expect((0, drag_hook_1.canDragNode)(draggableNode({ isDraggable: false, isEditing: false }))).toBe(false);
|
||||
});
|
||||
test("a node being renamed can't drag, even when draggable (#195)", () => {
|
||||
expect((0, drag_hook_1.canDragNode)(draggableNode({ isDraggable: true, isEditing: true }))).toBe(false);
|
||||
});
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
import { RefObject } from "react";
|
||||
import { ConnectDropTarget } from "react-dnd";
|
||||
import { NodeApi } from "../interfaces/node-api";
|
||||
export type DropResult = {
|
||||
parentId: string | null;
|
||||
index: number | null;
|
||||
};
|
||||
export declare function useDropHook(el: RefObject<HTMLElement | null>, node: NodeApi<any>): ConnectDropTarget;
|
||||
Generated
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.useDropHook = useDropHook;
|
||||
const react_dnd_1 = require("react-dnd");
|
||||
const context_1 = require("../context");
|
||||
const compute_drop_1 = require("./compute-drop");
|
||||
function useDropHook(el, node) {
|
||||
const tree = (0, context_1.useTreeApi)();
|
||||
const [_, dropRef] = (0, react_dnd_1.useDrop)(() => ({
|
||||
accept: "NODE",
|
||||
canDrop: () => tree.canDrop(),
|
||||
hover: (_item, m) => {
|
||||
const offset = m.getClientOffset();
|
||||
if (!el.current || !offset)
|
||||
return;
|
||||
const { cursor, drop } = (0, compute_drop_1.computeDrop)({
|
||||
element: el.current,
|
||||
offset: offset,
|
||||
indent: tree.indent,
|
||||
node: node,
|
||||
prevNode: node.prev,
|
||||
nextNode: node.next,
|
||||
});
|
||||
tree.hover(drop, cursor);
|
||||
},
|
||||
drop: (_, m) => {
|
||||
if (!m.canDrop())
|
||||
return null;
|
||||
tree.drop();
|
||||
},
|
||||
}), [node, el.current, tree.props]);
|
||||
return dropRef;
|
||||
}
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
import { XYCoord } from "react-dnd";
|
||||
export declare function measureHover(el: HTMLElement, offset: XYCoord, indent: number): {
|
||||
level: number;
|
||||
inTopHalf: boolean;
|
||||
inBottomHalf: boolean;
|
||||
inMiddle: boolean;
|
||||
};
|
||||
export type HoverData = ReturnType<typeof measureHover>;
|
||||
Generated
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.measureHover = measureHover;
|
||||
const utils_1 = require("../utils");
|
||||
function measureHover(el, offset, indent) {
|
||||
const nextEl = el.nextElementSibling;
|
||||
const prevEl = el.previousElementSibling;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = offset.x - Math.round(rect.x);
|
||||
const y = offset.y - Math.round(rect.y);
|
||||
const height = rect.height;
|
||||
const inTopHalf = y < height / 2;
|
||||
const inBottomHalf = !inTopHalf;
|
||||
const pad = height / 4;
|
||||
const inMiddle = y > pad && y < height - pad;
|
||||
const maxLevel = Number(inBottomHalf ? el.dataset.level : prevEl ? prevEl.dataset.level : 0);
|
||||
const minLevel = Number(inTopHalf ? el.dataset.level : nextEl ? nextEl.dataset.level : 0);
|
||||
const level = (0, utils_1.bound)(Math.floor(x / indent), minLevel, maxLevel);
|
||||
return { level, inTopHalf, inBottomHalf, inMiddle };
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare function useOuterDrop(): void;
|
||||
Generated
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.useOuterDrop = useOuterDrop;
|
||||
const react_dnd_1 = require("react-dnd");
|
||||
const context_1 = require("../context");
|
||||
const compute_drop_1 = require("./compute-drop");
|
||||
function useOuterDrop() {
|
||||
const tree = (0, context_1.useTreeApi)();
|
||||
// In case we drop an item at the bottom of the list
|
||||
const [, drop] = (0, react_dnd_1.useDrop)(() => ({
|
||||
accept: "NODE",
|
||||
canDrop: (_item, m) => {
|
||||
if (!m.isOver({ shallow: true }))
|
||||
return false;
|
||||
return tree.canDrop();
|
||||
},
|
||||
hover: (_item, m) => {
|
||||
if (!m.isOver({ shallow: true }))
|
||||
return;
|
||||
const offset = m.getClientOffset();
|
||||
if (!tree.listEl.current || !offset)
|
||||
return;
|
||||
const { cursor, drop } = (0, compute_drop_1.computeDrop)({
|
||||
element: tree.listEl.current,
|
||||
offset: offset,
|
||||
indent: tree.indent,
|
||||
node: null,
|
||||
prevNode: tree.visibleNodes[tree.visibleNodes.length - 1],
|
||||
nextNode: null,
|
||||
});
|
||||
tree.hover(drop, cursor);
|
||||
},
|
||||
drop: (_item, m) => {
|
||||
if (!m.isOver({ shallow: true }))
|
||||
return null;
|
||||
if (!m.canDrop())
|
||||
return null;
|
||||
tree.drop();
|
||||
},
|
||||
}), [tree]);
|
||||
drop(tree.listEl);
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export declare function useFreshNode<T>(index: number): import("..").NodeApi<T>;
|
||||
Generated
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.useFreshNode = useFreshNode;
|
||||
const react_1 = require("react");
|
||||
const context_1 = require("../context");
|
||||
function useFreshNode(index) {
|
||||
const tree = (0, context_1.useTreeApi)();
|
||||
const original = tree.at(index);
|
||||
if (!original)
|
||||
throw new Error(`Could not find node for index: ${index}`);
|
||||
return (0, react_1.useMemo)(() => {
|
||||
const fresh = original.clone();
|
||||
tree.visibleNodes[index] = fresh; // sneaky
|
||||
return fresh;
|
||||
// Return a fresh instance if the state values change
|
||||
}, [...Object.values(original.state), original]);
|
||||
}
|
||||
Generated
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
import { SimpleTreeOptions } from "../data/simple-tree";
|
||||
import { CreateHandler, DeleteHandler, MoveHandler, RenameHandler } from "../types/handlers";
|
||||
export type SimpleTreeData = {
|
||||
id: string;
|
||||
name: string;
|
||||
children?: SimpleTreeData[];
|
||||
};
|
||||
export type UseSimpleTreeOptions<T> = SimpleTreeOptions<T> & {
|
||||
onChange?: (data: readonly T[]) => void;
|
||||
};
|
||||
export declare function useSimpleTree<T>(initialData: readonly T[], options?: UseSimpleTreeOptions<T>): readonly [readonly T[], {
|
||||
onMove: MoveHandler<T>;
|
||||
onRename: RenameHandler<T>;
|
||||
onCreate: CreateHandler<T>;
|
||||
onDelete: DeleteHandler<T>;
|
||||
}];
|
||||
Generated
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.useSimpleTree = useSimpleTree;
|
||||
const react_1 = require("react");
|
||||
const simple_tree_1 = require("../data/simple-tree");
|
||||
let nextId = 0;
|
||||
function useSimpleTree(initialData, options = {}) {
|
||||
const [data, setData] = (0, react_1.useState)(initialData);
|
||||
const idAccessor = options.idAccessor;
|
||||
const childrenAccessor = options.childrenAccessor;
|
||||
const onChange = options.onChange;
|
||||
const tree = (0, react_1.useMemo)(() => new simple_tree_1.SimpleTree(data, { idAccessor, childrenAccessor }), [data, idAccessor, childrenAccessor]);
|
||||
/* Push the tree's new state into React and notify the caller. Reads
|
||||
tree.data once so onChange receives exactly what was committed. */
|
||||
const commit = () => {
|
||||
const next = tree.data;
|
||||
setData(next);
|
||||
onChange === null || onChange === void 0 ? void 0 : onChange(next);
|
||||
};
|
||||
const onMove = (args) => {
|
||||
for (const id of args.dragIds) {
|
||||
tree.move({ id, parentId: args.parentId, index: args.index });
|
||||
}
|
||||
commit();
|
||||
};
|
||||
const onRename = ({ name, id }) => {
|
||||
tree.update({ id, changes: { name } });
|
||||
commit();
|
||||
};
|
||||
// New nodes must carry their id/children under the same keys the accessors
|
||||
// read, or the controller (and the tree's own accessId) can't find them
|
||||
// afterward (issue #73). A function accessor can't be inverted to a writable
|
||||
// key, so node creation with one isn't supportable — fail fast instead of
|
||||
// returning a node that throws deeper in the tree.
|
||||
const idKey = typeof idAccessor === "string" ? idAccessor : "id";
|
||||
const childrenKey = typeof childrenAccessor === "string" ? childrenAccessor : "children";
|
||||
const onCreate = ({ parentId, index, type }) => {
|
||||
if (typeof idAccessor === "function") {
|
||||
throw new Error(`React Arborist => initialData can't create nodes when idAccessor is a function: the generated id can't be written under a key the accessor reads. Use a string idAccessor, or the controlled \`data\` prop with your own onCreate.`);
|
||||
}
|
||||
if (type === "internal" && typeof childrenAccessor === "function") {
|
||||
throw new Error(`React Arborist => initialData can't create folder nodes when childrenAccessor is a function: the new children array can't be written under a key the accessor reads. Use a string childrenAccessor, or the controlled \`data\` prop with your own onCreate.`);
|
||||
}
|
||||
const data = { [idKey]: `simple-tree-id-${nextId++}`, name: "" };
|
||||
if (type === "internal")
|
||||
data[childrenKey] = [];
|
||||
tree.create({ parentId, index, data });
|
||||
commit();
|
||||
return data;
|
||||
};
|
||||
const onDelete = (args) => {
|
||||
args.ids.forEach((id) => tree.drop({ id }));
|
||||
commit();
|
||||
};
|
||||
const controller = { onMove, onRename, onCreate, onDelete };
|
||||
return [data, controller];
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Generated
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const react_1 = require("@testing-library/react");
|
||||
const use_simple_tree_1 = require("./use-simple-tree");
|
||||
/* onCreate has to write a new node's id (and a folder's children) under a key
|
||||
the accessors will read back. A function accessor can't be inverted to a key,
|
||||
so creation with one must fail fast rather than return an unusable node
|
||||
(issue #73 review follow-up). */
|
||||
describe("useSimpleTree onCreate guards function accessors", () => {
|
||||
function controllerFor(data, options) {
|
||||
const { result } = (0, react_1.renderHook)(() => (0, use_simple_tree_1.useSimpleTree)(data, options));
|
||||
return result.current[1];
|
||||
}
|
||||
const create = { parentId: null, parentNode: null, index: 0 };
|
||||
test("throws when idAccessor is a function", () => {
|
||||
const controller = controllerFor([{ uuid: "1", name: "a" }], { idAccessor: (d) => d.uuid });
|
||||
expect(() => controller.onCreate(Object.assign(Object.assign({}, create), { type: "leaf" }))).toThrow(/idAccessor is a function/);
|
||||
});
|
||||
test("throws when creating a folder with a function childrenAccessor", () => {
|
||||
const controller = controllerFor([{ id: "1", name: "a" }], {
|
||||
childrenAccessor: (d) => d.kids,
|
||||
});
|
||||
expect(() => controller.onCreate(Object.assign(Object.assign({}, create), { type: "internal" }))).toThrow(/childrenAccessor is a function/);
|
||||
});
|
||||
test("a leaf can still be created when only childrenAccessor is a function", () => {
|
||||
const controller = controllerFor([{ id: "1", name: "a" }], {
|
||||
childrenAccessor: (d) => d.kids,
|
||||
});
|
||||
// onCreate calls setData, so run it inside act to keep the suite warning-clean.
|
||||
expect(() => (0, react_1.act)(() => void controller.onCreate(Object.assign(Object.assign({}, create), { type: "leaf" })))).not.toThrow();
|
||||
});
|
||||
});
|
||||
/* A single onChange callback lets callers persist the whole tree after any
|
||||
internal mutation, without wiring up each handler themselves (#302). */
|
||||
describe("useSimpleTree onChange fires on mutations with the latest data (#302)", () => {
|
||||
const initial = [
|
||||
{ id: "a", name: "a" },
|
||||
{ id: "b", name: "b" },
|
||||
];
|
||||
function setup() {
|
||||
const onChange = jest.fn();
|
||||
const { result } = (0, react_1.renderHook)(() => (0, use_simple_tree_1.useSimpleTree)(initial, { onChange }));
|
||||
return { onChange, data: () => result.current[0], controller: () => result.current[1] };
|
||||
}
|
||||
test("does not fire on mount", () => {
|
||||
const { onChange } = setup();
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
test("fires on rename with the exact array the hook now returns", () => {
|
||||
var _a;
|
||||
const { onChange, data, controller } = setup();
|
||||
(0, react_1.act)(() => controller().onRename({ id: "a", name: "renamed", node: null }));
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
const arg = onChange.mock.calls[0][0];
|
||||
expect((_a = arg.find((n) => n.id === "a")) === null || _a === void 0 ? void 0 : _a.name).toBe("renamed");
|
||||
// onChange receives the same reference committed to state, not a fresh copy.
|
||||
expect(data()).toBe(arg);
|
||||
});
|
||||
test("fires on move with the reordered data", () => {
|
||||
const { onChange, controller } = setup();
|
||||
(0, react_1.act)(() => controller().onMove({
|
||||
dragIds: ["a"],
|
||||
parentId: null,
|
||||
index: 2,
|
||||
dragNodes: [],
|
||||
parentNode: null,
|
||||
}));
|
||||
expect(onChange.mock.calls[0][0].map((n) => n.id)).toEqual(["b", "a"]);
|
||||
});
|
||||
test("fires on delete with the remaining data", () => {
|
||||
const { onChange, controller } = setup();
|
||||
(0, react_1.act)(() => controller().onDelete({ ids: ["a"], nodes: [] }));
|
||||
expect(onChange.mock.calls[0][0].map((n) => n.id)).toEqual(["b"]);
|
||||
});
|
||||
test("fires on create with the grown data", () => {
|
||||
const { onChange, controller } = setup();
|
||||
(0, react_1.act)(() => void controller().onCreate({ parentId: null, parentNode: null, index: 0, type: "leaf" }));
|
||||
expect(onChange).toHaveBeenCalledTimes(1);
|
||||
expect(onChange.mock.calls[0][0]).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
buildtime/client/logical-editor/node_modules/react-arborist/dist/main/hooks/use-validated-props.d.ts
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { TreeProps } from "../types/tree-props";
|
||||
export declare function useValidatedProps<T>(props: TreeProps<T>): TreeProps<T>;
|
||||
Generated
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.useValidatedProps = useValidatedProps;
|
||||
const use_simple_tree_1 = require("./use-simple-tree");
|
||||
function useValidatedProps(props) {
|
||||
if (props.initialData && props.data) {
|
||||
throw new Error(`React Arborist Tree => Provide either a data or initialData prop, but not both.`);
|
||||
}
|
||||
if (props.initialData && (props.onCreate || props.onDelete || props.onMove || props.onRename)) {
|
||||
throw new Error(`React Arborist Tree => You passed the initialData prop along with a data handler.
|
||||
Use the data prop if you want to provide your own handlers.`);
|
||||
}
|
||||
if (props.initialData) {
|
||||
/**
|
||||
* Let's break the rules of hooks here. If the initialData prop
|
||||
* is provided, we will assume it will not change for the life of
|
||||
* the component.
|
||||
*
|
||||
* We will provide the real data and the handlers to update it.
|
||||
* */
|
||||
const [data, controller] = (0, use_simple_tree_1.useSimpleTree)(props.initialData, {
|
||||
idAccessor: props.idAccessor,
|
||||
childrenAccessor: props.childrenAccessor,
|
||||
});
|
||||
return Object.assign(Object.assign(Object.assign({}, props), controller), { data });
|
||||
}
|
||||
else {
|
||||
return props;
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
export { Tree } from "./components/tree";
|
||||
export { DropContainer } from "./components/list-outer-element";
|
||||
export { ListOuterElement } from "./components/list-outer-element";
|
||||
export { ListInnerElement } from "./components/list-inner-element";
|
||||
export * from "./types/handlers";
|
||||
export * from "./types/renderers";
|
||||
export * from "./types/state";
|
||||
export * from "./interfaces/node-api";
|
||||
export * from "./interfaces/tree-api";
|
||||
export * from "./data/simple-tree";
|
||||
export { adjustMoveIndex } from "./data/adjust-move-index";
|
||||
export * from "./hooks/use-simple-tree";
|
||||
export { getTreeLinePrefix } from "./utils";
|
||||
export type { TreeLineChars } from "./utils";
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
||||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getTreeLinePrefix = exports.adjustMoveIndex = exports.ListInnerElement = exports.ListOuterElement = exports.DropContainer = exports.Tree = void 0;
|
||||
/* The Public Api */
|
||||
var tree_1 = require("./components/tree");
|
||||
Object.defineProperty(exports, "Tree", { enumerable: true, get: function () { return tree_1.Tree; } });
|
||||
var list_outer_element_1 = require("./components/list-outer-element");
|
||||
Object.defineProperty(exports, "DropContainer", { enumerable: true, get: function () { return list_outer_element_1.DropContainer; } });
|
||||
var list_outer_element_2 = require("./components/list-outer-element");
|
||||
Object.defineProperty(exports, "ListOuterElement", { enumerable: true, get: function () { return list_outer_element_2.ListOuterElement; } });
|
||||
var list_inner_element_1 = require("./components/list-inner-element");
|
||||
Object.defineProperty(exports, "ListInnerElement", { enumerable: true, get: function () { return list_inner_element_1.ListInnerElement; } });
|
||||
__exportStar(require("./types/handlers"), exports);
|
||||
__exportStar(require("./types/renderers"), exports);
|
||||
__exportStar(require("./types/state"), exports);
|
||||
__exportStar(require("./interfaces/node-api"), exports);
|
||||
__exportStar(require("./interfaces/tree-api"), exports);
|
||||
__exportStar(require("./data/simple-tree"), exports);
|
||||
var adjust_move_index_1 = require("./data/adjust-move-index");
|
||||
Object.defineProperty(exports, "adjustMoveIndex", { enumerable: true, get: function () { return adjust_move_index_1.adjustMoveIndex; } });
|
||||
__exportStar(require("./hooks/use-simple-tree"), exports);
|
||||
var utils_1 = require("./utils");
|
||||
Object.defineProperty(exports, "getTreeLinePrefix", { enumerable: true, get: function () { return utils_1.getTreeLinePrefix; } });
|
||||
Generated
Vendored
+72
@@ -0,0 +1,72 @@
|
||||
import React from "react";
|
||||
import { TreeApi } from "./tree-api";
|
||||
type Params<T> = {
|
||||
id: string;
|
||||
data: T;
|
||||
level: number;
|
||||
children: NodeApi<T>[] | null;
|
||||
parent: NodeApi<T> | null;
|
||||
isDraggable: boolean;
|
||||
rowIndex: number | null;
|
||||
tree: TreeApi<T>;
|
||||
};
|
||||
export declare class NodeApi<T = any> {
|
||||
tree: TreeApi<T>;
|
||||
id: string;
|
||||
data: T;
|
||||
level: number;
|
||||
children: NodeApi<T>[] | null;
|
||||
parent: NodeApi<T> | null;
|
||||
isDraggable: boolean;
|
||||
rowIndex: number | null;
|
||||
constructor(params: Params<T>);
|
||||
get isRoot(): boolean;
|
||||
get isLeaf(): boolean;
|
||||
get isInternal(): boolean;
|
||||
get isOpen(): boolean;
|
||||
get isClosed(): boolean;
|
||||
get isEditable(): boolean;
|
||||
get isSelectable(): boolean;
|
||||
get isEditing(): boolean;
|
||||
get isSelected(): boolean;
|
||||
get isOnlySelection(): boolean;
|
||||
get isSelectedStart(): boolean;
|
||||
get isSelectedEnd(): boolean;
|
||||
get isFocused(): boolean;
|
||||
get isDragging(): boolean;
|
||||
get willReceiveDrop(): boolean;
|
||||
get state(): {
|
||||
isClosed: boolean;
|
||||
isDragging: boolean;
|
||||
isEditing: boolean;
|
||||
isFocused: boolean;
|
||||
isInternal: boolean;
|
||||
isLeaf: boolean;
|
||||
isOpen: boolean;
|
||||
isSelected: boolean;
|
||||
isSelectedEnd: boolean;
|
||||
isSelectedStart: boolean;
|
||||
willReceiveDrop: boolean;
|
||||
};
|
||||
get childIndex(): number;
|
||||
get next(): NodeApi<T> | null;
|
||||
get prev(): NodeApi<T> | null;
|
||||
get nextSibling(): NodeApi<T> | null;
|
||||
isAncestorOf(node: NodeApi<T> | null): boolean;
|
||||
select(): void;
|
||||
deselect(): void;
|
||||
selectMulti(): void;
|
||||
selectContiguous(): void;
|
||||
activate(): void;
|
||||
focus(): void;
|
||||
toggle(): void;
|
||||
open(): void;
|
||||
openParents(): void;
|
||||
close(): void;
|
||||
submit(value: string): void;
|
||||
reset(): void;
|
||||
clone(): NodeApi<T>;
|
||||
edit(): Promise<import("..").EditResult>;
|
||||
handleClick: (e: React.MouseEvent) => void;
|
||||
}
|
||||
export {};
|
||||
Generated
Vendored
+187
@@ -0,0 +1,187 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.NodeApi = void 0;
|
||||
const create_root_1 = require("../data/create-root");
|
||||
class NodeApi {
|
||||
constructor(params) {
|
||||
this.handleClick = (e) => {
|
||||
if ((e.metaKey || e.ctrlKey) && !this.tree.props.disableMultiSelection) {
|
||||
if (this.isSelected)
|
||||
this.deselect();
|
||||
else
|
||||
this.selectMulti();
|
||||
}
|
||||
else if (e.shiftKey && !this.tree.props.disableMultiSelection) {
|
||||
this.selectContiguous();
|
||||
}
|
||||
else {
|
||||
this.select();
|
||||
this.activate();
|
||||
}
|
||||
};
|
||||
this.tree = params.tree;
|
||||
this.id = params.id;
|
||||
this.data = params.data;
|
||||
this.level = params.level;
|
||||
this.children = params.children;
|
||||
this.parent = params.parent;
|
||||
this.isDraggable = params.isDraggable;
|
||||
this.rowIndex = params.rowIndex;
|
||||
/* Bind the action methods to the instance so they can be passed as
|
||||
callbacks (e.g. `<Toggle onClick={node.toggle} />`) without losing `this`.
|
||||
A detached call would otherwise read `this.tree` off `undefined` and throw
|
||||
"Cannot read properties of undefined (reading 'tree')" (#301). */
|
||||
this.select = this.select.bind(this);
|
||||
this.deselect = this.deselect.bind(this);
|
||||
this.selectMulti = this.selectMulti.bind(this);
|
||||
this.selectContiguous = this.selectContiguous.bind(this);
|
||||
this.activate = this.activate.bind(this);
|
||||
this.focus = this.focus.bind(this);
|
||||
this.toggle = this.toggle.bind(this);
|
||||
this.open = this.open.bind(this);
|
||||
this.openParents = this.openParents.bind(this);
|
||||
this.close = this.close.bind(this);
|
||||
this.submit = this.submit.bind(this);
|
||||
this.reset = this.reset.bind(this);
|
||||
this.edit = this.edit.bind(this);
|
||||
}
|
||||
get isRoot() {
|
||||
return this.id === create_root_1.ROOT_ID;
|
||||
}
|
||||
get isLeaf() {
|
||||
return !Array.isArray(this.children);
|
||||
}
|
||||
get isInternal() {
|
||||
return !this.isLeaf;
|
||||
}
|
||||
get isOpen() {
|
||||
return this.isLeaf ? false : this.tree.isOpen(this.id);
|
||||
}
|
||||
get isClosed() {
|
||||
return this.isLeaf ? false : !this.tree.isOpen(this.id);
|
||||
}
|
||||
get isEditable() {
|
||||
return this.tree.isEditable(this.data);
|
||||
}
|
||||
get isSelectable() {
|
||||
return this.tree.isSelectable(this.data);
|
||||
}
|
||||
get isEditing() {
|
||||
return this.tree.editingId === this.id;
|
||||
}
|
||||
get isSelected() {
|
||||
return this.tree.isSelected(this.id);
|
||||
}
|
||||
get isOnlySelection() {
|
||||
return this.isSelected && this.tree.hasOneSelection;
|
||||
}
|
||||
get isSelectedStart() {
|
||||
var _a;
|
||||
return this.isSelected && !((_a = this.prev) === null || _a === void 0 ? void 0 : _a.isSelected);
|
||||
}
|
||||
get isSelectedEnd() {
|
||||
var _a;
|
||||
return this.isSelected && !((_a = this.next) === null || _a === void 0 ? void 0 : _a.isSelected);
|
||||
}
|
||||
get isFocused() {
|
||||
return this.tree.isFocused(this.id);
|
||||
}
|
||||
get isDragging() {
|
||||
return this.tree.isDragging(this.id);
|
||||
}
|
||||
get willReceiveDrop() {
|
||||
return this.tree.willReceiveDrop(this.id);
|
||||
}
|
||||
get state() {
|
||||
return {
|
||||
isClosed: this.isClosed,
|
||||
isDragging: this.isDragging,
|
||||
isEditing: this.isEditing,
|
||||
isFocused: this.isFocused,
|
||||
isInternal: this.isInternal,
|
||||
isLeaf: this.isLeaf,
|
||||
isOpen: this.isOpen,
|
||||
isSelected: this.isSelected,
|
||||
isSelectedEnd: this.isSelectedEnd,
|
||||
isSelectedStart: this.isSelectedStart,
|
||||
willReceiveDrop: this.willReceiveDrop,
|
||||
};
|
||||
}
|
||||
get childIndex() {
|
||||
if (this.parent && this.parent.children) {
|
||||
return this.parent.children.findIndex((child) => child.id === this.id);
|
||||
}
|
||||
else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
get next() {
|
||||
if (this.rowIndex === null)
|
||||
return null;
|
||||
return this.tree.at(this.rowIndex + 1);
|
||||
}
|
||||
get prev() {
|
||||
if (this.rowIndex === null)
|
||||
return null;
|
||||
return this.tree.at(this.rowIndex - 1);
|
||||
}
|
||||
get nextSibling() {
|
||||
var _a, _b;
|
||||
const i = this.childIndex;
|
||||
return (_b = (_a = this.parent) === null || _a === void 0 ? void 0 : _a.children[i + 1]) !== null && _b !== void 0 ? _b : null;
|
||||
}
|
||||
isAncestorOf(node) {
|
||||
if (!node)
|
||||
return false;
|
||||
let ancestor = node;
|
||||
while (ancestor) {
|
||||
if (ancestor.id === this.id)
|
||||
return true;
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
select() {
|
||||
this.tree.select(this);
|
||||
}
|
||||
deselect() {
|
||||
this.tree.deselect(this);
|
||||
}
|
||||
selectMulti() {
|
||||
this.tree.selectMulti(this);
|
||||
}
|
||||
selectContiguous() {
|
||||
this.tree.selectContiguous(this);
|
||||
}
|
||||
activate() {
|
||||
this.tree.activate(this);
|
||||
}
|
||||
focus() {
|
||||
this.tree.focus(this);
|
||||
}
|
||||
toggle() {
|
||||
this.tree.toggle(this);
|
||||
}
|
||||
open() {
|
||||
this.tree.open(this);
|
||||
}
|
||||
openParents() {
|
||||
this.tree.openParents(this);
|
||||
}
|
||||
close() {
|
||||
this.tree.close(this);
|
||||
}
|
||||
submit(value) {
|
||||
this.tree.submit(this, value);
|
||||
}
|
||||
reset() {
|
||||
this.tree.reset();
|
||||
}
|
||||
clone() {
|
||||
return new NodeApi(Object.assign({}, this));
|
||||
}
|
||||
edit() {
|
||||
return this.tree.edit(this);
|
||||
}
|
||||
}
|
||||
exports.NodeApi = NodeApi;
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Generated
Vendored
+66
@@ -0,0 +1,66 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const redux_1 = require("redux");
|
||||
const root_reducer_1 = require("../state/root-reducer");
|
||||
const tree_api_1 = require("./tree-api");
|
||||
function setupApi(props) {
|
||||
const store = (0, redux_1.createStore)(root_reducer_1.rootReducer);
|
||||
return new tree_api_1.TreeApi(store, props, { current: null }, { current: null });
|
||||
}
|
||||
const data = [{ id: "folder", children: [{ id: "child" }] }, { id: "leaf" }];
|
||||
/* NodeApi's action methods must survive being passed as bare callbacks — e.g.
|
||||
`<Toggle onClick={node.toggle} />` — without losing `this`. Before they were
|
||||
bound in the constructor, a detached call read `this.tree` off `undefined`
|
||||
and threw "Cannot read properties of undefined (reading 'tree')" (#301). */
|
||||
describe("NodeApi action methods are bound to the instance (#301)", () => {
|
||||
test("a detached node.toggle still toggles the node's open state", () => {
|
||||
const api = setupApi({ data });
|
||||
const node = api.get("folder");
|
||||
expect(node.isOpen).toBe(true); // openByDefault
|
||||
const { toggle } = node; // detach, as a custom renderer passing node.toggle would
|
||||
expect(() => toggle()).not.toThrow();
|
||||
expect(api.isOpen("folder")).toBe(false);
|
||||
toggle();
|
||||
expect(api.isOpen("folder")).toBe(true);
|
||||
});
|
||||
test("a detached node.select still selects the node", () => {
|
||||
const onSelect = jest.fn();
|
||||
const api = setupApi({ data, onSelect });
|
||||
const { select } = api.get("leaf");
|
||||
expect(() => select()).not.toThrow();
|
||||
expect(api.selectedIds.has("leaf")).toBe(true);
|
||||
});
|
||||
test("a detached node.submit still submits a rename (takes an argument)", () => {
|
||||
const onRename = jest.fn();
|
||||
const api = setupApi({ data, onRename });
|
||||
const { submit } = api.get("leaf");
|
||||
expect(() => submit("renamed")).not.toThrow();
|
||||
expect(onRename).toHaveBeenCalledWith(expect.objectContaining({ id: "leaf", name: "renamed" }));
|
||||
});
|
||||
test.each([
|
||||
"select",
|
||||
"deselect",
|
||||
"selectMulti",
|
||||
"selectContiguous",
|
||||
"activate",
|
||||
"focus",
|
||||
"toggle",
|
||||
"open",
|
||||
"openParents",
|
||||
"close",
|
||||
"reset",
|
||||
"submit",
|
||||
"edit",
|
||||
])("node.%s is bound and callable while detached", (method) => {
|
||||
const api = setupApi({ data });
|
||||
const node = api.get("folder");
|
||||
const fn = node[method];
|
||||
// The own, bound copy is a different reference than the prototype method.
|
||||
expect(node[method]).not.toBe(NodeApi_proto(node)[method]);
|
||||
expect(() => fn()).not.toThrow();
|
||||
});
|
||||
});
|
||||
// Reach the prototype to prove the bound method is an own property shadowing it.
|
||||
function NodeApi_proto(node) {
|
||||
return Object.getPrototypeOf(node);
|
||||
}
|
||||
Generated
Vendored
+289
@@ -0,0 +1,289 @@
|
||||
import { EditResult } from "../types/handlers";
|
||||
import { Identity, IdObj } from "../types/utils";
|
||||
import { TreeProps } from "../types/tree-props";
|
||||
import { MutableRefObject } from "react";
|
||||
import { Align, FixedSizeList, ListOnItemsRenderedProps, VariableSizeList } from "react-window";
|
||||
import { DefaultRow } from "../components/default-row";
|
||||
import { DefaultNode } from "../components/default-node";
|
||||
import { NodeApi } from "./node-api";
|
||||
import { Actions, RootState } from "../state/root-reducer";
|
||||
import { DefaultDragPreview } from "../components/default-drag-preview";
|
||||
import { DefaultContainer } from "../components/default-container";
|
||||
import { Cursor } from "../dnd/compute-drop";
|
||||
import type { DropResult } from "../dnd/drop-hook";
|
||||
import { Store } from "redux";
|
||||
export declare class TreeApi<T> {
|
||||
store: Store<RootState, Actions>;
|
||||
props: TreeProps<T>;
|
||||
list: MutableRefObject<FixedSizeList | VariableSizeList | null>;
|
||||
listEl: MutableRefObject<HTMLDivElement | null>;
|
||||
static editPromise: null | ((args: EditResult) => void);
|
||||
root: NodeApi<T>;
|
||||
visibleNodes: NodeApi<T>[];
|
||||
/** Number of nodes matching the current searchTerm (0 when not filtered).
|
||||
* Computed alongside visibleNodes so reading it never re-traverses the tree.
|
||||
* See the filteredCount getter for the consumer-facing contract. */
|
||||
private matchCount;
|
||||
visibleStartIndex: number;
|
||||
visibleStopIndex: number;
|
||||
idToIndex: {
|
||||
[id: string]: number;
|
||||
};
|
||||
private rowOffsets;
|
||||
constructor(store: Store<RootState, Actions>, props: TreeProps<T>, list: MutableRefObject<FixedSizeList | VariableSizeList | null>, listEl: MutableRefObject<HTMLDivElement | null>);
|
||||
update(props: TreeProps<T>): void;
|
||||
dispatch(action: Actions): {
|
||||
type: "FOCUS";
|
||||
id: string | null;
|
||||
} | {
|
||||
readonly type: "TREE_BLUR";
|
||||
} | {
|
||||
type: "EDIT";
|
||||
id: string | null;
|
||||
} | import("../types/utils").ActionTypes<{
|
||||
open(id: string, filtered: boolean): {
|
||||
type: "VISIBILITY_OPEN";
|
||||
id: string;
|
||||
filtered: boolean;
|
||||
};
|
||||
close(id: string, filtered: boolean): {
|
||||
type: "VISIBILITY_CLOSE";
|
||||
id: string;
|
||||
filtered: boolean;
|
||||
};
|
||||
toggle(id: string, filtered: boolean): {
|
||||
type: "VISIBILITY_TOGGLE";
|
||||
id: string;
|
||||
filtered: boolean;
|
||||
};
|
||||
clear(filtered: boolean): {
|
||||
type: "VISIBILITY_CLEAR";
|
||||
filtered: boolean;
|
||||
};
|
||||
}> | import("../types/utils").ActionTypes<{
|
||||
clear: () => {
|
||||
type: "SELECTION_CLEAR";
|
||||
};
|
||||
only: (id: string) => {
|
||||
type: "SELECTION_ONLY";
|
||||
id: string;
|
||||
};
|
||||
add: (id: string | string[]) => {
|
||||
type: "SELECTION_ADD";
|
||||
ids: string[];
|
||||
};
|
||||
remove: (id: string | string[]) => {
|
||||
type: "SELECTION_REMOVE";
|
||||
ids: string[];
|
||||
};
|
||||
set: (args: {
|
||||
ids: Set<string>;
|
||||
anchor: string | null;
|
||||
mostRecent: string | null;
|
||||
}) => {
|
||||
ids: Set<string>;
|
||||
anchor: string | null;
|
||||
mostRecent: string | null;
|
||||
type: "SELECTION_SET";
|
||||
};
|
||||
mostRecent: (id: string | null) => {
|
||||
type: "SELECTION_MOST_RECENT";
|
||||
id: string | null;
|
||||
};
|
||||
anchor: (id: string | null) => {
|
||||
type: "SELECTION_ANCHOR";
|
||||
id: string | null;
|
||||
};
|
||||
}> | import("../types/utils").ActionTypes<{
|
||||
cursor(cursor: Cursor): {
|
||||
type: "DND_CURSOR";
|
||||
cursor: Cursor;
|
||||
};
|
||||
dragStart(id: string, dragIds: string[]): {
|
||||
type: "DND_DRAG_START";
|
||||
id: string;
|
||||
dragIds: string[];
|
||||
};
|
||||
dragEnd(): {
|
||||
type: "DND_DRAG_END";
|
||||
};
|
||||
hovering(parentId: string | null, index: number | null): {
|
||||
type: "DND_HOVERING";
|
||||
parentId: string | null;
|
||||
index: number | null;
|
||||
};
|
||||
setDestination(parentId: string | null, index: number | null): {
|
||||
type: "DND_DESTINATION";
|
||||
parentId: string | null;
|
||||
index: number | null;
|
||||
};
|
||||
}>;
|
||||
get state(): {
|
||||
nodes: {
|
||||
focus: import("../state/focus-slice").FocusState;
|
||||
edit: import("../state/edit-slice").EditState;
|
||||
open: import("../state/open-slice").OpenSlice;
|
||||
selection: import("../state/selection-slice").SelectionState;
|
||||
drag: import("../state/drag-slice").DragSlice;
|
||||
};
|
||||
dnd: import("../state/dnd-slice").DndState;
|
||||
};
|
||||
get openState(): import("../state/open-slice").OpenMap;
|
||||
get width(): string | number;
|
||||
get height(): number;
|
||||
get indent(): number;
|
||||
/**
|
||||
* The fixed row height. When a `rowHeight` function is supplied for variable
|
||||
* heights, this returns the default (24); use `rowHeightAt(index)` to get the
|
||||
* height of a specific row.
|
||||
*/
|
||||
get rowHeight(): number;
|
||||
/**
|
||||
* The height of the row at `index`, evaluating the `rowHeight` function if
|
||||
* given. Falls back to the default height for an out-of-range index so this
|
||||
* never feeds an invalid `0` to react-window's `itemSize`.
|
||||
*/
|
||||
rowHeightAt: (index: number) => number;
|
||||
/** The pixel offset of the top of the row at `index` from the top of the list. */
|
||||
rowTopPosition: (index: number) => number;
|
||||
/**
|
||||
* Tell the underlying virtualized list to recompute row heights at and after
|
||||
* `index`. Call this if a `rowHeight` function's output changes for reasons
|
||||
* the tree can't observe (e.g. external state).
|
||||
*/
|
||||
redrawList: (afterIndex?: number) => void;
|
||||
/** Lazily-built prefix sum where offsets[i] is the top of row i. */
|
||||
private getRowOffsets;
|
||||
get overscanCount(): number;
|
||||
get searchTerm(): string;
|
||||
get matchFn(): (node: NodeApi<T>) => boolean;
|
||||
accessChildren(data: T): readonly T[] | null;
|
||||
accessId(data: T): string;
|
||||
/**
|
||||
* Resolve an identifier to a node id. Public methods accept an id string, a
|
||||
* NodeApi, or the raw row data; this is the one place that turns any of those
|
||||
* into the string id used internally. Raw data is run through the configured
|
||||
* `idAccessor` so a custom accessor (e.g. `uuid`) is honored everywhere, not
|
||||
* just where nodes were built. A NodeApi already carries its accessor-derived
|
||||
* `id`, so it is used directly rather than re-accessed (the accessor reads the
|
||||
* underlying data, which a NodeApi does not expose under that key). Unlike
|
||||
* `accessId`, an unresolved id comes back as `undefined` rather than throwing,
|
||||
* preserving the previous behavior of the `id`-only lookup.
|
||||
*/
|
||||
identify(identity: string | IdObj | T): string;
|
||||
identifyNull(identity: Identity | T): string | null;
|
||||
get firstNode(): NodeApi<T>;
|
||||
get lastNode(): NodeApi<T>;
|
||||
get focusedNode(): NodeApi<T> | null;
|
||||
get mostRecentNode(): NodeApi<T> | null;
|
||||
get nextNode(): NodeApi<T> | null;
|
||||
get prevNode(): NodeApi<T> | null;
|
||||
get(id: string | null): NodeApi<T> | null;
|
||||
at(index: number): NodeApi<T> | null;
|
||||
nodesBetween(startId: string | null, endId: string | null): NodeApi<T>[];
|
||||
indexOf(id: Identity | T): number | null;
|
||||
get editingId(): string | null;
|
||||
createInternal(): Promise<void>;
|
||||
createLeaf(): Promise<void>;
|
||||
create(opts?: {
|
||||
type?: "internal" | "leaf";
|
||||
parentId?: null | string;
|
||||
index?: null | number;
|
||||
}): Promise<void>;
|
||||
delete(node: Identity | T | (string | IdObj | T)[]): Promise<void>;
|
||||
edit(node: string | IdObj | T): Promise<EditResult>;
|
||||
submit(identity: Identity | T, value: string): Promise<void>;
|
||||
reset(): void;
|
||||
activate(id: Identity | T): void;
|
||||
private resolveEdit;
|
||||
get selectedIds(): Set<string>;
|
||||
get selectedNodes(): NodeApi<T>[];
|
||||
focus(node: Identity | T, opts?: {
|
||||
scroll?: boolean;
|
||||
}): void;
|
||||
pageUp(): void;
|
||||
pageDown(): void;
|
||||
select(node: Identity | T, opts?: {
|
||||
align?: Align;
|
||||
focus?: boolean;
|
||||
}): void;
|
||||
deselect(node: Identity | T): void;
|
||||
selectMulti(identity: Identity | T, opts?: {
|
||||
align?: Align;
|
||||
focus?: boolean;
|
||||
}): void;
|
||||
selectContiguous(identity: Identity | T): void;
|
||||
deselectAll(): void;
|
||||
selectAll(): void;
|
||||
private filterSelectableNodes;
|
||||
setSelection(args: {
|
||||
ids: (IdObj | string | T)[] | null;
|
||||
anchor: Identity | T;
|
||||
mostRecent: Identity | T;
|
||||
}): void;
|
||||
get cursorParentId(): string | null;
|
||||
get cursorOverFolder(): boolean;
|
||||
get dragNodes(): NodeApi<T>[];
|
||||
get dragNode(): NodeApi<T> | null;
|
||||
get dragDestinationParent(): NodeApi<T> | null;
|
||||
get dragDestinationIndex(): number | null;
|
||||
canDrop(): boolean;
|
||||
hover(drop: DropResult | null, cursor: Cursor | null): void;
|
||||
drop(): void;
|
||||
hideCursor(): void;
|
||||
showCursor(cursor: Cursor): void;
|
||||
open(identity: Identity | T, redraw?: boolean): void;
|
||||
close(identity: Identity | T, redraw?: boolean): void;
|
||||
toggle(identity: Identity | T): void;
|
||||
openParents(identity: Identity | T): void;
|
||||
openSiblings(node: NodeApi<T>): void;
|
||||
openAll(): void;
|
||||
closeAll(): void;
|
||||
scrollTo(identity: Identity | T, align?: Align): Promise<void> | undefined;
|
||||
/**
|
||||
* Horizontally scroll the list so the node's indented content is in view.
|
||||
* A no-op when the list doesn't overflow horizontally (the common case), so
|
||||
* it never disturbs scrolling for trees that fit their width.
|
||||
*/
|
||||
private scrollToNodeHorizontally;
|
||||
/**
|
||||
* Scroll the list vertically to an exact pixel offset from the top. This is
|
||||
* the offset-based counterpart to scrollTo(), handy for saving and restoring
|
||||
* a scroll position (#194). Negative values are clamped to the top; react-
|
||||
* window clamps the upper bound to the scrollable range.
|
||||
*/
|
||||
scrollToOffset(offset: number): void;
|
||||
/** The list's current vertical scroll offset, in pixels from the top. */
|
||||
get scrollOffset(): number;
|
||||
get isEditing(): boolean;
|
||||
get isFiltered(): boolean;
|
||||
/** The number of nodes matching the current search term, counted across the
|
||||
* whole tree regardless of which folders are open. Returns 0 when there is no
|
||||
* active search. Consumers use this to render match counts or a "no results"
|
||||
* message (#112, #256). Ancestors shown only to keep the tree's structure
|
||||
* intact are not counted. The count is computed once when the visible list is
|
||||
* built (see createList), so reading it never re-traverses the tree. */
|
||||
get filteredCount(): number;
|
||||
get hasFocus(): boolean;
|
||||
get hasNoSelection(): boolean;
|
||||
get hasOneSelection(): boolean;
|
||||
get hasMultipleSelections(): boolean;
|
||||
isSelected(id?: string): boolean;
|
||||
isOpen(id?: string): boolean;
|
||||
isEditable(data: T): boolean;
|
||||
isDraggable(data: T): boolean;
|
||||
isSelectable(data: T): boolean;
|
||||
private isActionPossible;
|
||||
isDragging(node: Identity | T): boolean;
|
||||
isFocused(id: string): boolean;
|
||||
isMatch(node: NodeApi<T>): boolean;
|
||||
willReceiveDrop(node: Identity | T): boolean;
|
||||
onFocus(): void;
|
||||
onBlur(): void;
|
||||
onItemsRendered(args: ListOnItemsRenderedProps): void;
|
||||
get renderContainer(): import("react").ElementType<{}> | typeof DefaultContainer;
|
||||
get renderRow(): import("react").ElementType<import("..").RowRendererProps<T>> | typeof DefaultRow;
|
||||
get renderNode(): import("react").ElementType<import("..").NodeRendererProps<T>> | typeof DefaultNode;
|
||||
get renderDragPreview(): import("react").ElementType<import("..").DragPreviewProps> | typeof DefaultDragPreview;
|
||||
get renderCursor(): import("react").ElementType<import("..").CursorProps> | import("react").NamedExoticComponent<import("..").CursorProps>;
|
||||
}
|
||||
Generated
Vendored
+880
@@ -0,0 +1,880 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.TreeApi = void 0;
|
||||
const utils = __importStar(require("../utils"));
|
||||
const default_cursor_1 = require("../components/default-cursor");
|
||||
const default_row_1 = require("../components/default-row");
|
||||
const default_node_1 = require("../components/default-node");
|
||||
const node_api_1 = require("./node-api");
|
||||
const edit_slice_1 = require("../state/edit-slice");
|
||||
const focus_slice_1 = require("../state/focus-slice");
|
||||
const create_root_1 = require("../data/create-root");
|
||||
const open_slice_1 = require("../state/open-slice");
|
||||
const selection_slice_1 = require("../state/selection-slice");
|
||||
const dnd_slice_1 = require("../state/dnd-slice");
|
||||
const default_drag_preview_1 = require("../components/default-drag-preview");
|
||||
const default_container_1 = require("../components/default-container");
|
||||
const create_list_1 = require("../data/create-list");
|
||||
const create_index_1 = require("../data/create-index");
|
||||
const { safeRun } = utils;
|
||||
class TreeApi {
|
||||
constructor(store, props, list, listEl) {
|
||||
this.store = store;
|
||||
this.props = props;
|
||||
this.list = list;
|
||||
this.listEl = listEl;
|
||||
/** Number of nodes matching the current searchTerm (0 when not filtered).
|
||||
* Computed alongside visibleNodes so reading it never re-traverses the tree.
|
||||
* See the filteredCount getter for the consumer-facing contract. */
|
||||
this.matchCount = 0;
|
||||
this.visibleStartIndex = 0;
|
||||
this.visibleStopIndex = 0;
|
||||
/* Memoized prefix-sum of row heights; only used for variable heights. */
|
||||
this.rowOffsets = null;
|
||||
/**
|
||||
* The height of the row at `index`, evaluating the `rowHeight` function if
|
||||
* given. Falls back to the default height for an out-of-range index so this
|
||||
* never feeds an invalid `0` to react-window's `itemSize`.
|
||||
*/
|
||||
this.rowHeightAt = (index) => {
|
||||
const rowHeight = this.props.rowHeight;
|
||||
if (typeof rowHeight === "function") {
|
||||
const node = this.at(index);
|
||||
return node ? rowHeight(node) : this.rowHeight;
|
||||
}
|
||||
return rowHeight !== null && rowHeight !== void 0 ? rowHeight : 24;
|
||||
};
|
||||
/** The pixel offset of the top of the row at `index` from the top of the list. */
|
||||
this.rowTopPosition = (index) => {
|
||||
/* Fixed heights: O(1). */
|
||||
if (typeof this.props.rowHeight !== "function") {
|
||||
return index * this.rowHeight;
|
||||
}
|
||||
/* Variable heights: O(1) amortized via a memoized prefix sum. */
|
||||
const offsets = this.getRowOffsets();
|
||||
const clamped = Math.max(0, Math.min(index, offsets.length - 1));
|
||||
return offsets[clamped];
|
||||
};
|
||||
/**
|
||||
* Tell the underlying virtualized list to recompute row heights at and after
|
||||
* `index`. Call this if a `rowHeight` function's output changes for reasons
|
||||
* the tree can't observe (e.g. external state).
|
||||
*/
|
||||
this.redrawList = (afterIndex = 0) => {
|
||||
this.rowOffsets = null;
|
||||
/* Only the VariableSizeList (function rowHeight) caches measurements; a
|
||||
FixedSizeList has constant heights and nothing to recompute. */
|
||||
const list = this.list.current;
|
||||
if (list && "resetAfterIndex" in list) {
|
||||
list.resetAfterIndex(Math.max(0, afterIndex));
|
||||
}
|
||||
};
|
||||
/* Changes here must also be made in update() */
|
||||
this.root = (0, create_root_1.createRoot)(this);
|
||||
const { list: visibleNodes, matchCount } = (0, create_list_1.createList)(this);
|
||||
this.visibleNodes = visibleNodes;
|
||||
this.matchCount = matchCount;
|
||||
this.idToIndex = (0, create_index_1.createIndex)(this.visibleNodes);
|
||||
}
|
||||
/* Changes here must also be made in constructor() */
|
||||
update(props) {
|
||||
this.props = props;
|
||||
this.root = (0, create_root_1.createRoot)(this);
|
||||
const { list: visibleNodes, matchCount } = (0, create_list_1.createList)(this);
|
||||
this.visibleNodes = visibleNodes;
|
||||
this.matchCount = matchCount;
|
||||
this.idToIndex = (0, create_index_1.createIndex)(this.visibleNodes);
|
||||
this.rowOffsets = null;
|
||||
/* Variable-height mode renders a VariableSizeList, which caches item
|
||||
measurements by index and never invalidates them on its own. When the
|
||||
visible nodes change (insert/remove/reorder), those cached sizes belong
|
||||
to the wrong rows, so drop them. Fixed-height mode renders a
|
||||
FixedSizeList (no cache, nothing to reset). update() runs during render,
|
||||
so pass shouldForceUpdate=false: the in-progress render repaints the list
|
||||
and a forceUpdate here would warn about setting state mid-render. */
|
||||
const list = this.list.current;
|
||||
if (list && "resetAfterIndex" in list) {
|
||||
list.resetAfterIndex(0, false);
|
||||
}
|
||||
}
|
||||
/* Store helpers */
|
||||
dispatch(action) {
|
||||
return this.store.dispatch(action);
|
||||
}
|
||||
get state() {
|
||||
return this.store.getState();
|
||||
}
|
||||
get openState() {
|
||||
return this.state.nodes.open.unfiltered;
|
||||
}
|
||||
/* Tree Props */
|
||||
get width() {
|
||||
var _a;
|
||||
return (_a = this.props.width) !== null && _a !== void 0 ? _a : 300;
|
||||
}
|
||||
get height() {
|
||||
var _a;
|
||||
return (_a = this.props.height) !== null && _a !== void 0 ? _a : 500;
|
||||
}
|
||||
get indent() {
|
||||
var _a;
|
||||
return (_a = this.props.indent) !== null && _a !== void 0 ? _a : 24;
|
||||
}
|
||||
/**
|
||||
* The fixed row height. When a `rowHeight` function is supplied for variable
|
||||
* heights, this returns the default (24); use `rowHeightAt(index)` to get the
|
||||
* height of a specific row.
|
||||
*/
|
||||
get rowHeight() {
|
||||
return typeof this.props.rowHeight === "number" ? this.props.rowHeight : 24;
|
||||
}
|
||||
/** Lazily-built prefix sum where offsets[i] is the top of row i. */
|
||||
getRowOffsets() {
|
||||
if (this.rowOffsets)
|
||||
return this.rowOffsets;
|
||||
const offsets = [0];
|
||||
for (let i = 0; i < this.visibleNodes.length; i++) {
|
||||
offsets.push(offsets[i] + this.rowHeightAt(i));
|
||||
}
|
||||
this.rowOffsets = offsets;
|
||||
return offsets;
|
||||
}
|
||||
get overscanCount() {
|
||||
var _a;
|
||||
return (_a = this.props.overscanCount) !== null && _a !== void 0 ? _a : 1;
|
||||
}
|
||||
get searchTerm() {
|
||||
return (this.props.searchTerm || "").trim();
|
||||
}
|
||||
get matchFn() {
|
||||
var _a;
|
||||
const match = (_a = this.props.searchMatch) !== null && _a !== void 0 ? _a : ((node, term) => {
|
||||
/* Search a node's own fields only. Including the children array would
|
||||
make every ancestor of a match match too, inflating filteredCount and
|
||||
matching folders on nested keys like "id" (#112). Ancestors are still
|
||||
kept in the filtered list to preserve the tree's structure. */
|
||||
const children = this.accessChildren(node.data);
|
||||
const values = Object.values(node.data).filter((value) => children === null || value !== children);
|
||||
const string = JSON.stringify(values);
|
||||
return string.toLocaleLowerCase().includes(term.toLocaleLowerCase());
|
||||
});
|
||||
return (node) => match(node, this.searchTerm);
|
||||
}
|
||||
accessChildren(data) {
|
||||
var _a;
|
||||
const get = this.props.childrenAccessor || "children";
|
||||
return (_a = utils.access(data, get)) !== null && _a !== void 0 ? _a : null;
|
||||
}
|
||||
accessId(data) {
|
||||
const get = this.props.idAccessor || "id";
|
||||
const id = utils.access(data, get);
|
||||
if (!id)
|
||||
throw new Error("Data must contain an 'id' property or props.idAccessor must return a string");
|
||||
return id;
|
||||
}
|
||||
/**
|
||||
* Resolve an identifier to a node id. Public methods accept an id string, a
|
||||
* NodeApi, or the raw row data; this is the one place that turns any of those
|
||||
* into the string id used internally. Raw data is run through the configured
|
||||
* `idAccessor` so a custom accessor (e.g. `uuid`) is honored everywhere, not
|
||||
* just where nodes were built. A NodeApi already carries its accessor-derived
|
||||
* `id`, so it is used directly rather than re-accessed (the accessor reads the
|
||||
* underlying data, which a NodeApi does not expose under that key). Unlike
|
||||
* `accessId`, an unresolved id comes back as `undefined` rather than throwing,
|
||||
* preserving the previous behavior of the `id`-only lookup.
|
||||
*/
|
||||
identify(identity) {
|
||||
if (typeof identity === "string")
|
||||
return identity;
|
||||
if (identity instanceof node_api_1.NodeApi)
|
||||
return identity.id;
|
||||
const get = this.props.idAccessor || "id";
|
||||
return utils.access(identity, get);
|
||||
}
|
||||
identifyNull(identity) {
|
||||
if (identity === null || identity === undefined)
|
||||
return null;
|
||||
return this.identify(identity);
|
||||
}
|
||||
/* Node Access */
|
||||
get firstNode() {
|
||||
var _a;
|
||||
return (_a = this.visibleNodes[0]) !== null && _a !== void 0 ? _a : null;
|
||||
}
|
||||
get lastNode() {
|
||||
var _a;
|
||||
return (_a = this.visibleNodes[this.visibleNodes.length - 1]) !== null && _a !== void 0 ? _a : null;
|
||||
}
|
||||
get focusedNode() {
|
||||
var _a;
|
||||
return (_a = this.get(this.state.nodes.focus.id)) !== null && _a !== void 0 ? _a : null;
|
||||
}
|
||||
get mostRecentNode() {
|
||||
var _a;
|
||||
return (_a = this.get(this.state.nodes.selection.mostRecent)) !== null && _a !== void 0 ? _a : null;
|
||||
}
|
||||
get nextNode() {
|
||||
const index = this.indexOf(this.focusedNode);
|
||||
if (index === null)
|
||||
return null;
|
||||
else
|
||||
return this.at(index + 1);
|
||||
}
|
||||
get prevNode() {
|
||||
const index = this.indexOf(this.focusedNode);
|
||||
if (index === null)
|
||||
return null;
|
||||
else
|
||||
return this.at(index - 1);
|
||||
}
|
||||
get(id) {
|
||||
if (!id)
|
||||
return null;
|
||||
if (id in this.idToIndex)
|
||||
return this.visibleNodes[this.idToIndex[id]] || null;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
at(index) {
|
||||
return this.visibleNodes[index] || null;
|
||||
}
|
||||
nodesBetween(startId, endId) {
|
||||
var _a;
|
||||
if (startId === null || endId === null)
|
||||
return [];
|
||||
const index1 = (_a = this.indexOf(startId)) !== null && _a !== void 0 ? _a : 0;
|
||||
const index2 = this.indexOf(endId);
|
||||
if (index2 === null)
|
||||
return [];
|
||||
const start = Math.min(index1, index2);
|
||||
const end = Math.max(index1, index2);
|
||||
return this.visibleNodes.slice(start, end + 1);
|
||||
}
|
||||
indexOf(id) {
|
||||
const key = this.identifyNull(id);
|
||||
if (!key)
|
||||
return null;
|
||||
return this.idToIndex[key];
|
||||
}
|
||||
/* Data Operations */
|
||||
get editingId() {
|
||||
return this.state.nodes.edit.id;
|
||||
}
|
||||
createInternal() {
|
||||
return this.create({ type: "internal" });
|
||||
}
|
||||
createLeaf() {
|
||||
return this.create({ type: "leaf" });
|
||||
}
|
||||
create() {
|
||||
return __awaiter(this, arguments, void 0, function* (opts = {}) {
|
||||
var _a, _b;
|
||||
const parentId = opts.parentId === undefined ? utils.getInsertParentId(this) : opts.parentId;
|
||||
const index = (_a = opts.index) !== null && _a !== void 0 ? _a : utils.getInsertIndex(this);
|
||||
const type = (_b = opts.type) !== null && _b !== void 0 ? _b : "leaf";
|
||||
const data = yield safeRun(this.props.onCreate, {
|
||||
type,
|
||||
parentId,
|
||||
index,
|
||||
parentNode: this.get(parentId),
|
||||
});
|
||||
if (data) {
|
||||
this.focus(data);
|
||||
setTimeout(() => {
|
||||
this.edit(data).then(() => {
|
||||
this.select(data);
|
||||
this.activate(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
delete(node) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
if (!node)
|
||||
return;
|
||||
const idents = Array.isArray(node) ? node : [node];
|
||||
const ids = idents.map((i) => this.identify(i));
|
||||
const nodes = ids.map((id) => this.get(id)).filter((n) => !!n);
|
||||
/* Guard against Math.min(...[]) === Infinity when no ids resolve to nodes. */
|
||||
const fromIndex = nodes.length ? Math.min(...nodes.map((n) => { var _a; return (_a = n.rowIndex) !== null && _a !== void 0 ? _a : 0; })) : 0;
|
||||
yield safeRun(this.props.onDelete, { nodes, ids });
|
||||
this.redrawList(fromIndex);
|
||||
});
|
||||
}
|
||||
edit(node) {
|
||||
var _a, _b;
|
||||
const id = this.identify(node);
|
||||
this.resolveEdit({ cancelled: true });
|
||||
this.scrollTo(id);
|
||||
this.dispatch((0, edit_slice_1.edit)(id));
|
||||
this.redrawList((_b = (_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0);
|
||||
return new Promise((resolve) => {
|
||||
TreeApi.editPromise = resolve;
|
||||
});
|
||||
}
|
||||
submit(identity, value) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
var _a, _b;
|
||||
if (!identity)
|
||||
return;
|
||||
const id = this.identify(identity);
|
||||
yield safeRun(this.props.onRename, {
|
||||
id,
|
||||
name: value,
|
||||
node: this.get(id),
|
||||
});
|
||||
this.dispatch((0, edit_slice_1.edit)(null));
|
||||
this.resolveEdit({ cancelled: false, value });
|
||||
this.redrawList((_b = (_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0);
|
||||
setTimeout(() => this.onFocus()); // Return focus to element;
|
||||
});
|
||||
}
|
||||
reset() {
|
||||
this.dispatch((0, edit_slice_1.edit)(null));
|
||||
this.resolveEdit({ cancelled: true });
|
||||
this.redrawList();
|
||||
setTimeout(() => this.onFocus()); // Return focus to element;
|
||||
}
|
||||
activate(id) {
|
||||
const node = this.get(this.identifyNull(id));
|
||||
if (!node)
|
||||
return;
|
||||
safeRun(this.props.onActivate, node);
|
||||
}
|
||||
resolveEdit(value) {
|
||||
const resolve = TreeApi.editPromise;
|
||||
if (resolve)
|
||||
resolve(value);
|
||||
TreeApi.editPromise = null;
|
||||
}
|
||||
/* Focus and Selection */
|
||||
get selectedIds() {
|
||||
return this.state.nodes.selection.ids;
|
||||
}
|
||||
get selectedNodes() {
|
||||
let nodes = [];
|
||||
for (let id of Array.from(this.selectedIds)) {
|
||||
const node = this.get(id);
|
||||
if (node)
|
||||
nodes.push(node);
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
focus(node, opts = {}) {
|
||||
if (!node)
|
||||
return;
|
||||
/* Focus is responsible for scrolling, while selection is
|
||||
* responsible for focus. If selectionFollowsFocus, then
|
||||
* just select it. */
|
||||
if (this.props.selectionFollowsFocus) {
|
||||
this.select(node);
|
||||
}
|
||||
else {
|
||||
this.dispatch((0, focus_slice_1.focus)(this.identify(node)));
|
||||
if (opts.scroll !== false)
|
||||
this.scrollTo(node);
|
||||
if (this.focusedNode)
|
||||
safeRun(this.props.onFocus, this.focusedNode);
|
||||
}
|
||||
}
|
||||
pageUp() {
|
||||
var _a, _b;
|
||||
const start = this.visibleStartIndex;
|
||||
const stop = this.visibleStopIndex;
|
||||
const page = stop - start;
|
||||
let index = (_b = (_a = this.focusedNode) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0;
|
||||
if (index > start) {
|
||||
index = start;
|
||||
}
|
||||
else {
|
||||
index = Math.max(start - page, 0);
|
||||
}
|
||||
this.focus(this.at(index));
|
||||
}
|
||||
pageDown() {
|
||||
var _a, _b;
|
||||
const start = this.visibleStartIndex;
|
||||
const stop = this.visibleStopIndex;
|
||||
const page = stop - start;
|
||||
let index = (_b = (_a = this.focusedNode) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0;
|
||||
if (index < stop) {
|
||||
index = stop;
|
||||
}
|
||||
else {
|
||||
index = Math.min(index + page, this.visibleNodes.length - 1);
|
||||
}
|
||||
this.focus(this.at(index));
|
||||
}
|
||||
select(node, opts = {}) {
|
||||
var _a;
|
||||
if (!node)
|
||||
return;
|
||||
const changeFocus = opts.focus !== false;
|
||||
const id = this.identify(node);
|
||||
if (changeFocus)
|
||||
this.dispatch((0, focus_slice_1.focus)(id));
|
||||
if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
|
||||
this.setSelection({
|
||||
ids: [id],
|
||||
anchor: id,
|
||||
mostRecent: id,
|
||||
});
|
||||
}
|
||||
this.scrollTo(id, opts.align);
|
||||
if (this.focusedNode && changeFocus) {
|
||||
safeRun(this.props.onFocus, this.focusedNode);
|
||||
}
|
||||
}
|
||||
deselect(node) {
|
||||
if (!node)
|
||||
return;
|
||||
const id = this.identify(node);
|
||||
this.dispatch(selection_slice_1.actions.remove(id));
|
||||
safeRun(this.props.onSelect, this.selectedNodes);
|
||||
}
|
||||
selectMulti(identity, opts = {}) {
|
||||
const node = this.get(this.identifyNull(identity));
|
||||
if (!node)
|
||||
return;
|
||||
const changeFocus = opts.focus !== false;
|
||||
if (changeFocus)
|
||||
this.dispatch((0, focus_slice_1.focus)(node.id));
|
||||
if (node.isSelectable) {
|
||||
this.dispatch(selection_slice_1.actions.add(node.id));
|
||||
this.dispatch(selection_slice_1.actions.anchor(node.id));
|
||||
this.dispatch(selection_slice_1.actions.mostRecent(node.id));
|
||||
}
|
||||
this.scrollTo(node, opts.align);
|
||||
if (this.focusedNode && changeFocus) {
|
||||
safeRun(this.props.onFocus, this.focusedNode);
|
||||
}
|
||||
safeRun(this.props.onSelect, this.selectedNodes);
|
||||
}
|
||||
selectContiguous(identity) {
|
||||
var _a;
|
||||
if (!identity)
|
||||
return;
|
||||
const id = this.identify(identity);
|
||||
this.dispatch((0, focus_slice_1.focus)(id));
|
||||
if ((_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.isSelectable) {
|
||||
const { anchor, mostRecent } = this.state.nodes.selection;
|
||||
const selectableNodes = this.filterSelectableNodes(this.nodesBetween(anchor, this.identifyNull(id)));
|
||||
this.dispatch(selection_slice_1.actions.remove(this.nodesBetween(anchor, mostRecent).map((n) => n.id)));
|
||||
this.dispatch(selection_slice_1.actions.add(selectableNodes.map((n) => n.id)));
|
||||
this.dispatch(selection_slice_1.actions.mostRecent(id));
|
||||
}
|
||||
this.scrollTo(id);
|
||||
if (this.focusedNode)
|
||||
safeRun(this.props.onFocus, this.focusedNode);
|
||||
safeRun(this.props.onSelect, this.selectedNodes);
|
||||
}
|
||||
deselectAll() {
|
||||
// setSelection fires onSelect; don't fire it again here (see #332).
|
||||
this.setSelection({ ids: [], anchor: null, mostRecent: null });
|
||||
}
|
||||
selectAll() {
|
||||
var _a, _b, _c;
|
||||
const allSelectableNodes = this.filterSelectableNodes(Object.keys(this.idToIndex));
|
||||
// setSelection fires onSelect; don't fire it again here (see #332).
|
||||
this.setSelection({
|
||||
ids: allSelectableNodes,
|
||||
anchor: (_a = allSelectableNodes[0]) !== null && _a !== void 0 ? _a : null,
|
||||
mostRecent: (_b = allSelectableNodes[allSelectableNodes.length - 1]) !== null && _b !== void 0 ? _b : null,
|
||||
});
|
||||
this.dispatch((0, focus_slice_1.focus)((_c = this.lastNode) === null || _c === void 0 ? void 0 : _c.id));
|
||||
if (this.focusedNode)
|
||||
safeRun(this.props.onFocus, this.focusedNode);
|
||||
}
|
||||
filterSelectableNodes(nodes) {
|
||||
return nodes
|
||||
.map((n) => this.get(this.identify(n)))
|
||||
.filter((n) => !!n && n.isSelectable);
|
||||
}
|
||||
setSelection(args) {
|
||||
var _a;
|
||||
const ids = new Set((_a = args.ids) === null || _a === void 0 ? void 0 : _a.map((i) => this.identify(i)));
|
||||
const anchor = this.identifyNull(args.anchor);
|
||||
const mostRecent = this.identifyNull(args.mostRecent);
|
||||
this.dispatch(selection_slice_1.actions.set({ ids, anchor, mostRecent }));
|
||||
safeRun(this.props.onSelect, this.selectedNodes);
|
||||
}
|
||||
/* Drag and Drop */
|
||||
get cursorParentId() {
|
||||
const { cursor } = this.state.dnd;
|
||||
switch (cursor.type) {
|
||||
case "highlight":
|
||||
return cursor.id;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
get cursorOverFolder() {
|
||||
return this.state.dnd.cursor.type === "highlight";
|
||||
}
|
||||
get dragNodes() {
|
||||
return this.state.dnd.dragIds.map((id) => this.get(id)).filter((n) => !!n);
|
||||
}
|
||||
get dragNode() {
|
||||
return this.get(this.state.nodes.drag.id);
|
||||
}
|
||||
get dragDestinationParent() {
|
||||
return this.get(this.state.nodes.drag.destinationParentId);
|
||||
}
|
||||
get dragDestinationIndex() {
|
||||
return this.state.nodes.drag.destinationIndex;
|
||||
}
|
||||
canDrop() {
|
||||
var _a;
|
||||
if (this.isFiltered)
|
||||
return false;
|
||||
const parentNode = (_a = this.get(this.state.dnd.parentId)) !== null && _a !== void 0 ? _a : this.root;
|
||||
const dragNodes = this.dragNodes;
|
||||
const isDisabled = this.props.disableDrop;
|
||||
for (const drag of dragNodes) {
|
||||
if (!drag)
|
||||
return false;
|
||||
if (!parentNode)
|
||||
return false;
|
||||
if (drag.isInternal && utils.isDescendant(parentNode, drag))
|
||||
return false;
|
||||
}
|
||||
// Allow the user to insert their own logic
|
||||
if (typeof isDisabled == "function") {
|
||||
return !isDisabled({
|
||||
parentNode,
|
||||
dragNodes: this.dragNodes,
|
||||
index: this.state.dnd.index || 0,
|
||||
});
|
||||
}
|
||||
else if (typeof isDisabled == "string") {
|
||||
// @ts-ignore
|
||||
return !parentNode.data[isDisabled];
|
||||
}
|
||||
else if (typeof isDisabled === "boolean") {
|
||||
return !isDisabled;
|
||||
}
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
/* Called by the drop hooks on every hover. Records the computed target for
|
||||
the drop guard (canDrop() and drop() read state.dnd.parentId), then — only
|
||||
when that target is actually droppable — surfaces it to consumers
|
||||
(willReceiveDrop, dragDestinationParent) and shows the cursor. When it
|
||||
isn't droppable, the consumer-facing destination and the cursor are both
|
||||
cleared so they never disagree with canDrop() (#247); the guard still sees
|
||||
the real target, so releasing over an invalid spot is rejected rather than
|
||||
falling back to a root drop. */
|
||||
hover(drop, cursor) {
|
||||
if (drop)
|
||||
this.dispatch(dnd_slice_1.actions.hovering(drop.parentId, drop.index));
|
||||
if (drop && this.canDrop()) {
|
||||
this.dispatch(dnd_slice_1.actions.setDestination(drop.parentId, drop.index));
|
||||
if (cursor)
|
||||
this.showCursor(cursor);
|
||||
}
|
||||
else {
|
||||
this.dispatch(dnd_slice_1.actions.setDestination(null, null));
|
||||
this.hideCursor();
|
||||
}
|
||||
}
|
||||
drop() {
|
||||
const { parentId, index, dragIds } = this.state.dnd;
|
||||
safeRun(this.props.onMove, {
|
||||
dragIds,
|
||||
parentId: parentId === create_root_1.ROOT_ID ? null : parentId,
|
||||
index: index === null ? 0 : index, // When it's null it was dropped over a folder
|
||||
dragNodes: this.dragNodes,
|
||||
parentNode: this.get(parentId),
|
||||
});
|
||||
this.open(parentId);
|
||||
}
|
||||
hideCursor() {
|
||||
this.dispatch(dnd_slice_1.actions.cursor({ type: "none" }));
|
||||
}
|
||||
showCursor(cursor) {
|
||||
this.dispatch(dnd_slice_1.actions.cursor(cursor));
|
||||
}
|
||||
/* Visibility */
|
||||
open(identity, redraw = true) {
|
||||
var _a, _b;
|
||||
const id = this.identifyNull(identity);
|
||||
if (!id)
|
||||
return;
|
||||
if (this.isOpen(id))
|
||||
return;
|
||||
this.dispatch(open_slice_1.actions.open(id, this.isFiltered));
|
||||
if (redraw)
|
||||
this.redrawList((_b = (_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0);
|
||||
safeRun(this.props.onToggle, id);
|
||||
}
|
||||
close(identity, redraw = true) {
|
||||
var _a, _b;
|
||||
const id = this.identifyNull(identity);
|
||||
if (!id)
|
||||
return;
|
||||
if (!this.isOpen(id))
|
||||
return;
|
||||
this.dispatch(open_slice_1.actions.close(id, this.isFiltered));
|
||||
if (redraw)
|
||||
this.redrawList((_b = (_a = this.get(id)) === null || _a === void 0 ? void 0 : _a.rowIndex) !== null && _b !== void 0 ? _b : 0);
|
||||
safeRun(this.props.onToggle, id);
|
||||
}
|
||||
toggle(identity) {
|
||||
const id = this.identifyNull(identity);
|
||||
if (!id)
|
||||
return;
|
||||
return this.isOpen(id) ? this.close(id) : this.open(id);
|
||||
}
|
||||
openParents(identity) {
|
||||
const id = this.identifyNull(identity);
|
||||
if (!id)
|
||||
return;
|
||||
const node = utils.dfs(this.root, id);
|
||||
let parent = node === null || node === void 0 ? void 0 : node.parent;
|
||||
while (parent) {
|
||||
this.open(parent.id, false);
|
||||
parent = parent.parent;
|
||||
}
|
||||
this.redrawList();
|
||||
}
|
||||
openSiblings(node) {
|
||||
const parent = node.parent;
|
||||
if (!parent) {
|
||||
this.toggle(node.id);
|
||||
}
|
||||
else if (parent.children) {
|
||||
const isOpen = node.isOpen;
|
||||
for (let sibling of parent.children) {
|
||||
if (sibling.isInternal) {
|
||||
if (isOpen)
|
||||
this.close(sibling.id, false);
|
||||
else
|
||||
this.open(sibling.id, false);
|
||||
}
|
||||
}
|
||||
this.redrawList();
|
||||
this.scrollTo(this.focusedNode);
|
||||
}
|
||||
}
|
||||
openAll() {
|
||||
utils.walk(this.root, (node) => {
|
||||
if (node.isInternal)
|
||||
this.open(node.id, false);
|
||||
});
|
||||
this.redrawList();
|
||||
}
|
||||
closeAll() {
|
||||
utils.walk(this.root, (node) => {
|
||||
if (node.isInternal)
|
||||
this.close(node.id, false);
|
||||
});
|
||||
this.redrawList();
|
||||
}
|
||||
/* Scrolling */
|
||||
scrollTo(identity, align = "smart") {
|
||||
if (!identity)
|
||||
return;
|
||||
const id = this.identify(identity);
|
||||
this.openParents(id);
|
||||
return utils
|
||||
.waitFor(() => id in this.idToIndex)
|
||||
.then(() => {
|
||||
var _a;
|
||||
const index = this.idToIndex[id];
|
||||
if (index === undefined)
|
||||
return;
|
||||
(_a = this.list.current) === null || _a === void 0 ? void 0 : _a.scrollToItem(index, align);
|
||||
/* react-window only scrolls vertically. A deeply nested node is
|
||||
indented by level * indent and can sit past the right edge when rows
|
||||
overflow horizontally, so bring it into view ourselves (#220). */
|
||||
this.scrollToNodeHorizontally(this.get(id));
|
||||
})
|
||||
.catch(() => {
|
||||
// Id: ${id} never appeared in the list.
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Horizontally scroll the list so the node's indented content is in view.
|
||||
* A no-op when the list doesn't overflow horizontally (the common case), so
|
||||
* it never disturbs scrolling for trees that fit their width.
|
||||
*/
|
||||
scrollToNodeHorizontally(node) {
|
||||
const el = this.listEl.current;
|
||||
if (!node || !el)
|
||||
return;
|
||||
const maxScroll = el.scrollWidth - el.clientWidth;
|
||||
if (maxScroll <= 0)
|
||||
return; // nothing to scroll
|
||||
const left = node.level * this.indent;
|
||||
const viewLeft = el.scrollLeft;
|
||||
const viewRight = el.scrollLeft + el.clientWidth;
|
||||
/* The visible range is half-open [viewLeft, viewRight): a pixel at viewRight
|
||||
is already clipped. Only move when the node's indentation falls outside
|
||||
it, aligning its content start to the left edge so the label is revealed,
|
||||
clamped to the list's scrollable range. */
|
||||
if (left < viewLeft || left >= viewRight) {
|
||||
el.scrollLeft = Math.max(0, Math.min(left, maxScroll));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Scroll the list vertically to an exact pixel offset from the top. This is
|
||||
* the offset-based counterpart to scrollTo(), handy for saving and restoring
|
||||
* a scroll position (#194). Negative values are clamped to the top; react-
|
||||
* window clamps the upper bound to the scrollable range.
|
||||
*/
|
||||
scrollToOffset(offset) {
|
||||
var _a;
|
||||
/* Coerce non-finite offsets (NaN/Infinity, easy to get from malformed
|
||||
persisted state) to the top rather than forwarding them to the list. */
|
||||
const safe = Number.isFinite(offset) ? Math.max(0, offset) : 0;
|
||||
(_a = this.list.current) === null || _a === void 0 ? void 0 : _a.scrollTo(safe);
|
||||
}
|
||||
/** The list's current vertical scroll offset, in pixels from the top. */
|
||||
get scrollOffset() {
|
||||
var _a, _b;
|
||||
return (_b = (_a = this.listEl.current) === null || _a === void 0 ? void 0 : _a.scrollTop) !== null && _b !== void 0 ? _b : 0;
|
||||
}
|
||||
/* State Checks */
|
||||
get isEditing() {
|
||||
return this.state.nodes.edit.id !== null;
|
||||
}
|
||||
get isFiltered() {
|
||||
var _a;
|
||||
return !!((_a = this.props.searchTerm) === null || _a === void 0 ? void 0 : _a.trim());
|
||||
}
|
||||
/** The number of nodes matching the current search term, counted across the
|
||||
* whole tree regardless of which folders are open. Returns 0 when there is no
|
||||
* active search. Consumers use this to render match counts or a "no results"
|
||||
* message (#112, #256). Ancestors shown only to keep the tree's structure
|
||||
* intact are not counted. The count is computed once when the visible list is
|
||||
* built (see createList), so reading it never re-traverses the tree. */
|
||||
get filteredCount() {
|
||||
return this.matchCount;
|
||||
}
|
||||
get hasFocus() {
|
||||
return this.state.nodes.focus.treeFocused;
|
||||
}
|
||||
get hasNoSelection() {
|
||||
return this.state.nodes.selection.ids.size === 0;
|
||||
}
|
||||
get hasOneSelection() {
|
||||
return this.state.nodes.selection.ids.size === 1;
|
||||
}
|
||||
get hasMultipleSelections() {
|
||||
return this.state.nodes.selection.ids.size > 1;
|
||||
}
|
||||
isSelected(id) {
|
||||
if (!id)
|
||||
return false;
|
||||
return this.state.nodes.selection.ids.has(id);
|
||||
}
|
||||
isOpen(id) {
|
||||
var _a, _b, _c;
|
||||
if (!id)
|
||||
return false;
|
||||
if (id === create_root_1.ROOT_ID)
|
||||
return true;
|
||||
const def = (_a = this.props.openByDefault) !== null && _a !== void 0 ? _a : true;
|
||||
if (this.isFiltered) {
|
||||
return (_b = this.state.nodes.open.filtered[id]) !== null && _b !== void 0 ? _b : true; // Filtered folders are always opened by default
|
||||
}
|
||||
else {
|
||||
return (_c = this.state.nodes.open.unfiltered[id]) !== null && _c !== void 0 ? _c : def;
|
||||
}
|
||||
}
|
||||
isEditable(data) {
|
||||
return this.isActionPossible(data, this.props.disableEdit);
|
||||
}
|
||||
isDraggable(data) {
|
||||
return this.isActionPossible(data, this.props.disableDrag);
|
||||
}
|
||||
isSelectable(data) {
|
||||
return this.isActionPossible(data, this.props.disableSelect);
|
||||
}
|
||||
isActionPossible(data, disabler = () => false) {
|
||||
return !utils.access(data, disabler);
|
||||
}
|
||||
isDragging(node) {
|
||||
const id = this.identifyNull(node);
|
||||
if (!id)
|
||||
return false;
|
||||
return this.state.nodes.drag.id === id;
|
||||
}
|
||||
isFocused(id) {
|
||||
return this.hasFocus && this.state.nodes.focus.id === id;
|
||||
}
|
||||
isMatch(node) {
|
||||
return this.matchFn(node);
|
||||
}
|
||||
willReceiveDrop(node) {
|
||||
const id = this.identifyNull(node);
|
||||
if (!id)
|
||||
return false;
|
||||
const { destinationParentId, destinationIndex } = this.state.nodes.drag;
|
||||
return id === destinationParentId && destinationIndex === null;
|
||||
}
|
||||
/* Tree Event Handlers */
|
||||
onFocus() {
|
||||
const node = this.focusedNode || this.firstNode;
|
||||
if (node)
|
||||
this.dispatch((0, focus_slice_1.focus)(node.id));
|
||||
}
|
||||
onBlur() {
|
||||
this.dispatch((0, focus_slice_1.treeBlur)());
|
||||
}
|
||||
onItemsRendered(args) {
|
||||
this.visibleStartIndex = args.visibleStartIndex;
|
||||
this.visibleStopIndex = args.visibleStopIndex;
|
||||
}
|
||||
/* Get Renderers */
|
||||
get renderContainer() {
|
||||
return this.props.renderContainer || default_container_1.DefaultContainer;
|
||||
}
|
||||
get renderRow() {
|
||||
return this.props.renderRow || default_row_1.DefaultRow;
|
||||
}
|
||||
get renderNode() {
|
||||
return this.props.children || default_node_1.DefaultNode;
|
||||
}
|
||||
get renderDragPreview() {
|
||||
return this.props.renderDragPreview || default_drag_preview_1.DefaultDragPreview;
|
||||
}
|
||||
get renderCursor() {
|
||||
return this.props.renderCursor || default_cursor_1.DefaultCursor;
|
||||
}
|
||||
}
|
||||
exports.TreeApi = TreeApi;
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Generated
Vendored
+396
@@ -0,0 +1,396 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const redux_1 = require("redux");
|
||||
const root_reducer_1 = require("../state/root-reducer");
|
||||
const dnd_slice_1 = require("../state/dnd-slice");
|
||||
const tree_api_1 = require("./tree-api");
|
||||
function setupApi(props) {
|
||||
const store = (0, redux_1.createStore)(root_reducer_1.rootReducer);
|
||||
return new tree_api_1.TreeApi(store, props, { current: null }, { current: null });
|
||||
}
|
||||
test("tree.canDrop()", () => {
|
||||
expect(setupApi({ disableDrop: true }).canDrop()).toBe(false);
|
||||
expect(setupApi({ disableDrop: () => false }).canDrop()).toBe(true);
|
||||
expect(setupApi({ disableDrop: false }).canDrop()).toBe(true);
|
||||
});
|
||||
const rowData = [{ id: "a" }, { id: "b" }, { id: "c" }];
|
||||
describe("tree.drop() fires onMove (#313)", () => {
|
||||
test("reports the hovered parent and index, mapping the root id to null", () => {
|
||||
const onMove = jest.fn();
|
||||
const api = setupApi({ data: rowData, onMove });
|
||||
// The bottom drop zone hovers the root with an index past the end, just like
|
||||
// computeDrop() reports it. tree.drop() should map the root id back to null.
|
||||
api.dispatch(dnd_slice_1.actions.dragStart("a", ["a"]));
|
||||
api.dispatch(dnd_slice_1.actions.hovering(api.root.id, 3));
|
||||
api.drop();
|
||||
expect(onMove).toHaveBeenCalledTimes(1);
|
||||
expect(onMove).toHaveBeenCalledWith(expect.objectContaining({ dragIds: ["a"], parentId: null, index: 3 }));
|
||||
});
|
||||
test("coerces a null index (dropped onto a folder) to 0", () => {
|
||||
const onMove = jest.fn();
|
||||
const folderData = [{ id: "folder", children: [{ id: "child" }] }];
|
||||
const api = setupApi({ data: folderData, onMove });
|
||||
// Dropping onto a folder (rather than between rows) reports the folder as the
|
||||
// parent with a null index, which tree.drop() should coerce to 0.
|
||||
api.dispatch(dnd_slice_1.actions.dragStart("child", ["child"]));
|
||||
api.dispatch(dnd_slice_1.actions.hovering("folder", null));
|
||||
api.drop();
|
||||
expect(onMove).toHaveBeenCalledWith(expect.objectContaining({ parentId: "folder", index: 0 }));
|
||||
});
|
||||
});
|
||||
describe("custom idAccessor is honored when methods receive raw data (#347)", () => {
|
||||
const uuidData = [{ uuid: "a" }, { uuid: "b" }, { uuid: "c" }];
|
||||
test("select(data) resolves the id through idAccessor", () => {
|
||||
const onSelect = jest.fn();
|
||||
const api = setupApi({ data: uuidData, idAccessor: "uuid", onSelect });
|
||||
api.select(uuidData[1]);
|
||||
expect(api.selectedIds.has("b")).toBe(true);
|
||||
expect(api.selectedNodes.map((n) => n.id)).toEqual(["b"]);
|
||||
});
|
||||
test("focus(data) resolves the id through idAccessor", () => {
|
||||
var _a;
|
||||
const api = setupApi({ data: uuidData, idAccessor: "uuid" });
|
||||
api.focus(uuidData[2]);
|
||||
expect((_a = api.focusedNode) === null || _a === void 0 ? void 0 : _a.id).toBe("c");
|
||||
});
|
||||
test("delete(data) passes the accessor-derived id to onDelete", () => {
|
||||
const onDelete = jest.fn();
|
||||
const api = setupApi({ data: uuidData, idAccessor: "uuid", onDelete });
|
||||
api.delete(uuidData[0]);
|
||||
expect(onDelete).toHaveBeenCalledWith(expect.objectContaining({ ids: ["a"] }));
|
||||
});
|
||||
test("create() focuses the new node by its accessor-derived id", () => __awaiter(void 0, void 0, void 0, function* () {
|
||||
// create() passes the raw row data returned by onCreate straight to
|
||||
// focus/edit/select; before the fix these read `.id` and lost the node.
|
||||
const onCreate = () => ({ uuid: "new" });
|
||||
const api = setupApi({ data: uuidData, idAccessor: "uuid", onCreate });
|
||||
yield api.create();
|
||||
expect(api.state.nodes.focus.id).toBe("new");
|
||||
}));
|
||||
test("a function idAccessor is honored too", () => {
|
||||
const fnData = [{ meta: { key: "x" } }, { meta: { key: "y" } }];
|
||||
const api = setupApi({
|
||||
data: fnData,
|
||||
idAccessor: (d) => d.meta.key,
|
||||
});
|
||||
api.select(fnData[1]);
|
||||
expect(api.selectedIds.has("y")).toBe(true);
|
||||
});
|
||||
test("selectContiguous resolves nodes to accessor-derived ids", () => {
|
||||
// selectContiguous feeds NodeApi lists to the selection slice, which stores
|
||||
// ids. Those must be the accessor-derived ids, not `undefined` from a missing
|
||||
// `.id` — guards the string-only selection slice against a custom accessor.
|
||||
const api = setupApi({
|
||||
data: [{ uuid: "a" }, { uuid: "b" }, { uuid: "c" }, { uuid: "d" }],
|
||||
idAccessor: "uuid",
|
||||
});
|
||||
api.select("a");
|
||||
api.selectContiguous("c");
|
||||
expect([...api.selectedIds].sort()).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
});
|
||||
describe("custom idAccessor flows through drag-and-drop onMove (#170)", () => {
|
||||
// Data keyed by `uuid` instead of `id`, with a folder to reorder into. The
|
||||
// drag hook and computeDrop only ever see `node.id`, which createRoot derives
|
||||
// via the accessor — so onMove should report uuid values, never `undefined`.
|
||||
const uuidData = [{ uuid: "folder", children: [{ uuid: "child" }] }, { uuid: "sibling" }];
|
||||
test("dropping onto a folder reports accessor-derived dragIds and parentId", () => {
|
||||
var _a;
|
||||
const onMove = jest.fn();
|
||||
const api = setupApi({ data: uuidData, idAccessor: "uuid", onMove });
|
||||
const [folder, sibling] = api.root.children;
|
||||
// Sanity: node ids come from the accessor, not a missing `.id`.
|
||||
expect([folder.id, sibling.id]).toEqual(["folder", "sibling"]);
|
||||
// Drag "sibling" onto the folder, exactly as the drag hook + computeDrop do:
|
||||
// both feed `node.id` into the dnd state that tree.drop() reads back.
|
||||
api.dispatch(dnd_slice_1.actions.dragStart(sibling.id, [sibling.id]));
|
||||
api.dispatch(dnd_slice_1.actions.hovering(folder.id, null));
|
||||
api.drop();
|
||||
expect(onMove).toHaveBeenCalledTimes(1);
|
||||
const args = onMove.mock.calls[0][0];
|
||||
expect(args).toEqual(expect.objectContaining({ dragIds: ["sibling"], parentId: "folder", index: 0 }));
|
||||
expect((_a = args.parentNode) === null || _a === void 0 ? void 0 : _a.id).toBe("folder");
|
||||
});
|
||||
test("reordering at the root reports accessor-derived ids with parentId null", () => {
|
||||
const onMove = jest.fn();
|
||||
const api = setupApi({ data: uuidData, idAccessor: "uuid", onMove });
|
||||
const sibling = api.root.children[1];
|
||||
// Reorder "sibling" above "folder" at the root; computeDrop reports the root
|
||||
// id, which tree.drop() maps back to null.
|
||||
api.dispatch(dnd_slice_1.actions.dragStart(sibling.id, [sibling.id]));
|
||||
api.dispatch(dnd_slice_1.actions.hovering(api.root.id, 0));
|
||||
api.drop();
|
||||
expect(onMove).toHaveBeenCalledWith(expect.objectContaining({ dragIds: ["sibling"], parentId: null, index: 0 }));
|
||||
});
|
||||
});
|
||||
describe("tree.filteredCount reports how many nodes match the search (#112, #256)", () => {
|
||||
// apple/apricot sit under a fruit folder; banana is a sibling leaf.
|
||||
const data = [{ id: "fruit", children: [{ id: "apple" }, { id: "apricot" }] }, { id: "banana" }];
|
||||
// Matches a node by its own id only, so folders never count just because a
|
||||
// child matched — isolating "true match" from "ancestor kept for structure".
|
||||
const matchById = (node, term) => node.id.includes(term);
|
||||
test("is 0 when there is no active search term", () => {
|
||||
expect(setupApi({ data }).filteredCount).toBe(0);
|
||||
// Whitespace-only terms are not a real search, matching isFiltered.
|
||||
expect(setupApi({ data, searchTerm: " " }).filteredCount).toBe(0);
|
||||
});
|
||||
test("counts only matching nodes, not the ancestors kept for structure", () => {
|
||||
// "apple" and "apricot" match "ap"; the "fruit" parent is shown to keep the
|
||||
// tree intact but is not itself a match under an id-only predicate.
|
||||
const api = setupApi({ data, searchTerm: "ap", searchMatch: matchById });
|
||||
expect(api.filteredCount).toBe(2);
|
||||
});
|
||||
test("counts matches even inside collapsed folders", () => {
|
||||
// filteredCount walks the whole tree, so open/closed state doesn't change it.
|
||||
const api = setupApi({ data, searchTerm: "apple", searchMatch: matchById });
|
||||
api.close("fruit");
|
||||
// close() only redraws; the provider rebuilds the list on open-state change,
|
||||
// so update() here is what actually re-counts against the collapsed tree.
|
||||
api.update(api.props);
|
||||
expect(api.visibleNodes.map((n) => n.id)).toEqual(["fruit"]);
|
||||
expect(api.filteredCount).toBe(1);
|
||||
});
|
||||
test("is 0 when the search term matches nothing", () => {
|
||||
expect(setupApi({ data, searchTerm: "zzz" }).filteredCount).toBe(0);
|
||||
});
|
||||
test("does not count ancestors under the default matcher", () => {
|
||||
// The default matcher searches a node's own fields, skipping its children,
|
||||
// so "fruit" is shown to keep the tree intact but doesn't inflate the count.
|
||||
const api = setupApi({ data, searchTerm: "ap" });
|
||||
expect(api.visibleNodes.map((n) => n.id)).toEqual(["fruit", "apple", "apricot"]);
|
||||
expect(api.filteredCount).toBe(2);
|
||||
});
|
||||
test("the default matcher ignores keys nested in the children data", () => {
|
||||
// Stringifying children leaked their keys, so every folder matched "id".
|
||||
expect(setupApi({ data, searchTerm: "id" }).filteredCount).toBe(0);
|
||||
});
|
||||
});
|
||||
test("rowHeight defaults to 24", () => {
|
||||
const api = setupApi({});
|
||||
expect(api.rowHeight).toBe(24);
|
||||
expect(api.rowHeightAt(0)).toBe(24);
|
||||
});
|
||||
test("fixed numeric rowHeight", () => {
|
||||
const api = setupApi({ data: rowData, rowHeight: 30 });
|
||||
expect(api.rowHeight).toBe(30);
|
||||
expect(api.rowHeightAt(0)).toBe(30);
|
||||
expect(api.rowTopPosition(0)).toBe(0);
|
||||
expect(api.rowTopPosition(2)).toBe(60);
|
||||
expect(api.rowTopPosition(3)).toBe(90); // total list height
|
||||
});
|
||||
test("variable rowHeight function", () => {
|
||||
const heights = { a: 10, b: 20, c: 40 };
|
||||
const api = setupApi({
|
||||
data: rowData,
|
||||
rowHeight: (node) => heights[node.id],
|
||||
});
|
||||
// The back-compat getter falls back to the default for variable heights.
|
||||
expect(api.rowHeight).toBe(24);
|
||||
expect(api.rowHeightAt(0)).toBe(10);
|
||||
expect(api.rowHeightAt(1)).toBe(20);
|
||||
expect(api.rowTopPosition(0)).toBe(0);
|
||||
expect(api.rowTopPosition(1)).toBe(10);
|
||||
expect(api.rowTopPosition(2)).toBe(30);
|
||||
expect(api.rowTopPosition(3)).toBe(70); // total list height
|
||||
// Out-of-range index falls back to the default height, never an invalid 0.
|
||||
expect(api.rowHeightAt(99)).toBe(24);
|
||||
});
|
||||
describe("onSelect fires exactly once per selection method (#332)", () => {
|
||||
function setupWithSpy() {
|
||||
const onSelect = jest.fn();
|
||||
const api = setupApi({ data: rowData, onSelect });
|
||||
return { api, onSelect };
|
||||
}
|
||||
test("setSelection", () => {
|
||||
const { api, onSelect } = setupWithSpy();
|
||||
api.setSelection({ ids: ["a"], anchor: "a", mostRecent: "a" });
|
||||
expect(onSelect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
test("select", () => {
|
||||
const { api, onSelect } = setupWithSpy();
|
||||
api.select("a");
|
||||
expect(onSelect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
test("selectMulti", () => {
|
||||
const { api, onSelect } = setupWithSpy();
|
||||
api.selectMulti("a");
|
||||
expect(onSelect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
test("selectContiguous", () => {
|
||||
const { api, onSelect } = setupWithSpy();
|
||||
api.select("a");
|
||||
onSelect.mockClear();
|
||||
api.selectContiguous("c");
|
||||
expect(onSelect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
test("selectAll", () => {
|
||||
const { api, onSelect } = setupWithSpy();
|
||||
api.selectAll();
|
||||
expect(api.selectedIds.size).toBe(3);
|
||||
expect(onSelect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
test("deselectAll", () => {
|
||||
const { api, onSelect } = setupWithSpy();
|
||||
api.selectAll();
|
||||
onSelect.mockClear();
|
||||
api.deselectAll();
|
||||
expect(api.selectedIds.size).toBe(0);
|
||||
expect(onSelect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
test("deselect", () => {
|
||||
const { api, onSelect } = setupWithSpy();
|
||||
api.selectMulti("a");
|
||||
api.selectMulti("b");
|
||||
onSelect.mockClear();
|
||||
api.deselect("a");
|
||||
expect(onSelect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
describe("scrollTo brings a deeply nested node into view horizontally (#220)", () => {
|
||||
// A folder tree where "deep" sits at level 2 (indented 2 * 24 = 48px).
|
||||
const nestedData = [{ id: "root", children: [{ id: "mid", children: [{ id: "deep" }] }] }];
|
||||
// react-window's scrollToItem only scrolls vertically; the horizontal scroll
|
||||
// happens on the outer list element, which we stub here.
|
||||
function setupWithListEl(el) {
|
||||
const store = (0, redux_1.createStore)(root_reducer_1.rootReducer);
|
||||
const list = { current: { scrollToItem: jest.fn() } };
|
||||
const listEl = { current: el };
|
||||
return new tree_api_1.TreeApi(store, { data: nestedData }, list, listEl);
|
||||
}
|
||||
test("scrolls right when the node is past the right edge", () => __awaiter(void 0, void 0, void 0, function* () {
|
||||
const el = { scrollWidth: 500, clientWidth: 40, scrollLeft: 0 };
|
||||
const api = setupWithListEl(el);
|
||||
yield api.scrollTo("deep");
|
||||
// Aligns the node's indentation start (level 2 * indent 24) to the left edge.
|
||||
expect(el.scrollLeft).toBe(48);
|
||||
}));
|
||||
test("scrolls left when the node is past the left edge", () => __awaiter(void 0, void 0, void 0, function* () {
|
||||
const el = { scrollWidth: 500, clientWidth: 100, scrollLeft: 200 };
|
||||
const api = setupWithListEl(el);
|
||||
yield api.scrollTo("deep");
|
||||
expect(el.scrollLeft).toBe(48);
|
||||
}));
|
||||
test("scrolls when the node's start sits exactly on the right edge", () => __awaiter(void 0, void 0, void 0, function* () {
|
||||
// viewRight === left (48): the visible range is half-open, so the content
|
||||
// start is already clipped and must be scrolled into view.
|
||||
const el = { scrollWidth: 500, clientWidth: 48, scrollLeft: 0 };
|
||||
const api = setupWithListEl(el);
|
||||
yield api.scrollTo("deep");
|
||||
expect(el.scrollLeft).toBe(48);
|
||||
}));
|
||||
test("clamps the target to the maximum scrollable distance", () => __awaiter(void 0, void 0, void 0, function* () {
|
||||
// left (48) exceeds maxScroll (60 - 40 = 20), so scrollLeft is clamped.
|
||||
const el = { scrollWidth: 60, clientWidth: 40, scrollLeft: 0 };
|
||||
const api = setupWithListEl(el);
|
||||
yield api.scrollTo("deep");
|
||||
expect(el.scrollLeft).toBe(20);
|
||||
}));
|
||||
test("leaves scroll untouched when the node is already in view", () => __awaiter(void 0, void 0, void 0, function* () {
|
||||
const el = { scrollWidth: 500, clientWidth: 200, scrollLeft: 0 };
|
||||
const api = setupWithListEl(el);
|
||||
yield api.scrollTo("deep");
|
||||
expect(el.scrollLeft).toBe(0);
|
||||
}));
|
||||
test("no-ops when the list does not overflow horizontally", () => __awaiter(void 0, void 0, void 0, function* () {
|
||||
const el = { scrollWidth: 100, clientWidth: 100, scrollLeft: 0 };
|
||||
const api = setupWithListEl(el);
|
||||
yield api.scrollTo("deep");
|
||||
expect(el.scrollLeft).toBe(0);
|
||||
}));
|
||||
});
|
||||
describe("scrollToOffset / scrollOffset set and read the vertical position (#194)", () => {
|
||||
const data = [{ id: "a" }, { id: "b" }, { id: "c" }];
|
||||
function setup(el) {
|
||||
const store = (0, redux_1.createStore)(root_reducer_1.rootReducer);
|
||||
const list = { current: { scrollTo: jest.fn() } };
|
||||
const listEl = { current: (el !== null && el !== void 0 ? el : null) };
|
||||
return { api: new tree_api_1.TreeApi(store, { data }, list, listEl), list, listEl };
|
||||
}
|
||||
test("scrollToOffset forwards the offset to the underlying list", () => {
|
||||
const { api, list } = setup();
|
||||
api.scrollToOffset(120);
|
||||
expect(list.current.scrollTo).toHaveBeenCalledWith(120);
|
||||
});
|
||||
test("scrollToOffset clamps negative offsets to the top", () => {
|
||||
const { api, list } = setup();
|
||||
api.scrollToOffset(-50);
|
||||
expect(list.current.scrollTo).toHaveBeenCalledWith(0);
|
||||
});
|
||||
test("scrollToOffset coerces non-finite offsets to the top", () => {
|
||||
const { api, list } = setup();
|
||||
api.scrollToOffset(NaN);
|
||||
api.scrollToOffset(Infinity);
|
||||
expect(list.current.scrollTo).toHaveBeenNthCalledWith(1, 0);
|
||||
expect(list.current.scrollTo).toHaveBeenNthCalledWith(2, 0);
|
||||
});
|
||||
test("scrollOffset reads the list element's scrollTop", () => {
|
||||
const { api } = setup({ scrollTop: 80 });
|
||||
expect(api.scrollOffset).toBe(80);
|
||||
});
|
||||
test("scrollOffset is 0 before the list element mounts", () => {
|
||||
const { api } = setup();
|
||||
expect(api.scrollOffset).toBe(0);
|
||||
});
|
||||
});
|
||||
describe("tree.hover() keeps the destination consistent with canDrop (#247)", () => {
|
||||
// box(0) > slider(1), folder2(0) > child2(1)
|
||||
const data = [
|
||||
{ id: "box", children: [{ id: "slider" }] },
|
||||
{ id: "folder2", children: [{ id: "child2" }] },
|
||||
];
|
||||
test("a droppable line hover records the destination and shows the cursor", () => {
|
||||
const api = setupApi({ data });
|
||||
api.dispatch(dnd_slice_1.actions.dragStart("slider", ["slider"]));
|
||||
// Drop slider as a sibling at the root — a valid line drop.
|
||||
api.hover({ parentId: null, index: 2 }, { type: "line", index: 2, level: 0 });
|
||||
expect(api.canDrop()).toBe(true);
|
||||
expect(api.state.nodes.drag.destinationIndex).toBe(2);
|
||||
expect(api.state.dnd.cursor).toEqual({ type: "line", index: 2, level: 0 });
|
||||
});
|
||||
test("a droppable folder hover highlights it via willReceiveDrop", () => {
|
||||
var _a;
|
||||
const api = setupApi({ data });
|
||||
api.dispatch(dnd_slice_1.actions.dragStart("slider", ["slider"]));
|
||||
// Drop slider INTO folder2 (index null → highlight).
|
||||
api.hover({ parentId: "folder2", index: null }, { type: "highlight", id: "folder2" });
|
||||
expect(api.canDrop()).toBe(true);
|
||||
expect(api.willReceiveDrop("folder2")).toBe(true);
|
||||
expect((_a = api.dragDestinationParent) === null || _a === void 0 ? void 0 : _a.id).toBe("folder2");
|
||||
expect(api.state.dnd.cursor).toEqual({ type: "highlight", id: "folder2" });
|
||||
});
|
||||
test("a can't-drop hover records no destination and hides the cursor", () => {
|
||||
const api = setupApi({ data });
|
||||
// A folder can't be dropped into its own subtree.
|
||||
api.dispatch(dnd_slice_1.actions.dragStart("box", ["box"]));
|
||||
api.hover({ parentId: "box", index: null }, { type: "highlight", id: "box" });
|
||||
expect(api.canDrop()).toBe(false);
|
||||
// The consumer-facing destination and cursor all agree: "no drop here".
|
||||
expect(api.willReceiveDrop("box")).toBe(false);
|
||||
expect(api.dragDestinationParent).toBe(null);
|
||||
expect(api.state.nodes.drag.destinationParentId).toBe(null);
|
||||
expect(api.state.dnd.cursor).toEqual({ type: "none" });
|
||||
// But the drop guard still sees the real target, so a release is rejected
|
||||
// rather than falling back to a root drop.
|
||||
expect(api.state.dnd.parentId).toBe("box");
|
||||
});
|
||||
test("moving from a droppable to a can't-drop hover clears the stale destination", () => {
|
||||
const api = setupApi({ data });
|
||||
api.dispatch(dnd_slice_1.actions.dragStart("box", ["box"]));
|
||||
// Valid: drop box INTO folder2.
|
||||
api.hover({ parentId: "folder2", index: null }, { type: "highlight", id: "folder2" });
|
||||
expect(api.willReceiveDrop("folder2")).toBe(true);
|
||||
// Then slide into box's own subtree, where the drop is invalid.
|
||||
api.hover({ parentId: "box", index: null }, { type: "highlight", id: "box" });
|
||||
expect(api.willReceiveDrop("folder2")).toBe(false);
|
||||
expect(api.dragDestinationParent).toBe(null);
|
||||
});
|
||||
});
|
||||
Generated
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
import { Cursor } from "../dnd/compute-drop";
|
||||
import { ActionTypes } from "../types/utils";
|
||||
export type DndState = {
|
||||
dragId: null | string;
|
||||
cursor: Cursor;
|
||||
dragIds: string[];
|
||||
parentId: null | string;
|
||||
index: number | null;
|
||||
};
|
||||
export declare const actions: {
|
||||
cursor(cursor: Cursor): {
|
||||
type: "DND_CURSOR";
|
||||
cursor: Cursor;
|
||||
};
|
||||
dragStart(id: string, dragIds: string[]): {
|
||||
type: "DND_DRAG_START";
|
||||
id: string;
|
||||
dragIds: string[];
|
||||
};
|
||||
dragEnd(): {
|
||||
type: "DND_DRAG_END";
|
||||
};
|
||||
hovering(parentId: string | null, index: number | null): {
|
||||
type: "DND_HOVERING";
|
||||
parentId: string | null;
|
||||
index: number | null;
|
||||
};
|
||||
setDestination(parentId: string | null, index: number | null): {
|
||||
type: "DND_DESTINATION";
|
||||
parentId: string | null;
|
||||
index: number | null;
|
||||
};
|
||||
};
|
||||
export declare function reducer(state: DndState | undefined, action: ActionTypes<typeof actions>): DndState;
|
||||
Generated
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.actions = void 0;
|
||||
exports.reducer = reducer;
|
||||
const initial_1 = require("./initial");
|
||||
/* Actions */
|
||||
exports.actions = {
|
||||
cursor(cursor) {
|
||||
return { type: "DND_CURSOR", cursor };
|
||||
},
|
||||
dragStart(id, dragIds) {
|
||||
return { type: "DND_DRAG_START", id, dragIds };
|
||||
},
|
||||
dragEnd() {
|
||||
return { type: "DND_DRAG_END" };
|
||||
},
|
||||
hovering(parentId, index) {
|
||||
return { type: "DND_HOVERING", parentId, index };
|
||||
},
|
||||
/* The consumer-facing destination (willReceiveDrop / dragDestinationParent).
|
||||
Dispatched by tree.hover() only when the target is actually droppable, so
|
||||
it never points somewhere canDrop()/the cursor forbids (#247). */
|
||||
setDestination(parentId, index) {
|
||||
return { type: "DND_DESTINATION", parentId, index };
|
||||
},
|
||||
};
|
||||
/* Reducer */
|
||||
function reducer(state = (0, initial_1.initialState)()["dnd"], action) {
|
||||
switch (action.type) {
|
||||
case "DND_CURSOR":
|
||||
return Object.assign(Object.assign({}, state), { cursor: action.cursor });
|
||||
case "DND_DRAG_START":
|
||||
return Object.assign(Object.assign({}, state), { dragId: action.id, dragIds: action.dragIds });
|
||||
case "DND_DRAG_END":
|
||||
return (0, initial_1.initialState)()["dnd"];
|
||||
case "DND_HOVERING":
|
||||
return Object.assign(Object.assign({}, state), { parentId: action.parentId, index: action.index });
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
import { ActionTypes } from "../types/utils";
|
||||
import { actions as dnd } from "./dnd-slice";
|
||||
export type DragSlice = {
|
||||
id: string | null;
|
||||
selectedIds: string[];
|
||||
destinationParentId: string | null;
|
||||
destinationIndex: number | null;
|
||||
};
|
||||
export declare function reducer(state: DragSlice | undefined, action: ActionTypes<typeof dnd>): DragSlice;
|
||||
Generated
Vendored
+22
@@ -0,0 +1,22 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.reducer = reducer;
|
||||
const initial_1 = require("./initial");
|
||||
/* Reducer */
|
||||
function reducer(state = (0, initial_1.initialState)().nodes.drag, action) {
|
||||
switch (action.type) {
|
||||
case "DND_DRAG_START":
|
||||
return Object.assign(Object.assign({}, state), { id: action.id, selectedIds: action.dragIds });
|
||||
case "DND_DRAG_END":
|
||||
return Object.assign(Object.assign({}, state), { id: null, destinationParentId: null, destinationIndex: null, selectedIds: [] });
|
||||
case "DND_DESTINATION":
|
||||
if (action.parentId !== state.destinationParentId || action.index != state.destinationIndex) {
|
||||
return Object.assign(Object.assign({}, state), { destinationParentId: action.parentId, destinationIndex: action.index });
|
||||
}
|
||||
else {
|
||||
return state;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
export type EditState = {
|
||||
id: string | null;
|
||||
};
|
||||
export declare function edit(id: string | null): {
|
||||
type: "EDIT";
|
||||
id: string | null;
|
||||
};
|
||||
export declare function reducer(state: EditState | undefined, action: ReturnType<typeof edit>): EditState;
|
||||
Generated
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.edit = edit;
|
||||
exports.reducer = reducer;
|
||||
/* Actions */
|
||||
function edit(id) {
|
||||
return { type: "EDIT", id };
|
||||
}
|
||||
/* Reducer */
|
||||
function reducer(state = { id: null }, action) {
|
||||
if (action.type === "EDIT") {
|
||||
return Object.assign(Object.assign({}, state), { id: action.id });
|
||||
}
|
||||
else {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
export type FocusState = {
|
||||
id: string | null;
|
||||
treeFocused: boolean;
|
||||
};
|
||||
export declare function focus(id: string | null): {
|
||||
type: "FOCUS";
|
||||
id: string | null;
|
||||
};
|
||||
export declare function treeBlur(): {
|
||||
readonly type: "TREE_BLUR";
|
||||
};
|
||||
export declare function reducer(state: FocusState | undefined, action: ReturnType<typeof focus> | ReturnType<typeof treeBlur>): FocusState;
|
||||
Generated
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
"use strict";
|
||||
/* Types */
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.focus = focus;
|
||||
exports.treeBlur = treeBlur;
|
||||
exports.reducer = reducer;
|
||||
/* Actions */
|
||||
function focus(id) {
|
||||
return { type: "FOCUS", id };
|
||||
}
|
||||
function treeBlur() {
|
||||
return { type: "TREE_BLUR" };
|
||||
}
|
||||
/* Reducer */
|
||||
function reducer(state = { id: null, treeFocused: false }, action) {
|
||||
if (action.type === "FOCUS") {
|
||||
return Object.assign(Object.assign({}, state), { id: action.id, treeFocused: true });
|
||||
}
|
||||
else if (action.type === "TREE_BLUR") {
|
||||
return Object.assign(Object.assign({}, state), { treeFocused: false });
|
||||
}
|
||||
else {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
import { TreeProps } from "../types/tree-props";
|
||||
import { RootState } from "./root-reducer";
|
||||
export declare const initialState: (props?: TreeProps<any>) => RootState;
|
||||
Generated
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.initialState = void 0;
|
||||
const initialState = (props) => {
|
||||
var _a;
|
||||
return ({
|
||||
nodes: {
|
||||
// Changes together
|
||||
open: { filtered: {}, unfiltered: (_a = props === null || props === void 0 ? void 0 : props.initialOpenState) !== null && _a !== void 0 ? _a : {} },
|
||||
focus: { id: null, treeFocused: false },
|
||||
edit: { id: null },
|
||||
drag: {
|
||||
id: null,
|
||||
selectedIds: [],
|
||||
destinationParentId: null,
|
||||
destinationIndex: null,
|
||||
},
|
||||
selection: { ids: new Set(), anchor: null, mostRecent: null },
|
||||
},
|
||||
dnd: {
|
||||
cursor: { type: "none" },
|
||||
dragId: null,
|
||||
dragIds: [],
|
||||
parentId: null,
|
||||
index: -1,
|
||||
},
|
||||
});
|
||||
};
|
||||
exports.initialState = initialState;
|
||||
Generated
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
import { ActionTypes } from "../types/utils";
|
||||
export type OpenMap = {
|
||||
[id: string]: boolean;
|
||||
};
|
||||
export type OpenSlice = {
|
||||
unfiltered: OpenMap;
|
||||
filtered: OpenMap;
|
||||
};
|
||||
export declare const actions: {
|
||||
open(id: string, filtered: boolean): {
|
||||
type: "VISIBILITY_OPEN";
|
||||
id: string;
|
||||
filtered: boolean;
|
||||
};
|
||||
close(id: string, filtered: boolean): {
|
||||
type: "VISIBILITY_CLOSE";
|
||||
id: string;
|
||||
filtered: boolean;
|
||||
};
|
||||
toggle(id: string, filtered: boolean): {
|
||||
type: "VISIBILITY_TOGGLE";
|
||||
id: string;
|
||||
filtered: boolean;
|
||||
};
|
||||
clear(filtered: boolean): {
|
||||
type: "VISIBILITY_CLEAR";
|
||||
filtered: boolean;
|
||||
};
|
||||
};
|
||||
export declare function reducer(state: OpenSlice | undefined, action: ActionTypes<typeof actions>): OpenSlice;
|
||||
Generated
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.actions = void 0;
|
||||
exports.reducer = reducer;
|
||||
/* Actions */
|
||||
exports.actions = {
|
||||
open(id, filtered) {
|
||||
return { type: "VISIBILITY_OPEN", id, filtered };
|
||||
},
|
||||
close(id, filtered) {
|
||||
return { type: "VISIBILITY_CLOSE", id, filtered };
|
||||
},
|
||||
toggle(id, filtered) {
|
||||
return { type: "VISIBILITY_TOGGLE", id, filtered };
|
||||
},
|
||||
clear(filtered) {
|
||||
return { type: "VISIBILITY_CLEAR", filtered };
|
||||
},
|
||||
};
|
||||
/* Reducer */
|
||||
function openMapReducer(state = {}, action) {
|
||||
if (action.type === "VISIBILITY_OPEN") {
|
||||
return Object.assign(Object.assign({}, state), { [action.id]: true });
|
||||
}
|
||||
else if (action.type === "VISIBILITY_CLOSE") {
|
||||
return Object.assign(Object.assign({}, state), { [action.id]: false });
|
||||
}
|
||||
else if (action.type === "VISIBILITY_TOGGLE") {
|
||||
const prev = state[action.id];
|
||||
return Object.assign(Object.assign({}, state), { [action.id]: !prev });
|
||||
}
|
||||
else if (action.type === "VISIBILITY_CLEAR") {
|
||||
return {};
|
||||
}
|
||||
else {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
function reducer(state = { filtered: {}, unfiltered: {} }, action) {
|
||||
if (!action.type.startsWith("VISIBILITY"))
|
||||
return state;
|
||||
if (action.filtered) {
|
||||
return Object.assign(Object.assign({}, state), { filtered: openMapReducer(state.filtered, action) });
|
||||
}
|
||||
else {
|
||||
return Object.assign(Object.assign({}, state), { unfiltered: openMapReducer(state.unfiltered, action) });
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+101
@@ -0,0 +1,101 @@
|
||||
import { ActionFromReducer } from "redux";
|
||||
export declare const rootReducer: import("redux").Reducer<{
|
||||
nodes: {
|
||||
focus: import("./focus-slice").FocusState;
|
||||
edit: import("./edit-slice").EditState;
|
||||
open: import("./open-slice").OpenSlice;
|
||||
selection: import("./selection-slice").SelectionState;
|
||||
drag: import("./drag-slice").DragSlice;
|
||||
};
|
||||
dnd: import("./dnd-slice").DndState;
|
||||
}, {
|
||||
type: "FOCUS";
|
||||
id: string | null;
|
||||
} | {
|
||||
readonly type: "TREE_BLUR";
|
||||
} | {
|
||||
type: "EDIT";
|
||||
id: string | null;
|
||||
} | import("../types/utils").ActionTypes<{
|
||||
open(id: string, filtered: boolean): {
|
||||
type: "VISIBILITY_OPEN";
|
||||
id: string;
|
||||
filtered: boolean;
|
||||
};
|
||||
close(id: string, filtered: boolean): {
|
||||
type: "VISIBILITY_CLOSE";
|
||||
id: string;
|
||||
filtered: boolean;
|
||||
};
|
||||
toggle(id: string, filtered: boolean): {
|
||||
type: "VISIBILITY_TOGGLE";
|
||||
id: string;
|
||||
filtered: boolean;
|
||||
};
|
||||
clear(filtered: boolean): {
|
||||
type: "VISIBILITY_CLEAR";
|
||||
filtered: boolean;
|
||||
};
|
||||
}> | import("../types/utils").ActionTypes<{
|
||||
clear: () => {
|
||||
type: "SELECTION_CLEAR";
|
||||
};
|
||||
only: (id: string) => {
|
||||
type: "SELECTION_ONLY";
|
||||
id: string;
|
||||
};
|
||||
add: (id: string | string[]) => {
|
||||
type: "SELECTION_ADD";
|
||||
ids: string[];
|
||||
};
|
||||
remove: (id: string | string[]) => {
|
||||
type: "SELECTION_REMOVE";
|
||||
ids: string[];
|
||||
};
|
||||
set: (args: {
|
||||
ids: Set<string>;
|
||||
anchor: string | null;
|
||||
mostRecent: string | null;
|
||||
}) => {
|
||||
ids: Set<string>;
|
||||
anchor: string | null;
|
||||
mostRecent: string | null;
|
||||
type: "SELECTION_SET";
|
||||
};
|
||||
mostRecent: (id: string | null) => {
|
||||
type: "SELECTION_MOST_RECENT";
|
||||
id: string | null;
|
||||
};
|
||||
anchor: (id: string | null) => {
|
||||
type: "SELECTION_ANCHOR";
|
||||
id: string | null;
|
||||
};
|
||||
}> | import("../types/utils").ActionTypes<{
|
||||
cursor(cursor: import("../dnd/compute-drop").Cursor): {
|
||||
type: "DND_CURSOR";
|
||||
cursor: import("../dnd/compute-drop").Cursor;
|
||||
};
|
||||
dragStart(id: string, dragIds: string[]): {
|
||||
type: "DND_DRAG_START";
|
||||
id: string;
|
||||
dragIds: string[];
|
||||
};
|
||||
dragEnd(): {
|
||||
type: "DND_DRAG_END";
|
||||
};
|
||||
hovering(parentId: string | null, index: number | null): {
|
||||
type: "DND_HOVERING";
|
||||
parentId: string | null;
|
||||
index: number | null;
|
||||
};
|
||||
setDestination(parentId: string | null, index: number | null): {
|
||||
type: "DND_DESTINATION";
|
||||
parentId: string | null;
|
||||
index: number | null;
|
||||
};
|
||||
}>, Partial<{
|
||||
nodes: never;
|
||||
dnd: never;
|
||||
}>>;
|
||||
export type RootState = ReturnType<typeof rootReducer>;
|
||||
export type Actions = ActionFromReducer<typeof rootReducer>;
|
||||
Generated
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.rootReducer = void 0;
|
||||
const redux_1 = require("redux");
|
||||
const focus_slice_1 = require("./focus-slice");
|
||||
const edit_slice_1 = require("./edit-slice");
|
||||
const dnd_slice_1 = require("./dnd-slice");
|
||||
const selection_slice_1 = require("./selection-slice");
|
||||
const open_slice_1 = require("./open-slice");
|
||||
const drag_slice_1 = require("./drag-slice");
|
||||
exports.rootReducer = (0, redux_1.combineReducers)({
|
||||
nodes: (0, redux_1.combineReducers)({
|
||||
focus: focus_slice_1.reducer,
|
||||
edit: edit_slice_1.reducer,
|
||||
open: open_slice_1.reducer,
|
||||
selection: selection_slice_1.reducer,
|
||||
drag: drag_slice_1.reducer,
|
||||
}),
|
||||
dnd: dnd_slice_1.reducer,
|
||||
});
|
||||
Generated
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
import { ActionTypes } from "../types/utils";
|
||||
export type SelectionState = {
|
||||
ids: Set<string>;
|
||||
anchor: string | null;
|
||||
mostRecent: string | null;
|
||||
};
|
||||
export declare const actions: {
|
||||
clear: () => {
|
||||
type: "SELECTION_CLEAR";
|
||||
};
|
||||
only: (id: string) => {
|
||||
type: "SELECTION_ONLY";
|
||||
id: string;
|
||||
};
|
||||
add: (id: string | string[]) => {
|
||||
type: "SELECTION_ADD";
|
||||
ids: string[];
|
||||
};
|
||||
remove: (id: string | string[]) => {
|
||||
type: "SELECTION_REMOVE";
|
||||
ids: string[];
|
||||
};
|
||||
set: (args: {
|
||||
ids: Set<string>;
|
||||
anchor: string | null;
|
||||
mostRecent: string | null;
|
||||
}) => {
|
||||
ids: Set<string>;
|
||||
anchor: string | null;
|
||||
mostRecent: string | null;
|
||||
type: "SELECTION_SET";
|
||||
};
|
||||
mostRecent: (id: string | null) => {
|
||||
type: "SELECTION_MOST_RECENT";
|
||||
id: string | null;
|
||||
};
|
||||
anchor: (id: string | null) => {
|
||||
type: "SELECTION_ANCHOR";
|
||||
id: string | null;
|
||||
};
|
||||
};
|
||||
export declare function reducer(state: SelectionState | undefined, action: ActionTypes<typeof actions>): SelectionState;
|
||||
Generated
Vendored
+61
@@ -0,0 +1,61 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.actions = void 0;
|
||||
exports.reducer = reducer;
|
||||
const initial_1 = require("./initial");
|
||||
/* Actions. These take ids already resolved to strings: callers go through
|
||||
TreeApi.identify(), which honors a custom idAccessor. Keeping the slice
|
||||
string-only means there is a single, accessor-aware identify() rather than a
|
||||
second `.id`-assuming one hiding here. */
|
||||
exports.actions = {
|
||||
clear: () => ({ type: "SELECTION_CLEAR" }),
|
||||
only: (id) => ({
|
||||
type: "SELECTION_ONLY",
|
||||
id,
|
||||
}),
|
||||
add: (id) => ({
|
||||
type: "SELECTION_ADD",
|
||||
ids: Array.isArray(id) ? id : [id],
|
||||
}),
|
||||
remove: (id) => ({
|
||||
type: "SELECTION_REMOVE",
|
||||
ids: Array.isArray(id) ? id : [id],
|
||||
}),
|
||||
set: (args) => (Object.assign({ type: "SELECTION_SET" }, args)),
|
||||
mostRecent: (id) => ({
|
||||
type: "SELECTION_MOST_RECENT",
|
||||
id,
|
||||
}),
|
||||
anchor: (id) => ({
|
||||
type: "SELECTION_ANCHOR",
|
||||
id,
|
||||
}),
|
||||
};
|
||||
/* Reducer */
|
||||
function reducer(state = (0, initial_1.initialState)()["nodes"]["selection"], action) {
|
||||
const ids = state.ids;
|
||||
switch (action.type) {
|
||||
case "SELECTION_CLEAR":
|
||||
return Object.assign(Object.assign({}, state), { ids: new Set() });
|
||||
case "SELECTION_ONLY":
|
||||
return Object.assign(Object.assign({}, state), { ids: new Set([action.id]) });
|
||||
case "SELECTION_ADD":
|
||||
if (action.ids.length === 0)
|
||||
return state;
|
||||
action.ids.forEach((id) => ids.add(id));
|
||||
return Object.assign(Object.assign({}, state), { ids: new Set(ids) });
|
||||
case "SELECTION_REMOVE":
|
||||
if (action.ids.length === 0)
|
||||
return state;
|
||||
action.ids.forEach((id) => ids.delete(id));
|
||||
return Object.assign(Object.assign({}, state), { ids: new Set(ids) });
|
||||
case "SELECTION_SET":
|
||||
return Object.assign(Object.assign({}, state), { ids: action.ids, mostRecent: action.mostRecent, anchor: action.anchor });
|
||||
case "SELECTION_MOST_RECENT":
|
||||
return Object.assign(Object.assign({}, state), { mostRecent: action.id });
|
||||
case "SELECTION_ANCHOR":
|
||||
return Object.assign(Object.assign({}, state), { anchor: action.id });
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
export type CursorLocation = {
|
||||
index: number | null;
|
||||
level: number | null;
|
||||
parentId: string | null;
|
||||
};
|
||||
export type DragItem<T = any> = {
|
||||
id: string;
|
||||
dragIds: string[];
|
||||
data: T;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user