Moving Nodes
Note
Moving a node is a data write operation and must be performed inside the write method.
Moving a Node
To move a node from one location to another, use the moveNode method.
async moveNode(
node: NodeId | TreeNode,
toNode?: NodeId | TreeNode,
posOrOptions?: FlexNodeRelPosition | FlexTreeMoveOptions
):Promise<void>The third parameter can be either a pos enum (legacy style, kept for backward compatibility) or an options object:
interface FlexTreeMoveOptions {
pos?: FlexNodeRelPosition // Relative position, defaults to NextSibling
treeId?: TreeId // Target tree for cross-tree moves, see "Cross-Tree Move"
}Note
When pos is omitted, it defaults to NextSibling (next sibling), not LastChild.
- Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
node | NodeId | TreeNode | None | Node id or node object |
toNode | NodeId | TreeNode | null | Optional. Specifies the target node |
posOrOptions | FlexNodeRelPosition | FlexTreeMoveOptions | NextSibling | Optional. Move position or options object |
The following uses a simple tree to illustrate node move operations:
- Root
- A
- A1
- A2
- A3
- B
- B1
- B2
- B3
- C
- C1
- C2
- C3
- A
Last Child
Move the node as the last child node of the toNode node.
import { FlexTreeManager,LastChild } from 'flextree';
const tree = new FlexTreeManager("tree",{...})
await tree.write(async ()=>{
const anode = await tree.findNode({name:"A"})
const bnode = await tree.findNode({name:"B"})
// Move node A under node B
await tree.moveNode(anode,bnode,LastChild)
})The tree structure after the move is as follows:
- Root
- B toNode
- B1
- B2
- B3
- +A
- +A1
- +A2
- +A3
- C
- B toNode
First Child
Move the node as the last child node of the toNode node.
import { FlexTreeManager,FirstChild } from 'flextree';
const tree = new FlexTreeManager("tree",{...})
await tree.write(async ()=>{
const anode = await tree.findNode({name:"A"})
const bnode = await tree.findNode({name:"B"})
await tree.moveNode(anode,bnode,FirstChild)
})The tree structure after the move is as follows:
- Root
- B toNode
- +A
- +A1
- +A2
- +A3
- B1
- B2
- B3
- +A
- C
- B toNode
Previous Sibling
Move the node as the previous sibling of the toNode node.
import { FlexTreeManager,PreviousSibling } from 'flextree';
const tree = new FlexTreeManager("tree",{...})
await tree.write(async ()=>{
const cnode = await tree.findNode({name:"C"})
const bnode = await tree.findNode({name:"B"})
await tree.moveNode(cnode,bnode,PreviousSibling)
})Move bnode as the previous sibling of cnode. The tree structure after the move is as follows:
- Root
- A
- A1
- A2
- A3
- +C
- +C1
- +C2
- +C3
- B toNode
- B1
- B2
- B3
- A
Next Sibling
Move the node as the previous sibling of the toNode node.
import { FlexTreeManager,NextSibling } from 'flextree';
const tree = new FlexTreeManager("tree",{...})
await tree.write(async ()=>{
const anode = await tree.findNode({name:"A"})
const bnode = await tree.findNode({name:"B"})
await tree.moveNode(anode,bnode,NextSibling)
})Move anode as the next sibling of cnode. The tree structure after the move is as follows:
- Root
- B toNode
- B1
- B2
- B3
- +A
- +A1
- +A2
- +A3
- C
- C1
- C2
- C3
- B toNode
Moving a Node Up
The moveUpNode method is used to move a node up.
async moveUpNode(node: NodeId | TreeNode):Promise<void>The envisioned scenario is in a UI interface, where the user can keep moving a node up via the Move Up button until it reaches the root node.
- Within the same level, moving a node up is essentially swapping positions with its previous sibling, or equivalently, moving it to be the previous sibling of its previous sibling.
- When the node has become the first child of its parent node, on the next move up, when the node no longer has a previous sibling, the node is moved to be the previous sibling of its parent node.
Note
When moving a node up, if the node is already the first child of the root node, it will not be moved up any further.
Moving a Node Down
The moveDownNode method is used to move a node down.
async moveDownNode(node: NodeId | TreeNode):Promise<void>The envisioned scenario is in a UI interface, where the user can keep moving a node down via the Move Down button until it reaches the bottom of the tree.
- Within the same level, moving a node down is essentially swapping positions with its next sibling, or equivalently, moving it to be the next sibling of its next sibling.
- When the node has become the last child of its parent node, on the next move down, the node is already the last node of its parent, and the node will continue to move to the next sibling of its parent node.
Determining Whether a Move Is Allowed
The canMoveNode method is used to determine whether a node can be moved.
async canMoveTo(
node: NodeId | TreeNode,
toNode?: NodeId | TreeNode,
options?: FlexTreeMoveOptions
):Promise<boolean>- Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
node | NodeId | TreeNode | None | Node id or node object |
toNode | NodeId | TreeNode | None | Target node id or node object |
options | FlexTreeMoveOptions | None | Optional. treeId specifies the target tree (cross-tree pre-check, same semantics as moveNode) |
- Return Value
| Type | Description |
|---|---|
boolean | Returns true if the node can be moved, otherwise false |
Notes
- In general, any node cannot be moved into any of its descendant nodes.
- For cross-tree pre-checks: moving the root node returns
true(equivalent to deleting the source tree, see "Moving the Root Node Across Trees"); iftoNodeis not found in the target tree, an error is thrown. - The
moveNode/moveUpNode/moveDownNodemethods above already perform this check internally, so you do not need to call it additionally.
Cross-Tree Move
In a multi-tree table scenario, you can move a node (along with all its descendants) to another tree via options.treeId:
import { FlexTreeManager,LastChild } from 'flextree';
// tree1 and tree2 manage different trees in the same multi-tree table
const tree1 = new FlexTreeManager("org",{ adapter, treeId:1 })
const tree2 = new FlexTreeManager("org",{ adapter, treeId:2 })
await tree1.write(async ()=>{
const anode = await tree1.findNode({name:"A"})
const cnode = await tree2.findNode({name:"C"})
// Move subtree A of tree 1 as the last child of node C in tree 2
await tree1.moveNode(anode,cnode,{ treeId:2, pos:LastChild })
})Notes
treeIdspecifies the target tree;toNodethen points to a node in that tree (either anidor a node object).- The direction is one-way: you can only move nodes out of the current tree into another tree, not the other way around — nodes of another tree do not exist in the current
manager(using one as the source throws a NotFound error). For the reverse direction, use a manager on the target tree side. - After the move, the
treeId,level,leftValue, andrightValueof all nodes in the subtree are recalculated for the target tree. - A cross-tree move emits two events: first
node:deleted(source-tree view — the node is removed from the source tree), thennode:moved(withtoTreepointing to the target tree). - When
treeIdequals the current tree, it is treated as a same-tree move (same as omitting it); providingtreeIdin single-tree mode throws an error. - Sibling positions (
NextSibling/PreviousSibling) are not allowed when the target is the root node of the target tree (a root has no siblings; same rule as same-tree moves). - A cross-tree move completes atomically with a fixed set of
SQLstatements — the number of database accesses is independent of the subtree size.
Moving Out as a New Tree
When performing a cross-tree move with toNode omitted, the node and its subtree are moved out to become the root of a new tree specified by treeId:
await tree1.write(async ()=>{
const anode = await tree1.findNode({name:"A"})
// Move subtree A out as a new tree with treeId=3, A becomes its root
await tree1.moveNode(anode,undefined,{ treeId:3 })
})Notes
- After the move,
nodebecomes the root of the new tree (level=0,leftValue=1); the internal structure of the subtree is preserved. - In this scenario
posis ineffective (a brand-new tree has no destination reference node; it is ignored if provided). - The target
treeIdmust not already have a tree — otherwise aTree already existserror is thrown. - This also applies to the source root node: equivalent to "relocating" the entire tree to a new
treeId(the original manager becomes invalid). - Event order is the same as cross-tree moves: first
node:deleted(source-tree view), thennode:moved.
- After the move,
Moving the Root Node Across Trees (Equivalent to Deleting the Source Tree)
Moving the root node across trees is allowed — the entire source tree (the root and all its descendants) is merged into the target tree:
await tree1.write(async ()=>{
const root = await tree1.getRoot()
const cnode = await tree2.findNode({name:"C"})
// Merge the entire tree1 into tree2 as the last child of C
await tree1.moveNode(root,cnode,{ treeId:2, pos:LastChild })
})Warning
Once this operation succeeds, the tree managed by tree1's manager has been deleted:
- Any subsequent operation on that
managerwill fail — reads return empty results (getNodes()returns[],getRoot()returnsnull), and write operations (such asaddNodes/moveNode/deleteNode) throw errors because the root node no longer exists. - To continue using that
treeId, you must first callcreateRoot()again to create a new tree. - Likewise, you cannot move the root node of one tree to the previous or next sibling position of the target tree's root node (a root has no siblings).
node:moved Event
The node:moved event is emitted after a move completes:
tree.on("node:moved",(e)=>{
// e.tree the source tree when the move was initiated
// e.toTree the tree where the destination lives (=== e.tree for same-tree moves)
// e.from the moved node
// e.to the destination reference node
// e.pos the relative position
})A cross-tree move additionally emits node:deleted first (source-tree view — the node and its descendants are removed from the source tree):
tree.on("node:deleted",(e)=>{
// e.tree the source tree
// e.node the removed node (subtree root)
})