2024-06-04 13:05:44 -07:00
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
2024-07-17 18:42:49 -07:00
import { lazy } from "@/util/util" ;
2024-06-04 13:05:44 -07:00
import {
addChildAt ,
addIntermediateNode ,
2024-07-31 21:27:46 -07:00
findInsertLocationFromIndexArr ,
2024-06-04 13:05:44 -07:00
findNextInsertLocation ,
findNode ,
findParent ,
removeChild ,
2024-06-11 17:42:10 -07:00
} from "./layoutNode" ;
2024-06-04 13:05:44 -07:00
import {
2024-07-03 14:31:02 -07:00
DefaultNodeSize ,
2024-08-21 17:43:11 -07:00
DropDirection ,
FlexDirection ,
2024-06-04 13:05:44 -07:00
LayoutTreeActionType ,
LayoutTreeComputeMoveNodeAction ,
LayoutTreeDeleteNodeAction ,
2024-08-26 11:56:00 -07:00
LayoutTreeFocusNodeAction ,
2024-06-04 13:05:44 -07:00
LayoutTreeInsertNodeAction ,
2024-07-31 21:27:46 -07:00
LayoutTreeInsertNodeAtIndexAction ,
2024-07-30 10:59:53 -07:00
LayoutTreeMagnifyNodeToggleAction ,
2024-06-04 13:05:44 -07:00
LayoutTreeMoveNodeAction ,
2024-07-03 14:31:02 -07:00
LayoutTreeResizeNodeAction ,
2024-06-04 13:05:44 -07:00
LayoutTreeState ,
2024-06-17 14:14:09 -07:00
LayoutTreeSwapNodeAction ,
2024-06-04 13:05:44 -07:00
MoveOperation ,
2024-08-14 18:40:41 -07:00
} from "./types" ;
2024-06-04 13:05:44 -07:00
2024-08-26 12:32:28 -07:00
export const DEFAULT_MAX_CHILDREN = 5 ;
2024-06-04 13:05:44 -07:00
/**
* Computes an operation for inserting a new node into the tree in the given direction relative to the specified node.
*
2024-08-14 18:40:41 -07:00
* @param layoutState The state of the tree.
2024-06-04 13:05:44 -07:00
* @param computeInsertAction The operation to compute.
*/
2024-08-14 18:40:41 -07:00
export function computeMoveNode ( layoutState : LayoutTreeState , computeInsertAction : LayoutTreeComputeMoveNodeAction ) {
const rootNode = layoutState . rootNode ;
2024-09-10 17:23:28 -07:00
const { nodeId , nodeToMoveId , direction } = computeInsertAction ;
if ( ! nodeId || ! nodeToMoveId ) {
console . warn ( "either nodeId or nodeToMoveId not set" , nodeId , nodeToMoveId );
return ;
}
2024-06-04 13:05:44 -07:00
if ( direction === undefined ) {
console . warn ( "No direction provided for insertItemInDirection" );
return ;
}
2024-09-10 17:23:28 -07:00
if ( nodeId === nodeToMoveId ) {
2024-06-17 14:14:09 -07:00
console . warn ( "Cannot compute move node action since both nodes are equal" );
return ;
}
2024-08-14 18:40:41 -07:00
let newMoveOperation : MoveOperation ;
2024-09-10 17:23:28 -07:00
const parent = lazy (() => findParent ( rootNode , nodeId ));
2024-06-11 13:03:41 -07:00
const grandparent = lazy (() => findParent ( rootNode , parent (). id ));
2024-09-10 17:23:28 -07:00
const indexInParent = lazy (() => parent () ? . children . findIndex (( child ) => nodeId === child . id ));
2024-06-11 13:03:41 -07:00
const indexInGrandparent = lazy (() => grandparent () ? . children . findIndex (( child ) => parent (). id === child . id ));
2024-09-10 17:23:28 -07:00
const nodeToMoveParent = lazy (() => findParent ( rootNode , nodeToMoveId ));
2024-06-18 16:03:00 -07:00
const nodeToMoveIndexInParent = lazy (() =>
2024-09-10 17:23:28 -07:00
nodeToMoveParent () ? . children . findIndex (( child ) => nodeToMoveId === child . id )
2024-06-18 16:03:00 -07:00
);
2024-09-10 17:23:28 -07:00
const isRoot = rootNode . id === nodeId ;
// TODO: this should not be necessary. The drag layer is having trouble tracking changes to the LayoutNode fields, so I need to grab the node again here to get the latest data.
const node = findNode ( rootNode , nodeId );
const nodeToMove = findNode ( rootNode , nodeToMoveId );
if ( ! node || ! nodeToMove ) {
console . warn ( "node or nodeToMove not set" , nodeId , nodeToMoveId );
return ;
}
2024-06-04 13:05:44 -07:00
switch ( direction ) {
2024-06-11 13:03:41 -07:00
case DropDirection.OuterTop :
if ( node . flexDirection === FlexDirection . Column ) {
const grandparentNode = grandparent ();
if ( grandparentNode ) {
const index = indexInGrandparent ();
2024-06-17 14:14:09 -07:00
newMoveOperation = {
2024-06-11 13:03:41 -07:00
parentId : grandparentNode.id ,
node : nodeToMove ,
index ,
};
break ;
}
}
2024-06-04 13:05:44 -07:00
case DropDirection.Top :
if ( node . flexDirection === FlexDirection . Column ) {
2024-09-10 17:23:28 -07:00
newMoveOperation = { parentId : nodeId , index : 0 , node : nodeToMove };
2024-06-04 13:05:44 -07:00
} else {
if ( isRoot )
2024-06-17 14:14:09 -07:00
newMoveOperation = {
2024-06-04 13:05:44 -07:00
node : nodeToMove ,
index : 0 ,
insertAtRoot : true ,
};
const parentNode = parent ();
if ( parentNode )
2024-06-17 14:14:09 -07:00
newMoveOperation = {
2024-06-04 13:05:44 -07:00
parentId : parentNode.id ,
index : indexInParent () ?? 0 ,
node : nodeToMove ,
};
}
break ;
2024-06-11 13:03:41 -07:00
case DropDirection.OuterBottom :
if ( node . flexDirection === FlexDirection . Column ) {
const grandparentNode = grandparent ();
if ( grandparentNode ) {
const index = indexInGrandparent () + 1 ;
2024-06-17 14:14:09 -07:00
newMoveOperation = {
2024-06-11 13:03:41 -07:00
parentId : grandparentNode.id ,
node : nodeToMove ,
index ,
};
break ;
}
}
2024-06-04 13:05:44 -07:00
case DropDirection.Bottom :
if ( node . flexDirection === FlexDirection . Column ) {
2024-09-10 17:23:28 -07:00
newMoveOperation = { parentId : nodeId , index : 1 , node : nodeToMove };
2024-06-04 13:05:44 -07:00
} else {
if ( isRoot )
2024-06-17 14:14:09 -07:00
newMoveOperation = {
2024-06-04 13:05:44 -07:00
node : nodeToMove ,
index : 1 ,
insertAtRoot : true ,
};
const parentNode = parent ();
if ( parentNode )
2024-06-17 14:14:09 -07:00
newMoveOperation = {
2024-06-04 13:05:44 -07:00
parentId : parentNode.id ,
index : indexInParent () + 1 ,
node : nodeToMove ,
};
}
break ;
2024-06-11 13:03:41 -07:00
case DropDirection.OuterLeft :
if ( node . flexDirection === FlexDirection . Row ) {
const grandparentNode = grandparent ();
if ( grandparentNode ) {
const index = indexInGrandparent ();
2024-06-17 14:14:09 -07:00
newMoveOperation = {
2024-06-11 13:03:41 -07:00
parentId : grandparentNode.id ,
node : nodeToMove ,
index ,
};
break ;
}
}
2024-06-04 13:05:44 -07:00
case DropDirection.Left :
if ( node . flexDirection === FlexDirection . Row ) {
2024-09-10 17:23:28 -07:00
newMoveOperation = { parentId : nodeId , index : 0 , node : nodeToMove };
2024-06-04 13:05:44 -07:00
} else {
const parentNode = parent ();
if ( parentNode )
2024-06-17 14:14:09 -07:00
newMoveOperation = {
2024-06-04 13:05:44 -07:00
parentId : parentNode.id ,
index : indexInParent (),
node : nodeToMove ,
};
}
break ;
2024-06-11 13:03:41 -07:00
case DropDirection.OuterRight :
if ( node . flexDirection === FlexDirection . Row ) {
const grandparentNode = grandparent ();
if ( grandparentNode ) {
const index = indexInGrandparent () + 1 ;
2024-06-17 14:14:09 -07:00
newMoveOperation = {
2024-06-11 13:03:41 -07:00
parentId : grandparentNode.id ,
node : nodeToMove ,
index ,
};
break ;
}
}
2024-06-04 13:05:44 -07:00
case DropDirection.Right :
if ( node . flexDirection === FlexDirection . Row ) {
2024-09-10 17:23:28 -07:00
newMoveOperation = { parentId : nodeId , index : 1 , node : nodeToMove };
2024-06-04 13:05:44 -07:00
} else {
const parentNode = parent ();
if ( parentNode )
2024-06-17 14:14:09 -07:00
newMoveOperation = {
2024-06-04 13:05:44 -07:00
parentId : parentNode.id ,
index : indexInParent () + 1 ,
node : nodeToMove ,
};
}
break ;
2024-06-11 13:03:41 -07:00
case DropDirection.Center :
2024-09-10 17:23:28 -07:00
if ( nodeId !== rootNode . id && nodeToMoveId !== rootNode . id ) {
2024-07-03 14:31:02 -07:00
const swapAction : LayoutTreeSwapNodeAction = {
2024-06-17 14:14:09 -07:00
type : LayoutTreeActionType . Swap ,
2024-09-10 17:23:28 -07:00
node1Id : nodeId ,
node2Id : nodeToMoveId ,
2024-06-17 14:14:09 -07:00
};
2024-08-14 18:40:41 -07:00
return swapAction ;
2024-06-17 14:14:09 -07:00
} else {
console . warn ( "cannot swap" );
}
2024-06-11 13:03:41 -07:00
break ;
2024-06-04 13:05:44 -07:00
default :
2024-06-11 13:03:41 -07:00
throw new Error ( `Invalid direction: ${ direction } ` );
2024-06-04 13:05:44 -07:00
}
2024-06-18 16:03:00 -07:00
if (
newMoveOperation ? . parentId !== nodeToMoveParent () ? . id ||
( newMoveOperation . index !== nodeToMoveIndexInParent () &&
newMoveOperation . index !== nodeToMoveIndexInParent () + 1 )
)
2024-08-14 18:40:41 -07:00
return {
2024-06-17 14:14:09 -07:00
type : LayoutTreeActionType . Move ,
... newMoveOperation ,
2024-08-14 18:40:41 -07:00
} as LayoutTreeMoveNodeAction ;
2024-06-04 13:05:44 -07:00
}
2024-08-14 18:40:41 -07:00
export function moveNode ( layoutState : LayoutTreeState , action : LayoutTreeMoveNodeAction ) {
2024-09-10 17:23:28 -07:00
console . log ( "moveNode" , layoutState , action );
2024-08-14 18:40:41 -07:00
const rootNode = layoutState . rootNode ;
2024-06-04 13:05:44 -07:00
if ( ! action ) {
console . error ( "no move node action provided" );
return ;
}
if ( action . parentId && action . insertAtRoot ) {
console . error ( "parent and insertAtRoot cannot both be defined in a move node action" );
return ;
}
2024-06-17 14:14:09 -07:00
const node = findNode ( rootNode , action . node . id ) ?? action . node ;
const parent = findNode ( rootNode , action . parentId );
const oldParent = findParent ( rootNode , action . node . id );
2024-06-04 13:05:44 -07:00
2024-06-17 14:14:09 -07:00
let startingIndex = 0 ;
// If moving under the same parent, we need to make sure that we are removing the child from its old position, not its new one.
// If the new index is before the old index, we need to start our search for the node to delete after the new index position.
2024-07-03 14:31:02 -07:00
// If a node is being moved under the same parent, it can keep its size. Otherwise, it should get reset.
if ( oldParent && parent ) {
if ( oldParent . id === parent . id ) {
const curIndexInParent = parent . children ! . indexOf ( node );
if ( curIndexInParent >= action . index ) {
startingIndex = action . index + 1 ;
}
} else {
node . size = DefaultNodeSize ;
2024-06-17 14:14:09 -07:00
}
}
2024-06-04 13:05:44 -07:00
if ( ! parent && action . insertAtRoot ) {
2024-06-11 13:03:41 -07:00
if ( ! rootNode . children ) {
addIntermediateNode ( rootNode );
}
2024-06-04 13:05:44 -07:00
addChildAt ( rootNode , action . index , node );
} else if ( parent ) {
addChildAt ( parent , action . index , node );
} else {
throw new Error ( "Invalid InsertOperation" );
}
2024-06-11 15:10:20 -07:00
// Remove nodeToInsert from its old parent
if ( oldParent ) {
2024-06-17 14:14:09 -07:00
removeChild ( oldParent , node , startingIndex );
2024-06-11 15:10:20 -07:00
}
2024-08-26 11:56:00 -07:00
layoutState . generation ++ ;
2024-06-04 13:05:44 -07:00
}
2024-08-14 18:40:41 -07:00
export function insertNode ( layoutState : LayoutTreeState , action : LayoutTreeInsertNodeAction ) {
2024-06-04 13:05:44 -07:00
if ( ! action ? . node ) {
2024-07-31 21:27:46 -07:00
console . error ( "insertNode cannot run, no insert node action provided" );
2024-06-04 13:05:44 -07:00
return ;
}
2024-08-14 18:40:41 -07:00
if ( ! layoutState . rootNode ) {
2024-08-20 20:14:14 -07:00
layoutState . rootNode = action . node ;
2024-08-26 11:56:00 -07:00
} else {
2024-08-26 12:32:28 -07:00
const insertLoc = findNextInsertLocation ( layoutState . rootNode , DEFAULT_MAX_CHILDREN );
2024-08-26 11:56:00 -07:00
addChildAt ( insertLoc . node , insertLoc . index , action . node );
if ( action . magnified ) {
layoutState . magnifiedNodeId = action . node . id ;
2024-08-27 23:16:07 -07:00
layoutState . focusedNodeId = action . node . id ;
2024-08-26 11:56:00 -07:00
}
2024-08-27 23:16:07 -07:00
}
if ( action . focused ) {
2024-08-26 11:56:00 -07:00
layoutState . focusedNodeId = action . node . id ;
2024-08-20 20:14:14 -07:00
}
2024-08-26 11:56:00 -07:00
layoutState . generation ++ ;
2024-06-04 13:05:44 -07:00
}
2024-08-14 18:40:41 -07:00
export function insertNodeAtIndex ( layoutState : LayoutTreeState , action : LayoutTreeInsertNodeAtIndexAction ) {
2024-07-31 21:27:46 -07:00
if ( ! action ? . node || ! action ? . indexArr ) {
console . error ( "insertNodeAtIndex cannot run, either node or indexArr field is missing" );
return ;
}
2024-08-14 18:40:41 -07:00
if ( ! layoutState . rootNode ) {
2024-08-20 20:14:14 -07:00
layoutState . rootNode = action . node ;
2024-08-26 11:56:00 -07:00
} else {
const insertLoc = findInsertLocationFromIndexArr ( layoutState . rootNode , action . indexArr );
if ( ! insertLoc ) {
console . error ( "insertNodeAtIndex unable to find insert location" );
return ;
}
addChildAt ( insertLoc . node , insertLoc . index + 1 , action . node );
if ( action . magnified ) {
layoutState . magnifiedNodeId = action . node . id ;
2024-08-27 23:16:07 -07:00
layoutState . focusedNodeId = action . node . id ;
2024-08-26 11:56:00 -07:00
}
2024-08-27 23:16:07 -07:00
}
if ( action . focused ) {
2024-08-26 11:56:00 -07:00
layoutState . focusedNodeId = action . node . id ;
2024-08-20 20:14:14 -07:00
}
2024-08-26 11:56:00 -07:00
layoutState . generation ++ ;
2024-07-31 21:27:46 -07:00
}
2024-08-14 18:40:41 -07:00
export function swapNode ( layoutState : LayoutTreeState , action : LayoutTreeSwapNodeAction ) {
2024-07-03 14:31:02 -07:00
if ( ! action . node1Id || ! action . node2Id ) {
2024-06-17 14:14:09 -07:00
console . error ( "invalid swapNode action, both node1 and node2 must be defined" );
return ;
}
2024-07-03 14:31:02 -07:00
2024-08-14 18:40:41 -07:00
if ( action . node1Id === layoutState . rootNode . id || action . node2Id === layoutState . rootNode . id ) {
2024-06-17 14:14:09 -07:00
console . error ( "invalid swapNode action, the root node cannot be swapped" );
return ;
}
2024-07-03 14:31:02 -07:00
if ( action . node1Id === action . node2Id ) {
2024-06-17 14:14:09 -07:00
console . error ( "invalid swapNode action, node1 and node2 are equal" );
return ;
}
2024-08-14 18:40:41 -07:00
const parentNode1 = findParent ( layoutState . rootNode , action . node1Id );
const parentNode2 = findParent ( layoutState . rootNode , action . node2Id );
2024-07-03 14:31:02 -07:00
const parentNode1Index = parentNode1 . children ! . findIndex (( child ) => child . id === action . node1Id );
const parentNode2Index = parentNode2 . children ! . findIndex (( child ) => child . id === action . node2Id );
2024-06-17 14:14:09 -07:00
2024-07-03 14:31:02 -07:00
const node1 = parentNode1 . children ! [ parentNode1Index ];
const node2 = parentNode2 . children ! [ parentNode2Index ];
const node1Size = node1 . size ;
node1 . size = node2 . size ;
node2 . size = node1Size ;
parentNode1 . children [ parentNode1Index ] = node2 ;
parentNode2 . children [ parentNode2Index ] = node1 ;
2024-08-26 11:56:00 -07:00
layoutState . generation ++ ;
2024-06-17 14:14:09 -07:00
}
2024-08-14 18:40:41 -07:00
export function deleteNode ( layoutState : LayoutTreeState , action : LayoutTreeDeleteNodeAction ) {
2024-06-04 13:05:44 -07:00
if ( ! action ? . nodeId ) {
console . error ( "no delete node action provided" );
return ;
}
2024-08-14 18:40:41 -07:00
if ( ! layoutState . rootNode ) {
2024-06-04 13:05:44 -07:00
console . error ( "no root node" );
return ;
}
2024-08-14 18:40:41 -07:00
if ( layoutState . rootNode . id === action . nodeId ) {
layoutState . rootNode = undefined ;
2024-06-04 13:05:44 -07:00
} else {
2024-08-26 11:56:00 -07:00
const parent = findParent ( layoutState . rootNode , action . nodeId );
if ( parent ) {
const node = parent . children . find (( child ) => child . id === action . nodeId );
removeChild ( parent , node );
if ( layoutState . focusedNodeId === node . id ) {
layoutState . focusedNodeId = undefined ;
}
} else {
console . error ( "unable to delete node, not found in tree" );
}
2024-06-04 13:05:44 -07:00
}
2024-08-26 11:56:00 -07:00
layoutState . generation ++ ;
2024-06-04 13:05:44 -07:00
}
2024-07-03 14:31:02 -07:00
2024-08-14 18:40:41 -07:00
export function resizeNode ( layoutState : LayoutTreeState , action : LayoutTreeResizeNodeAction ) {
2024-07-03 14:31:02 -07:00
if ( ! action . resizeOperations ) {
console . error ( "invalid resizeNode operation. nodeSizes array must be defined." );
}
for ( const resize of action . resizeOperations ) {
if ( ! resize . nodeId || resize . size < 0 || resize . size > 100 ) {
console . error ( "invalid resizeNode operation. nodeId must be defined and size must be between 0 and 100" );
return ;
}
2024-08-14 18:40:41 -07:00
const node = findNode ( layoutState . rootNode , resize . nodeId );
2024-07-03 14:31:02 -07:00
node . size = resize . size ;
}
2024-08-26 11:56:00 -07:00
layoutState . generation ++ ;
}
export function focusNode ( layoutState : LayoutTreeState , action : LayoutTreeFocusNodeAction ) {
if ( ! action . nodeId ) {
console . error ( "invalid focusNode operation, nodeId must be defined." );
return ;
}
layoutState . focusedNodeId = action . nodeId ;
layoutState . generation ++ ;
2024-07-03 14:31:02 -07:00
}
2024-07-30 10:59:53 -07:00
2024-08-14 18:40:41 -07:00
export function magnifyNodeToggle ( layoutState : LayoutTreeState , action : LayoutTreeMagnifyNodeToggleAction ) {
2024-07-30 10:59:53 -07:00
if ( ! action . nodeId ) {
console . error ( "invalid magnifyNodeToggle operation. nodeId must be defined." );
return ;
}
2024-08-14 18:40:41 -07:00
if ( layoutState . rootNode . id === action . nodeId ) {
2024-07-30 10:59:53 -07:00
console . warn ( `cannot toggle magnification of node ${ action . nodeId } because it is the root node.` );
return ;
}
2024-08-14 18:40:41 -07:00
if ( layoutState . magnifiedNodeId === action . nodeId ) {
layoutState . magnifiedNodeId = undefined ;
2024-07-30 10:59:53 -07:00
} else {
2024-08-14 18:40:41 -07:00
layoutState . magnifiedNodeId = action . nodeId ;
2024-08-26 11:56:00 -07:00
layoutState . focusedNodeId = action . nodeId ;
2024-07-30 10:59:53 -07:00
}
2024-08-26 11:56:00 -07:00
layoutState . generation ++ ;
2024-07-30 10:59:53 -07:00
}
2024-09-16 17:26:37 -07:00
export function clearTree ( layoutState : LayoutTreeState ) {
layoutState . rootNode = undefined ;
layoutState . leafOrder = undefined ;
layoutState . focusedNodeId = undefined ;
layoutState . magnifiedNodeId = undefined ;
layoutState . generation ++ ;
}