Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drag and drop refactor #730

Merged
merged 7 commits into from
Aug 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions packages/toolpad-app/src/appDom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,16 @@ export function getNodeIdByName(dom: AppDom, name: string): NodeId | null {
return index.get(name) ?? null;
}

export function getNodeFirstChild(dom: AppDom, node: ElementNode | PageNode, parentProp: string) {
const nodeChildren = (getChildNodes(dom, node) as NodeChildren<ElementNode>)[parentProp] || [];
return nodeChildren.length > 0 ? nodeChildren[0] : null;
}

export function getNodeLastChild(dom: AppDom, node: ElementNode | PageNode, parentProp: string) {
const nodeChildren = (getChildNodes(dom, node) as NodeChildren<ElementNode>)[parentProp] || [];
return nodeChildren.length > 0 ? nodeChildren[nodeChildren.length - 1] : null;
}

export function getSiblingBeforeNode(
dom: AppDom,
node: ElementNode | PageNode,
Expand Down Expand Up @@ -746,9 +756,7 @@ export function getNewFirstParentIndexInNode(
node: ElementNode | PageNode,
parentProp: string,
) {
const children = (getChildNodes(dom, node) as NodeChildren<ElementNode>)[parentProp] || [];
const firstChild = children.length > 0 ? children[0] : null;

const firstChild = getNodeFirstChild(dom, node, parentProp);
return createFractionalIndex(null, firstChild?.parentIndex || null);
}

Expand All @@ -757,9 +765,7 @@ export function getNewLastParentIndexInNode(
node: ElementNode | PageNode,
parentProp: string,
) {
const children = (getChildNodes(dom, node) as NodeChildren<ElementNode>)[parentProp] || [];
const lastChild = children.length > 0 ? children[children.length - 1] : null;

const lastChild = getNodeLastChild(dom, node, parentProp);
return createFractionalIndex(lastChild?.parentIndex || null, null);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import * as React from 'react';
import { NodeId } from '@mui/toolpad-core';

import { SlotsState, FlowDirection } from '../../../../types';
import * as appDom from '../../../../appDom';
import { Rectangle } from '../../../../utils/geometry';
import { ExactEntriesOf } from '../../../../utils/types';

import { useDom } from '../../../DomLoader';

import NodeDropArea from './NodeDropArea';
import {
DROP_ZONE_CENTER,
DROP_ZONE_BOTTOM,
DROP_ZONE_LEFT,
DROP_ZONE_RIGHT,
DROP_ZONE_TOP,
DropZone,
usePageEditorState,
} from '../PageEditorProvider';
import { isPageRow } from '../../../../toolpadComponents';

function getChildNodeHighlightedZone(parentFlowDirection: FlowDirection): DropZone | null {
switch (parentFlowDirection) {
case 'row':
return DROP_ZONE_RIGHT;
case 'column':
return DROP_ZONE_BOTTOM;
case 'row-reverse':
return DROP_ZONE_LEFT;
case 'column-reverse':
return DROP_ZONE_TOP;
default:
return null;
}
}

interface DragAndDropNodeProps {
node: appDom.AppDomNode;
getDropAreaRect: (nodeId: NodeId, parentProp?: string) => Rectangle;
availableDropZones: DropZone[];
}

export default function DragAndDropNode({
node,
getDropAreaRect,
availableDropZones,
}: DragAndDropNodeProps) {
const dom = useDom();
const { dragOverNodeId, dragOverSlotParentProp, dragOverZone, viewState } = usePageEditorState();

const { nodes: nodesInfo } = viewState;

const nodeInfo = nodesInfo[node.id];
const nodeParentProp = node.parentProp;

const parent = appDom.getParent(dom, node) as appDom.PageNode | appDom.ElementNode;
const parentInfo = (parent && nodesInfo[parent.id]) || null;

const isPageNode = appDom.isPage(node);
const isPageChild = parent ? appDom.isPage(parent) : false;

const isPageRowChild = parent ? appDom.isElement(parent) && isPageRow(parent) : false;

const childNodes = appDom.getChildNodes(dom, node) as appDom.NodeChildren<appDom.ElementNode>;

const freeSlots = nodeInfo?.slots || {};
const freeSlotEntries = Object.entries(freeSlots) as ExactEntriesOf<SlotsState>;

const hasFreeSlots = freeSlotEntries.length > 0;
const hasMultipleFreeSlots = freeSlotEntries.length > 1;

const getNodeDropAreaHighlightedZone = React.useCallback(
(parentProp: string | null = null): DropZone | null => {
const parentParent = parent && appDom.getParent(dom, parent);

if (dragOverZone && !availableDropZones.includes(dragOverZone)) {
return null;
}

if (dragOverZone === DROP_ZONE_TOP) {
// Is dragging over page top
if (parent && parent.id === dragOverNodeId && appDom.isPage(parent)) {
const pageFirstChild = appDom.getNodeFirstChild(dom, parent, 'children');

const isPageFirstChild = pageFirstChild ? node.id === pageFirstChild.id : false;

return isPageFirstChild ? DROP_ZONE_TOP : null;
}
}

if (dragOverZone === DROP_ZONE_LEFT) {
// Is dragging over parent page row left, and parent page row is a child of the page
if (
parent &&
parentParent &&
parent.id === dragOverNodeId &&
appDom.isElement(parent) &&
isPageRowChild &&
appDom.isPage(parentParent)
) {
const parentFirstChild = nodeParentProp
? appDom.getNodeFirstChild(dom, parent, nodeParentProp)
: null;

const isParentFirstChild = parentFirstChild ? node.id === parentFirstChild.id : false;

return isParentFirstChild ? DROP_ZONE_LEFT : null;
}

// Is dragging over left, is page row and child of the page
if (parent && appDom.isElement(node) && isPageRow(node) && isPageChild) {
return null;
}
}

if (dragOverZone === DROP_ZONE_CENTER) {
// Is dragging over parent element center
if (parent && parent.id === dragOverNodeId) {
const parentLastChild =
nodeParentProp && (appDom.isPage(parent) || appDom.isElement(parent))
? appDom.getNodeLastChild(dom, parent, nodeParentProp)
: null;

const isParentLastChild = parentLastChild ? node.id === parentLastChild.id : false;

const parentSlots = parentInfo?.slots || null;

const parentFlowDirection =
parentSlots && nodeParentProp && parentSlots[nodeParentProp]?.flowDirection;

return parentFlowDirection && isParentLastChild && (!hasMultipleFreeSlots || !parentProp)
? getChildNodeHighlightedZone(parentFlowDirection)
: null;
}
// Is dragging over slot center
if (node.id === dragOverNodeId && parentProp && parentProp === dragOverSlotParentProp) {
if (isPageNode) {
return DROP_ZONE_CENTER;
}

const nodeChildren =
(parentProp && appDom.isElement(node) && childNodes[parentProp]) || [];
return nodeChildren.length === 0 ? DROP_ZONE_CENTER : null;
}
}

// Common cases
return node.id === dragOverNodeId && parentProp === dragOverSlotParentProp
? dragOverZone
: null;
},
[
availableDropZones,
childNodes,
dom,
dragOverNodeId,
dragOverSlotParentProp,
dragOverZone,
hasMultipleFreeSlots,
isPageChild,
isPageNode,
isPageRowChild,
node,
nodeParentProp,
parent,
parentInfo?.slots,
],
);

const nodeRect = nodeInfo?.rect || null;
const hasNodeOverlay = isPageNode || appDom.isElement(node);

if (!nodeRect || !hasNodeOverlay) {
return null;
}

const hasOwnDropArea = !hasFreeSlots || hasMultipleFreeSlots;
const hasSlotDropAreas = hasFreeSlots;

return (
<React.Fragment>
{hasSlotDropAreas
? freeSlotEntries.map(([parentProp, freeSlot]) => {
if (!freeSlot) {
return null;
}

const dropAreaRect = getDropAreaRect(node.id, parentProp);

const slotChildNodes = childNodes[parentProp] || [];
const isEmptySlot = slotChildNodes.length === 0;

if (isPageNode && !isEmptySlot) {
return null;
}

return (
<NodeDropArea
key={`${node.id}:${parentProp}`}
node={node}
parentInfo={parentInfo}
layoutRect={nodeRect}
dropAreaRect={dropAreaRect}
slotRect={freeSlot.rect}
highlightedZone={getNodeDropAreaHighlightedZone(parentProp)}
isEmptySlot={isEmptySlot}
isPageChild={isPageChild}
/>
);
})
: null}
{hasOwnDropArea ? (
<NodeDropArea
node={node}
parentInfo={parentInfo}
layoutRect={nodeRect}
dropAreaRect={getDropAreaRect(node.id)}
highlightedZone={getNodeDropAreaHighlightedZone()}
isEmptySlot={false}
isPageChild={isPageChild}
/>
) : null}
</React.Fragment>
);
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import * as React from 'react';
import clsx from 'clsx';
import { styled } from '@mui/material';
import { NodeInfo } from '../../../types';
import * as appDom from '../../../appDom';
import { NodeInfo } from '../../../../types';
import * as appDom from '../../../../appDom';
import {
absolutePositionCss,
isHorizontalFlow,
isVerticalFlow,
Rectangle,
} from '../../../utils/geometry';
} from '../../../../utils/geometry';
import {
DropZone,
DROP_ZONE_BOTTOM,
DROP_ZONE_CENTER,
DROP_ZONE_LEFT,
DROP_ZONE_RIGHT,
DROP_ZONE_TOP,
} from './PageEditorProvider';
} from '../PageEditorProvider';

const dropAreaHighlightClasses = {
highlightedTop: 'DropArea_HighlightedTop',
Expand Down Expand Up @@ -134,11 +134,11 @@ interface NodeDropAreaProps {

export default function NodeDropArea({
node,
highlightedZone,
parentInfo,
layoutRect,
slotRect,
dropAreaRect,
slotRect,
highlightedZone,
isEmptySlot,
isPageChild,
}: NodeDropAreaProps) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ import clsx from 'clsx';
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
import DeleteIcon from '@mui/icons-material/Delete';
import { IconButton, styled } from '@mui/material';
import * as appDom from '../../../appDom';
import * as appDom from '../../../../appDom';
import {
absolutePositionCss,
Rectangle,
RectangleEdge,
RECTANGLE_EDGE_BOTTOM,
RECTANGLE_EDGE_LEFT,
RECTANGLE_EDGE_RIGHT,
} from '../../../utils/geometry';
import { useDom } from '../../DomLoader';
import { useToolpadComponent } from '../toolpadComponents';
import { getElementNodeComponentId } from '../../../toolpadComponents';
} from '../../../../utils/geometry';
import { useDom } from '../../../DomLoader';
import { useToolpadComponent } from '../../toolpadComponents';
import { getElementNodeComponentId } from '../../../../toolpadComponents';

const nodeHudClasses = {
allowNodeInteraction: 'NodeHud_AllowNodeInteraction',
Expand Down Expand Up @@ -108,10 +108,10 @@ const ResizePreview = styled('div')({
});

interface NodeHudProps {
node: appDom.ElementNode | appDom.PageNode;
node: appDom.AppDomNode;
rect: Rectangle;
selected?: boolean;
allowInteraction?: boolean;
isSelected?: boolean;
isInteractive?: boolean;
onNodeDragStart?: React.DragEventHandler<HTMLElement>;
draggableEdges?: RectangleEdge[];
onEdgeDragStart?: (
Expand All @@ -125,9 +125,9 @@ interface NodeHudProps {

export default function NodeHud({
node,
selected,
allowInteraction,
rect,
isSelected,
isInteractive,
onNodeDragStart,
draggableEdges = [],
onEdgeDragStart,
Expand All @@ -140,32 +140,21 @@ export default function NodeHud({
const componentId = appDom.isElement(node) ? getElementNodeComponentId(node) : '';
const component = useToolpadComponent(dom, componentId);

const handleDelete = React.useCallback(
(event: React.MouseEvent<HTMLElement>) => {
event.stopPropagation();

if (onDelete) {
onDelete(event);
}
},
[onDelete],
);

return (
<NodeHudWrapper
data-node-id={node.id}
style={absolutePositionCss(rect)}
className={clsx({
[nodeHudClasses.allowNodeInteraction]: allowInteraction,
[nodeHudClasses.allowNodeInteraction]: isInteractive,
})}
>
{selected ? (
{isSelected ? (
<React.Fragment>
<span className={nodeHudClasses.selected} />
<div draggable className={nodeHudClasses.selectionHint} onDragStart={onNodeDragStart}>
{component?.displayName || '<unknown>'}
<DragIndicatorIcon color="inherit" />
<IconButton aria-label="Remove element" color="inherit" onMouseUp={handleDelete}>
<IconButton aria-label="Remove element" color="inherit" onMouseUp={onDelete}>
<DeleteIcon color="inherit" />
</IconButton>
</div>
Expand Down
Loading