mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-22 16:48:23 +01:00
428 lines
15 KiB
TypeScript
428 lines
15 KiB
TypeScript
// Copyright 2024, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
import { lazy } from "@/util/util";
|
|
import {
|
|
addChildAt,
|
|
addIntermediateNode,
|
|
findInsertLocationFromIndexArr,
|
|
findNextInsertLocation,
|
|
findNode,
|
|
findParent,
|
|
removeChild,
|
|
} from "./layoutNode";
|
|
import {
|
|
DefaultNodeSize,
|
|
DropDirection,
|
|
FlexDirection,
|
|
LayoutTreeActionType,
|
|
LayoutTreeComputeMoveNodeAction,
|
|
LayoutTreeDeleteNodeAction,
|
|
LayoutTreeFocusNodeAction,
|
|
LayoutTreeInsertNodeAction,
|
|
LayoutTreeInsertNodeAtIndexAction,
|
|
LayoutTreeMagnifyNodeToggleAction,
|
|
LayoutTreeMoveNodeAction,
|
|
LayoutTreeResizeNodeAction,
|
|
LayoutTreeState,
|
|
LayoutTreeSwapNodeAction,
|
|
MoveOperation,
|
|
} from "./types";
|
|
|
|
export const DEFAULT_MAX_CHILDREN = 5;
|
|
|
|
/**
|
|
* Computes an operation for inserting a new node into the tree in the given direction relative to the specified node.
|
|
*
|
|
* @param layoutState The state of the tree.
|
|
* @param computeInsertAction The operation to compute.
|
|
*/
|
|
export function computeMoveNode(layoutState: LayoutTreeState, computeInsertAction: LayoutTreeComputeMoveNodeAction) {
|
|
const rootNode = layoutState.rootNode;
|
|
const { nodeId, nodeToMoveId, direction } = computeInsertAction;
|
|
if (!nodeId || !nodeToMoveId) {
|
|
console.warn("either nodeId or nodeToMoveId not set", nodeId, nodeToMoveId);
|
|
return;
|
|
}
|
|
if (direction === undefined) {
|
|
console.warn("No direction provided for insertItemInDirection");
|
|
return;
|
|
}
|
|
|
|
if (nodeId === nodeToMoveId) {
|
|
console.warn("Cannot compute move node action since both nodes are equal");
|
|
return;
|
|
}
|
|
|
|
let newMoveOperation: MoveOperation;
|
|
const parent = lazy(() => findParent(rootNode, nodeId));
|
|
const grandparent = lazy(() => findParent(rootNode, parent().id));
|
|
const indexInParent = lazy(() => parent()?.children.findIndex((child) => nodeId === child.id));
|
|
const indexInGrandparent = lazy(() => grandparent()?.children.findIndex((child) => parent().id === child.id));
|
|
const nodeToMoveParent = lazy(() => findParent(rootNode, nodeToMoveId));
|
|
const nodeToMoveIndexInParent = lazy(() =>
|
|
nodeToMoveParent()?.children.findIndex((child) => nodeToMoveId === child.id)
|
|
);
|
|
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;
|
|
}
|
|
|
|
switch (direction) {
|
|
case DropDirection.OuterTop:
|
|
if (node.flexDirection === FlexDirection.Column) {
|
|
const grandparentNode = grandparent();
|
|
if (grandparentNode) {
|
|
const index = indexInGrandparent();
|
|
newMoveOperation = {
|
|
parentId: grandparentNode.id,
|
|
node: nodeToMove,
|
|
index,
|
|
};
|
|
break;
|
|
}
|
|
}
|
|
case DropDirection.Top:
|
|
if (node.flexDirection === FlexDirection.Column) {
|
|
newMoveOperation = { parentId: nodeId, index: 0, node: nodeToMove };
|
|
} else {
|
|
if (isRoot)
|
|
newMoveOperation = {
|
|
node: nodeToMove,
|
|
index: 0,
|
|
insertAtRoot: true,
|
|
};
|
|
|
|
const parentNode = parent();
|
|
if (parentNode)
|
|
newMoveOperation = {
|
|
parentId: parentNode.id,
|
|
index: indexInParent() ?? 0,
|
|
node: nodeToMove,
|
|
};
|
|
}
|
|
break;
|
|
case DropDirection.OuterBottom:
|
|
if (node.flexDirection === FlexDirection.Column) {
|
|
const grandparentNode = grandparent();
|
|
if (grandparentNode) {
|
|
const index = indexInGrandparent() + 1;
|
|
newMoveOperation = {
|
|
parentId: grandparentNode.id,
|
|
node: nodeToMove,
|
|
index,
|
|
};
|
|
break;
|
|
}
|
|
}
|
|
case DropDirection.Bottom:
|
|
if (node.flexDirection === FlexDirection.Column) {
|
|
newMoveOperation = { parentId: nodeId, index: 1, node: nodeToMove };
|
|
} else {
|
|
if (isRoot)
|
|
newMoveOperation = {
|
|
node: nodeToMove,
|
|
index: 1,
|
|
insertAtRoot: true,
|
|
};
|
|
|
|
const parentNode = parent();
|
|
if (parentNode)
|
|
newMoveOperation = {
|
|
parentId: parentNode.id,
|
|
index: indexInParent() + 1,
|
|
node: nodeToMove,
|
|
};
|
|
}
|
|
break;
|
|
case DropDirection.OuterLeft:
|
|
if (node.flexDirection === FlexDirection.Row) {
|
|
const grandparentNode = grandparent();
|
|
if (grandparentNode) {
|
|
const index = indexInGrandparent();
|
|
newMoveOperation = {
|
|
parentId: grandparentNode.id,
|
|
node: nodeToMove,
|
|
index,
|
|
};
|
|
break;
|
|
}
|
|
}
|
|
case DropDirection.Left:
|
|
if (node.flexDirection === FlexDirection.Row) {
|
|
newMoveOperation = { parentId: nodeId, index: 0, node: nodeToMove };
|
|
} else {
|
|
const parentNode = parent();
|
|
if (parentNode)
|
|
newMoveOperation = {
|
|
parentId: parentNode.id,
|
|
index: indexInParent(),
|
|
node: nodeToMove,
|
|
};
|
|
}
|
|
break;
|
|
case DropDirection.OuterRight:
|
|
if (node.flexDirection === FlexDirection.Row) {
|
|
const grandparentNode = grandparent();
|
|
if (grandparentNode) {
|
|
const index = indexInGrandparent() + 1;
|
|
newMoveOperation = {
|
|
parentId: grandparentNode.id,
|
|
node: nodeToMove,
|
|
index,
|
|
};
|
|
break;
|
|
}
|
|
}
|
|
case DropDirection.Right:
|
|
if (node.flexDirection === FlexDirection.Row) {
|
|
newMoveOperation = { parentId: nodeId, index: 1, node: nodeToMove };
|
|
} else {
|
|
const parentNode = parent();
|
|
if (parentNode)
|
|
newMoveOperation = {
|
|
parentId: parentNode.id,
|
|
index: indexInParent() + 1,
|
|
node: nodeToMove,
|
|
};
|
|
}
|
|
break;
|
|
case DropDirection.Center:
|
|
if (nodeId !== rootNode.id && nodeToMoveId !== rootNode.id) {
|
|
const swapAction: LayoutTreeSwapNodeAction = {
|
|
type: LayoutTreeActionType.Swap,
|
|
node1Id: nodeId,
|
|
node2Id: nodeToMoveId,
|
|
};
|
|
return swapAction;
|
|
} else {
|
|
console.warn("cannot swap");
|
|
}
|
|
break;
|
|
default:
|
|
throw new Error(`Invalid direction: ${direction}`);
|
|
}
|
|
|
|
if (
|
|
newMoveOperation?.parentId !== nodeToMoveParent()?.id ||
|
|
(newMoveOperation.index !== nodeToMoveIndexInParent() &&
|
|
newMoveOperation.index !== nodeToMoveIndexInParent() + 1)
|
|
)
|
|
return {
|
|
type: LayoutTreeActionType.Move,
|
|
...newMoveOperation,
|
|
} as LayoutTreeMoveNodeAction;
|
|
}
|
|
|
|
export function moveNode(layoutState: LayoutTreeState, action: LayoutTreeMoveNodeAction) {
|
|
console.log("moveNode", layoutState, action);
|
|
const rootNode = layoutState.rootNode;
|
|
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;
|
|
}
|
|
|
|
const node = findNode(rootNode, action.node.id) ?? action.node;
|
|
const parent = findNode(rootNode, action.parentId);
|
|
const oldParent = findParent(rootNode, action.node.id);
|
|
|
|
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.
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
if (!parent && action.insertAtRoot) {
|
|
if (!rootNode.children) {
|
|
addIntermediateNode(rootNode);
|
|
}
|
|
addChildAt(rootNode, action.index, node);
|
|
} else if (parent) {
|
|
addChildAt(parent, action.index, node);
|
|
} else {
|
|
throw new Error("Invalid InsertOperation");
|
|
}
|
|
|
|
// Remove nodeToInsert from its old parent
|
|
if (oldParent) {
|
|
removeChild(oldParent, node, startingIndex);
|
|
}
|
|
layoutState.generation++;
|
|
}
|
|
|
|
export function insertNode(layoutState: LayoutTreeState, action: LayoutTreeInsertNodeAction) {
|
|
if (!action?.node) {
|
|
console.error("insertNode cannot run, no insert node action provided");
|
|
return;
|
|
}
|
|
if (!layoutState.rootNode) {
|
|
layoutState.rootNode = action.node;
|
|
} else {
|
|
const insertLoc = findNextInsertLocation(layoutState.rootNode, DEFAULT_MAX_CHILDREN);
|
|
addChildAt(insertLoc.node, insertLoc.index, action.node);
|
|
if (action.magnified) {
|
|
layoutState.magnifiedNodeId = action.node.id;
|
|
layoutState.focusedNodeId = action.node.id;
|
|
}
|
|
}
|
|
if (action.focused) {
|
|
layoutState.focusedNodeId = action.node.id;
|
|
}
|
|
layoutState.generation++;
|
|
}
|
|
|
|
export function insertNodeAtIndex(layoutState: LayoutTreeState, action: LayoutTreeInsertNodeAtIndexAction) {
|
|
if (!action?.node || !action?.indexArr) {
|
|
console.error("insertNodeAtIndex cannot run, either node or indexArr field is missing");
|
|
return;
|
|
}
|
|
if (!layoutState.rootNode) {
|
|
layoutState.rootNode = action.node;
|
|
} 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;
|
|
layoutState.focusedNodeId = action.node.id;
|
|
}
|
|
}
|
|
if (action.focused) {
|
|
layoutState.focusedNodeId = action.node.id;
|
|
}
|
|
layoutState.generation++;
|
|
}
|
|
|
|
export function swapNode(layoutState: LayoutTreeState, action: LayoutTreeSwapNodeAction) {
|
|
if (!action.node1Id || !action.node2Id) {
|
|
console.error("invalid swapNode action, both node1 and node2 must be defined");
|
|
return;
|
|
}
|
|
|
|
if (action.node1Id === layoutState.rootNode.id || action.node2Id === layoutState.rootNode.id) {
|
|
console.error("invalid swapNode action, the root node cannot be swapped");
|
|
return;
|
|
}
|
|
if (action.node1Id === action.node2Id) {
|
|
console.error("invalid swapNode action, node1 and node2 are equal");
|
|
return;
|
|
}
|
|
|
|
const parentNode1 = findParent(layoutState.rootNode, action.node1Id);
|
|
const parentNode2 = findParent(layoutState.rootNode, action.node2Id);
|
|
const parentNode1Index = parentNode1.children!.findIndex((child) => child.id === action.node1Id);
|
|
const parentNode2Index = parentNode2.children!.findIndex((child) => child.id === action.node2Id);
|
|
|
|
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;
|
|
layoutState.generation++;
|
|
}
|
|
|
|
export function deleteNode(layoutState: LayoutTreeState, action: LayoutTreeDeleteNodeAction) {
|
|
if (!action?.nodeId) {
|
|
console.error("no delete node action provided");
|
|
return;
|
|
}
|
|
if (!layoutState.rootNode) {
|
|
console.error("no root node");
|
|
return;
|
|
}
|
|
if (layoutState.rootNode.id === action.nodeId) {
|
|
layoutState.rootNode = undefined;
|
|
} else {
|
|
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");
|
|
}
|
|
}
|
|
|
|
layoutState.generation++;
|
|
}
|
|
|
|
export function resizeNode(layoutState: LayoutTreeState, action: LayoutTreeResizeNodeAction) {
|
|
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;
|
|
}
|
|
const node = findNode(layoutState.rootNode, resize.nodeId);
|
|
node.size = resize.size;
|
|
}
|
|
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++;
|
|
}
|
|
|
|
export function magnifyNodeToggle(layoutState: LayoutTreeState, action: LayoutTreeMagnifyNodeToggleAction) {
|
|
if (!action.nodeId) {
|
|
console.error("invalid magnifyNodeToggle operation. nodeId must be defined.");
|
|
return;
|
|
}
|
|
if (layoutState.rootNode.id === action.nodeId) {
|
|
console.warn(`cannot toggle magnification of node ${action.nodeId} because it is the root node.`);
|
|
return;
|
|
}
|
|
if (layoutState.magnifiedNodeId === action.nodeId) {
|
|
layoutState.magnifiedNodeId = undefined;
|
|
} else {
|
|
layoutState.magnifiedNodeId = action.nodeId;
|
|
layoutState.focusedNodeId = action.nodeId;
|
|
}
|
|
layoutState.generation++;
|
|
}
|
|
|
|
export function clearTree(layoutState: LayoutTreeState) {
|
|
layoutState.rootNode = undefined;
|
|
layoutState.leafOrder = undefined;
|
|
layoutState.focusedNodeId = undefined;
|
|
layoutState.magnifiedNodeId = undefined;
|
|
layoutState.generation++;
|
|
}
|