mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
Add Swap Node functionality (#56)
Adds the ability to swap nodes by dragging to the center of a tile. Also fixes a bug where moving a node to a new lesser index under the same parent would produce a no-op.
This commit is contained in:
parent
b71ae8e6e8
commit
48d4611a05
@ -29,6 +29,7 @@ import {
|
||||
LayoutTreeDeleteNodeAction,
|
||||
LayoutTreeMoveNodeAction,
|
||||
LayoutTreeState,
|
||||
LayoutTreeSwapNodeAction,
|
||||
PreviewRenderer,
|
||||
WritableLayoutTreeStateAtom,
|
||||
} from "./model";
|
||||
@ -518,68 +519,98 @@ const Placeholder = <T,>({ layoutTreeState, overlayContainerRef, nodeRefs, style
|
||||
|
||||
useEffect(() => {
|
||||
let newPlaceholderOverlay: ReactNode;
|
||||
if (layoutTreeState?.pendingAction?.type === LayoutTreeActionType.Move && overlayContainerRef?.current) {
|
||||
const action = layoutTreeState.pendingAction as LayoutTreeMoveNodeAction<T>;
|
||||
let parentId: string;
|
||||
if (action.insertAtRoot) {
|
||||
parentId = layoutTreeState.rootNode.id;
|
||||
} else {
|
||||
parentId = action.parentId;
|
||||
}
|
||||
if (overlayContainerRef?.current) {
|
||||
switch (layoutTreeState?.pendingAction?.type) {
|
||||
case LayoutTreeActionType.Move: {
|
||||
const action = layoutTreeState.pendingAction as LayoutTreeMoveNodeAction<T>;
|
||||
let parentId: string;
|
||||
if (action.insertAtRoot) {
|
||||
parentId = layoutTreeState.rootNode.id;
|
||||
} else {
|
||||
parentId = action.parentId;
|
||||
}
|
||||
|
||||
const parentNode = findNode(layoutTreeState.rootNode, parentId);
|
||||
if (action.index !== undefined && parentNode) {
|
||||
const targetIndex = Math.min(
|
||||
parentNode.children ? parentNode.children.length - 1 : 0,
|
||||
Math.max(0, action.index - 1)
|
||||
);
|
||||
let targetNode = parentNode?.children?.at(targetIndex);
|
||||
let targetRef: React.RefObject<HTMLElement>;
|
||||
if (targetNode) {
|
||||
targetRef = nodeRefs.get(targetNode.id);
|
||||
} else {
|
||||
targetRef = nodeRefs.get(parentNode.id);
|
||||
targetNode = parentNode;
|
||||
}
|
||||
if (targetRef?.current) {
|
||||
const overlayBoundingRect = overlayContainerRef.current.getBoundingClientRect();
|
||||
const targetBoundingRect = targetRef.current.getBoundingClientRect();
|
||||
|
||||
// Placeholder should be either half the height or half the width of the targetNode, depending on the flex direction of the targetNode's parent.
|
||||
// Default to placing the placeholder in the first half of the target node.
|
||||
const placeholderDimensions: Dimensions = {
|
||||
height:
|
||||
parentNode.flexDirection === FlexDirection.Column
|
||||
? targetBoundingRect.height / 2
|
||||
: targetBoundingRect.height,
|
||||
width:
|
||||
parentNode.flexDirection === FlexDirection.Row
|
||||
? targetBoundingRect.width / 2
|
||||
: targetBoundingRect.width,
|
||||
top: targetBoundingRect.top - overlayBoundingRect.top,
|
||||
left: targetBoundingRect.left - overlayBoundingRect.left,
|
||||
};
|
||||
|
||||
if (action.index > targetIndex) {
|
||||
if (action.index >= (parentNode.children?.length ?? 1)) {
|
||||
// If there are no more nodes after the specified index, place the placeholder in the second half of the target node (either right or bottom).
|
||||
placeholderDimensions.top +=
|
||||
parentNode.flexDirection === FlexDirection.Column && targetBoundingRect.height / 2;
|
||||
placeholderDimensions.left +=
|
||||
parentNode.flexDirection === FlexDirection.Row && targetBoundingRect.width / 2;
|
||||
const parentNode = findNode(layoutTreeState.rootNode, parentId);
|
||||
if (action.index !== undefined && parentNode) {
|
||||
const targetIndex = Math.min(
|
||||
parentNode.children ? parentNode.children.length - 1 : 0,
|
||||
Math.max(0, action.index - 1)
|
||||
);
|
||||
let targetNode = parentNode?.children?.at(targetIndex);
|
||||
let targetRef: React.RefObject<HTMLElement>;
|
||||
if (targetNode) {
|
||||
targetRef = nodeRefs.get(targetNode.id);
|
||||
} else {
|
||||
// Otherwise, place the placeholder between the target node (the one after which it will be inserted) and the next node
|
||||
placeholderDimensions.top +=
|
||||
parentNode.flexDirection === FlexDirection.Column &&
|
||||
(3 * targetBoundingRect.height) / 4;
|
||||
placeholderDimensions.left +=
|
||||
parentNode.flexDirection === FlexDirection.Row && (3 * targetBoundingRect.width) / 4;
|
||||
targetRef = nodeRefs.get(parentNode.id);
|
||||
targetNode = parentNode;
|
||||
}
|
||||
if (targetRef?.current) {
|
||||
const overlayBoundingRect = overlayContainerRef.current.getBoundingClientRect();
|
||||
const targetBoundingRect = targetRef.current.getBoundingClientRect();
|
||||
|
||||
// Placeholder should be either half the height or half the width of the targetNode, depending on the flex direction of the targetNode's parent.
|
||||
// Default to placing the placeholder in the first half of the target node.
|
||||
const placeholderDimensions: Dimensions = {
|
||||
height:
|
||||
parentNode.flexDirection === FlexDirection.Column
|
||||
? targetBoundingRect.height / 2
|
||||
: targetBoundingRect.height,
|
||||
width:
|
||||
parentNode.flexDirection === FlexDirection.Row
|
||||
? targetBoundingRect.width / 2
|
||||
: targetBoundingRect.width,
|
||||
top: targetBoundingRect.top - overlayBoundingRect.top,
|
||||
left: targetBoundingRect.left - overlayBoundingRect.left,
|
||||
};
|
||||
|
||||
if (action.index > targetIndex) {
|
||||
if (action.index >= (parentNode.children?.length ?? 1)) {
|
||||
// If there are no more nodes after the specified index, place the placeholder in the second half of the target node (either right or bottom).
|
||||
placeholderDimensions.top +=
|
||||
parentNode.flexDirection === FlexDirection.Column &&
|
||||
targetBoundingRect.height / 2;
|
||||
placeholderDimensions.left +=
|
||||
parentNode.flexDirection === FlexDirection.Row && targetBoundingRect.width / 2;
|
||||
} else {
|
||||
// Otherwise, place the placeholder between the target node (the one after which it will be inserted) and the next node
|
||||
placeholderDimensions.top +=
|
||||
parentNode.flexDirection === FlexDirection.Column &&
|
||||
(3 * targetBoundingRect.height) / 4;
|
||||
placeholderDimensions.left +=
|
||||
parentNode.flexDirection === FlexDirection.Row &&
|
||||
(3 * targetBoundingRect.width) / 4;
|
||||
}
|
||||
}
|
||||
|
||||
const placeholderTransform = createTransform(placeholderDimensions);
|
||||
newPlaceholderOverlay = <div className="placeholder" style={{ ...placeholderTransform }} />;
|
||||
}
|
||||
}
|
||||
const placeholderTransform = createTransform(placeholderDimensions);
|
||||
|
||||
newPlaceholderOverlay = <div className="placeholder" style={{ ...placeholderTransform }} />;
|
||||
break;
|
||||
}
|
||||
case LayoutTreeActionType.Swap: {
|
||||
const action = layoutTreeState.pendingAction as LayoutTreeSwapNodeAction<T>;
|
||||
console.log("placeholder for swap", action);
|
||||
const targetNode = action.node1;
|
||||
const targetRef = nodeRefs.get(targetNode?.id);
|
||||
if (targetRef?.current) {
|
||||
const overlayBoundingRect = overlayContainerRef.current.getBoundingClientRect();
|
||||
const targetBoundingRect = targetRef.current.getBoundingClientRect();
|
||||
const placeholderDimensions: Dimensions = {
|
||||
top: targetBoundingRect.top - overlayBoundingRect.top,
|
||||
left: targetBoundingRect.left - overlayBoundingRect.left,
|
||||
height: targetBoundingRect.height,
|
||||
width: targetBoundingRect.width,
|
||||
};
|
||||
|
||||
const placeholderTransform = createTransform(placeholderDimensions);
|
||||
newPlaceholderOverlay = <div className="placeholder" style={{ ...placeholderTransform }} />;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// No-op
|
||||
break;
|
||||
}
|
||||
}
|
||||
setPlaceholderOverlay(newPlaceholderOverlay);
|
||||
|
@ -99,12 +99,13 @@ export function addIntermediateNode<T>(node: LayoutNode<T>): LayoutNode<T> {
|
||||
* Attempts to remove the specified node from its parent.
|
||||
* @param parent The parent node.
|
||||
* @param childToRemove The node to remove.
|
||||
* @param startingIndex The index in children to start the search from.
|
||||
* @template T The type of data associated with the node.
|
||||
* @returns The updated parent node, or undefined if the node was not found.
|
||||
*/
|
||||
export function removeChild<T>(parent: LayoutNode<T>, childToRemove: LayoutNode<T>) {
|
||||
export function removeChild<T>(parent: LayoutNode<T>, childToRemove: LayoutNode<T>, startingIndex: number = 0) {
|
||||
if (!parent.children) return;
|
||||
const idx = parent.children.indexOf(childToRemove);
|
||||
const idx = parent.children.indexOf(childToRemove, startingIndex);
|
||||
if (idx === -1) return;
|
||||
parent.children?.splice(idx, 1);
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
LayoutTreeInsertNodeAction,
|
||||
LayoutTreeMoveNodeAction,
|
||||
LayoutTreeState,
|
||||
LayoutTreeSwapNodeAction,
|
||||
MoveOperation,
|
||||
} from "./model";
|
||||
import { DropDirection, FlexDirection, lazy } from "./utils";
|
||||
@ -88,6 +89,10 @@ function layoutTreeStateReducerInner<T>(layoutTreeState: LayoutTreeState<T>, act
|
||||
deleteNode(layoutTreeState, action as LayoutTreeDeleteNodeAction);
|
||||
layoutTreeState.generation++;
|
||||
break;
|
||||
case LayoutTreeActionType.Swap:
|
||||
swapNode(layoutTreeState, action as LayoutTreeSwapNodeAction<T>);
|
||||
layoutTreeState.generation++;
|
||||
break;
|
||||
default: {
|
||||
console.error("Invalid reducer action", layoutTreeState, action);
|
||||
}
|
||||
@ -114,7 +119,12 @@ function computeMoveNode<T>(
|
||||
return;
|
||||
}
|
||||
|
||||
let newOperation: MoveOperation<T>;
|
||||
if (node.id === nodeToMove.id) {
|
||||
console.warn("Cannot compute move node action since both nodes are equal");
|
||||
return;
|
||||
}
|
||||
|
||||
let newMoveOperation: MoveOperation<T>;
|
||||
const parent = lazy(() => findParent(rootNode, node.id));
|
||||
const grandparent = lazy(() => findParent(rootNode, parent().id));
|
||||
const indexInParent = lazy(() => parent()?.children.findIndex((child) => node.id === child.id));
|
||||
@ -129,7 +139,7 @@ function computeMoveNode<T>(
|
||||
if (grandparentNode) {
|
||||
console.log("has grandparent", grandparentNode);
|
||||
const index = indexInGrandparent();
|
||||
newOperation = {
|
||||
newMoveOperation = {
|
||||
parentId: grandparentNode.id,
|
||||
node: nodeToMove,
|
||||
index,
|
||||
@ -139,10 +149,10 @@ function computeMoveNode<T>(
|
||||
}
|
||||
case DropDirection.Top:
|
||||
if (node.flexDirection === FlexDirection.Column) {
|
||||
newOperation = { parentId: node.id, index: 0, node: nodeToMove };
|
||||
newMoveOperation = { parentId: node.id, index: 0, node: nodeToMove };
|
||||
} else {
|
||||
if (isRoot)
|
||||
newOperation = {
|
||||
newMoveOperation = {
|
||||
node: nodeToMove,
|
||||
index: 0,
|
||||
insertAtRoot: true,
|
||||
@ -150,7 +160,7 @@ function computeMoveNode<T>(
|
||||
|
||||
const parentNode = parent();
|
||||
if (parentNode)
|
||||
newOperation = {
|
||||
newMoveOperation = {
|
||||
parentId: parentNode.id,
|
||||
index: indexInParent() ?? 0,
|
||||
node: nodeToMove,
|
||||
@ -164,7 +174,7 @@ function computeMoveNode<T>(
|
||||
if (grandparentNode) {
|
||||
console.log("has grandparent", grandparentNode);
|
||||
const index = indexInGrandparent() + 1;
|
||||
newOperation = {
|
||||
newMoveOperation = {
|
||||
parentId: grandparentNode.id,
|
||||
node: nodeToMove,
|
||||
index,
|
||||
@ -174,10 +184,10 @@ function computeMoveNode<T>(
|
||||
}
|
||||
case DropDirection.Bottom:
|
||||
if (node.flexDirection === FlexDirection.Column) {
|
||||
newOperation = { parentId: node.id, index: 1, node: nodeToMove };
|
||||
newMoveOperation = { parentId: node.id, index: 1, node: nodeToMove };
|
||||
} else {
|
||||
if (isRoot)
|
||||
newOperation = {
|
||||
newMoveOperation = {
|
||||
node: nodeToMove,
|
||||
index: 1,
|
||||
insertAtRoot: true,
|
||||
@ -185,7 +195,7 @@ function computeMoveNode<T>(
|
||||
|
||||
const parentNode = parent();
|
||||
if (parentNode)
|
||||
newOperation = {
|
||||
newMoveOperation = {
|
||||
parentId: parentNode.id,
|
||||
index: indexInParent() + 1,
|
||||
node: nodeToMove,
|
||||
@ -199,7 +209,7 @@ function computeMoveNode<T>(
|
||||
if (grandparentNode) {
|
||||
console.log("has grandparent", grandparentNode);
|
||||
const index = indexInGrandparent();
|
||||
newOperation = {
|
||||
newMoveOperation = {
|
||||
parentId: grandparentNode.id,
|
||||
node: nodeToMove,
|
||||
index,
|
||||
@ -209,11 +219,11 @@ function computeMoveNode<T>(
|
||||
}
|
||||
case DropDirection.Left:
|
||||
if (node.flexDirection === FlexDirection.Row) {
|
||||
newOperation = { parentId: node.id, index: 0, node: nodeToMove };
|
||||
newMoveOperation = { parentId: node.id, index: 0, node: nodeToMove };
|
||||
} else {
|
||||
const parentNode = parent();
|
||||
if (parentNode)
|
||||
newOperation = {
|
||||
newMoveOperation = {
|
||||
parentId: parentNode.id,
|
||||
index: indexInParent(),
|
||||
node: nodeToMove,
|
||||
@ -227,7 +237,7 @@ function computeMoveNode<T>(
|
||||
if (grandparentNode) {
|
||||
console.log("has grandparent", grandparentNode);
|
||||
const index = indexInGrandparent() + 1;
|
||||
newOperation = {
|
||||
newMoveOperation = {
|
||||
parentId: grandparentNode.id,
|
||||
node: nodeToMove,
|
||||
index,
|
||||
@ -237,11 +247,11 @@ function computeMoveNode<T>(
|
||||
}
|
||||
case DropDirection.Right:
|
||||
if (node.flexDirection === FlexDirection.Row) {
|
||||
newOperation = { parentId: node.id, index: 1, node: nodeToMove };
|
||||
newMoveOperation = { parentId: node.id, index: 1, node: nodeToMove };
|
||||
} else {
|
||||
const parentNode = parent();
|
||||
if (parentNode)
|
||||
newOperation = {
|
||||
newMoveOperation = {
|
||||
parentId: parentNode.id,
|
||||
index: indexInParent() + 1,
|
||||
node: nodeToMove,
|
||||
@ -249,14 +259,28 @@ function computeMoveNode<T>(
|
||||
}
|
||||
break;
|
||||
case DropDirection.Center:
|
||||
// TODO: handle center drop
|
||||
console.log("center drop");
|
||||
console.log("center drop", rootNode, node, nodeToMove);
|
||||
if (node.id !== rootNode.id && nodeToMove.id !== rootNode.id) {
|
||||
const swapAction: LayoutTreeSwapNodeAction<T> = {
|
||||
type: LayoutTreeActionType.Swap,
|
||||
node1: node,
|
||||
node2: nodeToMove,
|
||||
};
|
||||
console.log("swapAction", swapAction);
|
||||
layoutTreeState.pendingAction = swapAction;
|
||||
} else {
|
||||
console.warn("cannot swap");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Invalid direction: ${direction}`);
|
||||
}
|
||||
|
||||
if (newOperation) layoutTreeState.pendingAction = { type: LayoutTreeActionType.Move, ...newOperation };
|
||||
if (newMoveOperation)
|
||||
layoutTreeState.pendingAction = {
|
||||
type: LayoutTreeActionType.Move,
|
||||
...newMoveOperation,
|
||||
} as LayoutTreeMoveNodeAction<T>;
|
||||
}
|
||||
|
||||
function moveNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeMoveNodeAction<T>) {
|
||||
@ -271,12 +295,23 @@ function moveNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeMove
|
||||
return;
|
||||
}
|
||||
|
||||
let node = findNode(rootNode, action.node.id) ?? action.node;
|
||||
let parent = findNode(rootNode, action.parentId);
|
||||
let oldParent = findParent(rootNode, action.node.id);
|
||||
const node = findNode(rootNode, action.node.id) ?? action.node;
|
||||
const parent = findNode(rootNode, action.parentId);
|
||||
const oldParent = findParent(rootNode, action.node.id);
|
||||
|
||||
console.log(node, parent, oldParent);
|
||||
|
||||
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 (oldParent?.id === parent.id) {
|
||||
const curIndexInParent = parent.children!.indexOf(node);
|
||||
if (curIndexInParent >= action.index) {
|
||||
startingIndex = action.index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!parent && action.insertAtRoot) {
|
||||
if (!rootNode.children) {
|
||||
addIntermediateNode(rootNode);
|
||||
@ -290,7 +325,7 @@ function moveNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeMove
|
||||
|
||||
// Remove nodeToInsert from its old parent
|
||||
if (oldParent) {
|
||||
removeChild(oldParent, node);
|
||||
removeChild(oldParent, node, startingIndex);
|
||||
}
|
||||
|
||||
const { node: newRootNode, leafs } = balanceNode(layoutTreeState.rootNode);
|
||||
@ -317,6 +352,34 @@ function insertNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeIn
|
||||
layoutTreeState.leafs = leafs;
|
||||
}
|
||||
|
||||
function swapNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeSwapNodeAction<T>) {
|
||||
console.log("swapNode", layoutTreeState, action);
|
||||
if (!action.node1 || !action.node2) {
|
||||
console.error("invalid swapNode action, both node1 and node2 must be defined");
|
||||
return;
|
||||
}
|
||||
if (action.node1.id === layoutTreeState.rootNode.id || action.node2.id === layoutTreeState.rootNode.id) {
|
||||
console.error("invalid swapNode action, the root node cannot be swapped");
|
||||
return;
|
||||
}
|
||||
if (action.node1.id === action.node2.id) {
|
||||
console.error("invalid swapNode action, node1 and node2 are equal");
|
||||
return;
|
||||
}
|
||||
|
||||
const parentNode1 = findParent(layoutTreeState.rootNode, action.node1.id);
|
||||
const parentNode2 = findParent(layoutTreeState.rootNode, action.node2.id);
|
||||
const parentNode1Index = parentNode1.children!.findIndex((child) => child.id === action.node1.id);
|
||||
const parentNode2Index = parentNode2.children!.findIndex((child) => child.id === action.node2.id);
|
||||
|
||||
parentNode1.children[parentNode1Index] = action.node2;
|
||||
parentNode2.children[parentNode2Index] = action.node1;
|
||||
|
||||
const { node: newRootNode, leafs } = balanceNode(layoutTreeState.rootNode);
|
||||
layoutTreeState.rootNode = newRootNode;
|
||||
layoutTreeState.leafs = leafs;
|
||||
}
|
||||
|
||||
function deleteNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeDeleteNodeAction) {
|
||||
console.log("deleteNode", layoutTreeState, action);
|
||||
if (!action?.nodeId) {
|
||||
|
@ -35,6 +35,7 @@ export type MoveOperation<T> = {
|
||||
export enum LayoutTreeActionType {
|
||||
ComputeMove = "computeMove",
|
||||
Move = "move",
|
||||
Swap = "swap",
|
||||
CommitPendingAction = "commit",
|
||||
ResizeNode = "resize",
|
||||
InsertNode = "insert",
|
||||
@ -72,6 +73,13 @@ export interface LayoutTreeMoveNodeAction<T> extends LayoutTreeAction, MoveOpera
|
||||
type: LayoutTreeActionType.Move;
|
||||
}
|
||||
|
||||
export interface LayoutTreeSwapNodeAction<T> extends LayoutTreeAction {
|
||||
type: LayoutTreeActionType.Swap;
|
||||
|
||||
node1: LayoutNode<T>;
|
||||
node2: LayoutNode<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action for committing a pending action to the layout tree.
|
||||
*/
|
||||
|
@ -43,14 +43,13 @@ export function determineDropDirection(dimensions?: Dimensions, offset?: XYCoord
|
||||
// Lies outside of the box
|
||||
if (y < 0 || y > height || x < 0 || x > width) return undefined;
|
||||
|
||||
// TODO: uncomment once center drop is supported
|
||||
// // Determines if a drop point falls within the center fifth of the box, meaning we should return Center.
|
||||
// const centerX1 = (2 * width) / 5;
|
||||
// const centerX2 = (3 * width) / 5;
|
||||
// const centerY1 = (2 * height) / 5;
|
||||
// const centerY2 = (3 * width) / 5;
|
||||
// Determines if a drop point falls within the center fifth of the box, meaning we should return Center.
|
||||
const centerX1 = (2 * width) / 5;
|
||||
const centerX2 = (3 * width) / 5;
|
||||
const centerY1 = (2 * height) / 5;
|
||||
const centerY2 = (3 * height) / 5;
|
||||
|
||||
// if (x > centerX1 && x < centerX2 && y > centerY1 && y < centerY2) return DropDirection.Center;
|
||||
if (x > centerX1 && x < centerX2 && y > centerY1 && y < centerY2) return DropDirection.Center;
|
||||
|
||||
const diagonal1 = y * width - x * height;
|
||||
const diagonal2 = y * width + x * height - height * width;
|
||||
|
@ -82,14 +82,21 @@ test("determineDropDirection", () => {
|
||||
DropDirection.OuterLeft
|
||||
);
|
||||
|
||||
// TODO: uncomment once center direction is supported
|
||||
// assert.equal(
|
||||
// determineDropDirection(dimensions, {
|
||||
// x: 2.5,
|
||||
// y: 2.5,
|
||||
// }),
|
||||
// DropDirection.Center
|
||||
// );
|
||||
assert.equal(
|
||||
determineDropDirection(dimensions, {
|
||||
x: 2.5,
|
||||
y: 2.5,
|
||||
}),
|
||||
DropDirection.Center
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
determineDropDirection(dimensions, {
|
||||
x: 2.51,
|
||||
y: 2.51,
|
||||
}),
|
||||
DropDirection.Center
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
determineDropDirection(dimensions, {
|
||||
|
Loading…
Reference in New Issue
Block a user