Skip to content

Commit

Permalink
Editor - Resizing elements inside page rows (#645)
Browse files Browse the repository at this point in the history
* Fixes and remove unneeded stylings in recent visual editor update

* Enable dragging row child edges (WIP) (broken)

* Small refactor

* Use context instead of state

* Attempt to resize columns in rows

* Add logic for right edge dragging

* Enable resizing in DOM

* Normalize column sizes (will extract this logic later)

* Remove unneeded stop propagation

* Drag & drop improvements

* Drag preview + fix resizing result

* Fix resizing preview sizes + show 12-column grid while dragging

* Disallow resizing from left of first and right of last element in a row

* Add minimum resize width and snapping to grid

* Fix resizing positions + show resize cursor while resizing

* Fix some function names and types

* Fix element selection after resize

* Fix deleting elements by clicking

* Fix resizing on add + delete + edge cases with automatic column behavior

* Fix deleting with Backspace

* Set row column sizes prop in runtime - remove prop control

* Change OverlayGrid file location

* Simplicy some logic + separate resizing logic in its own hook

* Add box around Image component

* Increase and move horizontal resize drag areas to make resizing more intuitive

* Extract some components to their own file

* Move pageViw utils to geometry utils

Co-authored-by: Jan Potoms <[email protected]>
  • Loading branch information
apedroferreira and Janpot authored Jul 27, 2022
1 parent 464ffc9 commit 2d06db6
Show file tree
Hide file tree
Showing 14 changed files with 1,085 additions and 426 deletions.
75 changes: 47 additions & 28 deletions packages/toolpad-app/src/appDom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ export interface ElementNode<P = any> extends AppDomNodeBase {
readonly component: ConstantAttrValue<string>;
};
readonly props?: BindableAttrValues<P>;
readonly layout?: {
readonly columnSize?: ConstantAttrValue<number>;
};
}

export interface CodeComponentNode extends AppDomNodeBase {
Expand Down Expand Up @@ -430,6 +433,7 @@ export function createElement<P>(
dom: AppDom,
component: string,
props: Partial<BindableAttrValues<P>> = {},
layout: Partial<BindableAttrValues<P>> = {},
name?: string,
): ElementNode {
return createNode(dom, 'element', {
Expand All @@ -438,6 +442,7 @@ export function createElement<P>(
attributes: {
component: createConst(component),
},
layout,
});
}

Expand Down Expand Up @@ -592,11 +597,11 @@ function setNodeParent<N extends AppDomNode>(
parentProp: string,
parentIndex?: string,
) {
const parent = getNode(dom, parentId);

if (!parentIndex) {
const siblings: readonly AppDomNode[] = (getChildNodes(dom, parent) as any)[parentProp] ?? [];
const lastIndex = siblings.length > 0 ? siblings[siblings.length - 1].parentIndex : null;
const parent = getNode(dom, parentId);

const children: readonly AppDomNode[] = (getChildNodes(dom, parent) as any)[parentProp] ?? [];
const lastIndex = children.length > 0 ? children[children.length - 1].parentIndex : null;
parentIndex = createFractionalIndex(lastIndex, null);
}

Expand Down Expand Up @@ -698,6 +703,42 @@ export function getNodeIdByName(dom: AppDom, name: string): NodeId | null {
return index.get(name) ?? null;
}

export function getSiblingBeforeNode(
dom: AppDom,
node: ElementNode | PageNode,
parentProp: string,
) {
const parent = getParent(dom, node);

invariant(parent, `Node: "${node.id}" has no parent`);

const parentChildren =
((isPage(parent) || isElement(parent)) &&
(getChildNodes(dom, parent) as NodeChildren<ElementNode>)[parentProp]) ||
[];

const nodeIndex = parentChildren.findIndex((child) => child.id === node.id);
const nodeBefore = nodeIndex > 0 ? parentChildren[nodeIndex - 1] : null;

return nodeBefore;
}

export function getSiblingAfterNode(dom: AppDom, node: ElementNode | PageNode, parentProp: string) {
const parent = getParent(dom, node);

invariant(parent, `Node: "${node.id}" has no parent`);

const parentChildren =
((isPage(parent) || isElement(parent)) &&
(getChildNodes(dom, parent) as NodeChildren<ElementNode>)[parentProp]) ||
[];

const nodeIndex = parentChildren.findIndex((child) => child.id === node.id);
const nodeAfter = nodeIndex < parentChildren.length - 1 ? parentChildren[nodeIndex + 1] : null;

return nodeAfter;
}

export function getNewFirstParentIndexInNode(
dom: AppDom,
node: ElementNode | PageNode,
Expand Down Expand Up @@ -725,18 +766,7 @@ export function getNewParentIndexBeforeNode(
node: ElementNode | PageNode,
parentProp: string,
) {
const parent = getParent(dom, node);

invariant(parent, `Node: "${node.id}" has no parent`);

const parentChildren =
((isPage(parent) || isElement(parent)) &&
(getChildNodes(dom, parent) as NodeChildren<ElementNode>)[parentProp]) ||
[];

const nodeIndex = parentChildren.findIndex((child) => child.id === node.id);
const nodeBefore = nodeIndex > 0 ? parentChildren[nodeIndex - 1] : null;

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

Expand All @@ -745,18 +775,7 @@ export function getNewParentIndexAfterNode(
node: ElementNode | PageNode,
parentProp: string,
) {
const parent = getParent(dom, node);

invariant(parent, `Node: "${node.id}" has no parent`);

const parentChildren =
((isPage(parent) || isElement(parent)) &&
(getChildNodes(dom, parent) as NodeChildren<ElementNode>)[parentProp]) ||
[];

const nodeIndex = parentChildren.findIndex((child) => child.id === node.id);
const nodeAfter = nodeIndex < parentChildren.length - 1 ? parentChildren[nodeIndex + 1] : null;

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

Expand Down
14 changes: 12 additions & 2 deletions packages/toolpad-app/src/runtime/ToolpadApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ import {
import * as appDom from '../appDom';
import { VersionOrPreview } from '../types';
import { createProvidedContext } from '../utils/react';
import { getElementNodeComponentId, isPageRow, PAGE_ROW_COMPONENT_ID } from '../toolpadComponents';
import AppOverview from './AppOverview';
import { getElementNodeComponentId, PAGE_ROW_COMPONENT_ID } from '../toolpadComponents';
import AppThemeProvider from './AppThemeProvider';
import evalJsBindings, {
BindingEvaluationResult,
Expand Down Expand Up @@ -252,14 +252,24 @@ function RenderedNodeContent({ node, childNodes, Component }: RenderedNodeConten
: // `undefined` to ensure the defaultProps get picked up
undefined;

const layoutProps = React.useMemo(() => {
if (appDom.isElement(node) && isPageRow(node)) {
return {
layoutColumnSizes: childNodes.map((childNode) => childNode.layout?.columnSize?.value),
};
}
return {};
}, [childNodes, node]);

const props: Record<string, any> = React.useMemo(() => {
return {
...boundProps,
...onChangeHandlers,
...eventHandlers,
...layoutProps,
children: reactChildren,
};
}, [boundProps, eventHandlers, onChangeHandlers, reactChildren]);
}, [boundProps, eventHandlers, layoutProps, onChangeHandlers, reactChildren]);

// Wrap with slots
for (const [propName, argType] of Object.entries(argTypes)) {
Expand Down
198 changes: 198 additions & 0 deletions packages/toolpad-app/src/toolpad/AppEditor/PageEditor/NodeDropArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import * as React from 'react';
import clsx from 'clsx';
import { styled } from '@mui/material';
import { NodeInfo } from '../../../types';
import * as appDom from '../../../appDom';
import {
absolutePositionCss,
isHorizontalFlow,
isVerticalFlow,
Rectangle,
} from '../../../utils/geometry';
import {
DropZone,
DROP_ZONE_BOTTOM,
DROP_ZONE_CENTER,
DROP_ZONE_LEFT,
DROP_ZONE_RIGHT,
DROP_ZONE_TOP,
} from './PageEditorProvider';

const dropAreaHighlightClasses = {
highlightedTop: 'DropArea_HighlightedTop',
highlightedRight: 'DropArea_HighlightedRight',
highlightedBottom: 'DropArea_HighlightedBottom',
highlightedLeft: 'DropArea_HighlightedLeft',
highlightedCenter: 'DropArea_HighlightedCenter',
};

function getHighlightedZoneOverlayClass(
highlightedZone: DropZone,
): typeof dropAreaHighlightClasses[keyof typeof dropAreaHighlightClasses] | null {
switch (highlightedZone) {
case DROP_ZONE_TOP:
return dropAreaHighlightClasses.highlightedTop;
case DROP_ZONE_RIGHT:
return dropAreaHighlightClasses.highlightedRight;
case DROP_ZONE_BOTTOM:
return dropAreaHighlightClasses.highlightedBottom;
case DROP_ZONE_LEFT:
return dropAreaHighlightClasses.highlightedLeft;
case DROP_ZONE_CENTER:
return dropAreaHighlightClasses.highlightedCenter;
default:
return null;
}
}

const StyledNodeDropArea = styled('div', {
shouldForwardProp: (prop) => prop !== 'highlightRelativeRect',
})<{
highlightRelativeRect?: Partial<Rectangle>;
}>(({ highlightRelativeRect = {} }) => {
const {
x: highlightRelativeX = 0,
y: highlightRelativeY = 0,
width: highlightWidth = '100%',
height: highlightHeight = '100%',
} = highlightRelativeRect;

return {
pointerEvents: 'none',
position: 'absolute',
[`&.${dropAreaHighlightClasses.highlightedTop}`]: {
'&:after': {
backgroundColor: '#44EB2D',
content: "''",
position: 'absolute',
height: 4,
width: highlightWidth,
top: -2,
left: highlightRelativeX,
},
},
[`&.${dropAreaHighlightClasses.highlightedRight}`]: {
'&:after': {
backgroundColor: '#44EB2D',
content: "''",
position: 'absolute',
height: highlightHeight,
width: 4,
top: highlightRelativeY,
right: -2,
},
},
[`&.${dropAreaHighlightClasses.highlightedBottom}`]: {
'&:after': {
backgroundColor: '#44EB2D',
content: "''",
position: 'absolute',
height: 4,
width: highlightWidth,
bottom: -2,
left: highlightRelativeX,
},
},
[`&.${dropAreaHighlightClasses.highlightedLeft}`]: {
'&:after': {
backgroundColor: '#44EB2D',
content: "''",
position: 'absolute',
height: highlightHeight,
width: 4,
left: -2,
top: highlightRelativeY,
},
},
[`&.${dropAreaHighlightClasses.highlightedCenter}`]: {
border: '4px solid #44EB2D',
},
};
});

const EmptySlot = styled('div')({
alignItems: 'center',
border: '1px dashed green',
color: 'green',
display: 'flex',
fontSize: 20,
justifyContent: 'center',
position: 'absolute',
opacity: 0.75,
});

interface NodeDropAreaProps {
node: appDom.ElementNode | appDom.PageNode;
parentInfo: NodeInfo | null;
layoutRect: Rectangle;
dropAreaRect: Rectangle;
slotRect?: Rectangle;
highlightedZone?: DropZone | null;
isEmptySlot: boolean;
isPageChild: boolean;
}

export default function NodeDropArea({
node,
highlightedZone,
parentInfo,
layoutRect,
slotRect,
dropAreaRect,
isEmptySlot,
isPageChild,
}: NodeDropAreaProps) {
const highlightedZoneOverlayClass =
highlightedZone && getHighlightedZoneOverlayClass(highlightedZone);

const nodeParentProp = node.parentProp;

const parentSlots = parentInfo?.slots;
const parentSlot = parentSlots && nodeParentProp && parentSlots[nodeParentProp];

const parentRect = parentInfo?.rect;

const isHorizontalContainerChild = parentSlot
? isHorizontalFlow(parentSlot.flowDirection)
: false;
const isVerticalContainerChild = parentSlot ? isVerticalFlow(parentSlot.flowDirection) : false;

const highlightHeight =
isHorizontalContainerChild && parentRect ? parentRect.height : layoutRect.height;
const highlightWidth =
!isPageChild && isVerticalContainerChild && parentRect ? parentRect.width : layoutRect.width;

const highlightRelativeX =
(!isPageChild && isVerticalContainerChild && parentRect ? parentRect.x : layoutRect.x) -
dropAreaRect.x;
const highlightRelativeY =
(isHorizontalContainerChild && parentRect ? parentRect.y : layoutRect.y) - dropAreaRect.y;

const isHighlightingCenter = highlightedZone === DROP_ZONE_CENTER;

const highlightRect = isHighlightingCenter && isEmptySlot && slotRect ? slotRect : dropAreaRect;

return (
<React.Fragment>
<StyledNodeDropArea
style={absolutePositionCss(highlightRect)}
className={clsx(
highlightedZoneOverlayClass
? {
[highlightedZoneOverlayClass]: !isHighlightingCenter || isEmptySlot,
}
: {},
)}
highlightRelativeRect={{
x: highlightRelativeX,
y: highlightRelativeY,
width: highlightWidth,
height: highlightHeight,
}}
/>
{isEmptySlot && slotRect ? (
<EmptySlot style={absolutePositionCss(slotRect)}>+</EmptySlot>
) : null}
</React.Fragment>
);
}
Loading

0 comments on commit 2d06db6

Please sign in to comment.