Add placeholder for layout drag and drop (#26)

This commit is contained in:
Evan Simkowitz 2024-06-06 17:58:37 -07:00 committed by GitHub
parent 2b456f9725
commit 441463b172
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 109 additions and 20 deletions

View File

@ -2,11 +2,22 @@
// SPDX-License-Identifier: Apache-2.0
import clsx from "clsx";
import { CSSProperties, RefObject, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import {
CSSProperties,
ReactNode,
RefObject,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { useDrag, useDragLayer, useDrop } from "react-dnd";
import useResizeObserver from "@react-hook/resize-observer";
import { useLayoutTreeStateReducerAtom } from "./layoutAtom.js";
import { findNode } from "./layoutNode.js";
import {
ContentRenderer,
LayoutNode,
@ -14,11 +25,12 @@ import {
LayoutTreeActionType,
LayoutTreeComputeMoveNodeAction,
LayoutTreeDeleteNodeAction,
LayoutTreeMoveNodeAction,
LayoutTreeState,
WritableLayoutTreeStateAtom,
} from "./model.js";
import "./tilelayout.less";
import { setTransform as createTransform, debounce, determineDropDirection } from "./utils.js";
import { FlexDirection, setTransform as createTransform, debounce, determineDropDirection } from "./utils.js";
export interface TileLayoutProps<T> {
layoutTreeStateAtom: WritableLayoutTreeStateAtom<T>;
@ -178,6 +190,13 @@ export const TileLayout = <T,>({ layoutTreeStateAtom, className, renderContent,
);
})}
</div>
<Placeholder
key="placeholder"
layoutTreeState={layoutTreeState}
overlayContainerRef={overlayContainerRef}
nodeRefs={nodeRefs}
style={{ top: 10000, ...overlayTransform }}
/>
<div
key="overlay"
ref={overlayContainerRef}
@ -362,3 +381,87 @@ const OverlayNode = <T,>({ layoutNode, layoutTreeState, dispatch, setRef, delete
</div>
);
};
interface PlaceholderProps<T> {
layoutTreeState: LayoutTreeState<T>;
overlayContainerRef: React.RefObject<HTMLElement>;
nodeRefs: Map<string, React.RefObject<HTMLElement>>;
style: React.CSSProperties;
}
const Placeholder = <T,>({ layoutTreeState, overlayContainerRef, nodeRefs, style }: PlaceholderProps<T>) => {
const [placeholderOverlay, setPlaceholderOverlay] = useState<ReactNode>(null);
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;
}
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();
let placeholderTransform: CSSProperties;
const placeholderHeight =
parentNode.flexDirection === FlexDirection.Column
? targetBoundingRect.height / 2
: targetBoundingRect.height;
const placeholderWidth =
parentNode.flexDirection === FlexDirection.Row
? targetBoundingRect.width / 2
: targetBoundingRect.width;
if (action.index > targetIndex) {
placeholderTransform = createTransform({
top:
targetBoundingRect.top +
(parentNode.flexDirection === FlexDirection.Column && targetBoundingRect.height / 2) -
overlayBoundingRect.top,
left:
targetBoundingRect.left +
(parentNode.flexDirection === FlexDirection.Row && targetBoundingRect.width / 2) -
overlayBoundingRect.left,
width: placeholderWidth,
height: placeholderHeight,
});
} else {
placeholderTransform = createTransform({
top: targetBoundingRect.top - overlayBoundingRect.top,
left: targetBoundingRect.left - overlayBoundingRect.left,
width: placeholderWidth,
height: placeholderHeight,
});
}
newPlaceholderOverlay = <div className="placeholder" style={{ ...placeholderTransform }} />;
}
}
}
setPlaceholderOverlay(newPlaceholderOverlay);
}, [layoutTreeState, nodeRefs, overlayContainerRef]);
return (
<div className="placeholder-container" style={style}>
{placeholderOverlay}
</div>
);
};

View File

@ -24,8 +24,6 @@
.overlay-container {
z-index: 2;
background-color: coral;
opacity: 0.5;
}
.tile-node,
@ -47,7 +45,8 @@
}
&.animate {
.tile-node {
.tile-node,
.placeholder {
transition-duration: 0.15s;
transition-timing-function: ease-in;
transition-property: transform, width, height;
@ -66,28 +65,15 @@
flex: 1 1 auto;
max-height: 100%;
max-width: 100%;
/* margin: 5px; */
}
.tile-leaf {
border: 1px solid black;
}
.overlay-leaf {
border: 10px solid red;
}
// .tile-leaf {
// border: 1px solid black;
// }
// .overlay-leaf {
// margin: 1px;
// }
.placeholder {
display: flex;
flex: 1 1 auto;
background-color: aqua;
opacity: 0.5;
border-radius: 5px;
}
}