Skip to content

Commit

Permalink
Improve visual editor UX (#466)
Browse files Browse the repository at this point in the history
* Remove current slot system

* Add drag preview to DOM elements

* Recreate dragOver event

* Use page editor context

* Add boundary highlighting

* Add center boundary

* Drag over behavior for center zones

* Fix formatting discrepancies

* WIP

* Fixes and improvements to drag over behavior and highlighting

* Show dropzone for empty containers

* Automatic row/column creation

* Finalize layout composition drop logic (without indices)

* Disable dropping on non-highlighted zones

* Fix bug when dropping on page

* Fix logic for moving components

* Place elements in the correct order

* Fix type errors

* Fix some bugs that appeared during type fixes

* Support drop on empty space + allow dropping inside rows/columns only

* Fix bug when moving element to deleted container + delete columns with one element remaining

* Handle moving rows and columns

* Refactor available drop zones

* More bug fixes

* Disable dragging to column center, enable sides only

* Consider container gap in node overlays

* More uniform highlights and reduce layout frames

* UX improvement for columns

* Make cursor size constant

* Improvements and fixes to drag & drop logic

* Fix adding elements vertically in horizontal container

* Infinitely composable rows and columns

* Row/column composition improvements

* Fix more inconsistencies with dragging layout elements

* Exclude gap from element selection overlays

* Fix empty slot size

* Small refactoring

* Fix custom components (WIP)

* Enable custom components and slot props

* Show correct container borders

* Improve selection areas

* Calculate gap automatically

* Fixes to for components with multiple slots

* Allow dragging rows to column center

* Fix reverse containers

* Fix untyped argument

* Allow placing components in page edges

* Make components interactive again

* Improve column behavior for now

* Fix wrong position when placing elements between column rows

* Add implicit containers (Jan's PR)

* Fix column cursor + allow aligment in implicit element containers

* Make column elements take full width

* Fix empty slots

* Re-implement CSS grid in page columns

* Determine flow direction in grid elements

* Add padding to Paper component

* Remove Stack from component options

* Replace enums for drop zones / rectangle edges

* Remove unnecessary stylings + only allow dropping in center of empty page

* Disable dragging to inside of single slot elements

* Fix dropping in empty slots in custom parent props
  • Loading branch information
apedroferreira authored Jul 6, 2022
1 parent c5436ab commit 7622368
Show file tree
Hide file tree
Showing 18 changed files with 1,501 additions and 618 deletions.
79 changes: 72 additions & 7 deletions packages/toolpad-app/src/appDom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ export function createDom(): AppDom {
}

/**
* Creates a new DOM node representing aReact Element
* Creates a new DOM node representing a React Element
*/
export function createElement<P>(
dom: AppDom,
Expand Down Expand Up @@ -627,15 +627,14 @@ export function addNode<Parent extends AppDomNode, Child extends AppDomNode>(
return setNodeParent(dom, newNode, parent.id, parentProp, parentIndex);
}

export function moveNode(
export function moveNode<Parent extends AppDomNode, Child extends AppDomNode>(
dom: AppDom,
nodeId: NodeId,
parentId: NodeId,
parentProp: string,
node: Child,
parent: Parent,
parentProp: ParentPropOf<Child, Parent>,
parentIndex?: string,
) {
const node = getNode(dom, nodeId);
return setNodeParent(dom, node, parentId, parentProp, parentIndex);
return setNodeParent(dom, node, parent.id, parentProp, parentIndex);
}

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

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

return createFractionalIndex(null, firstChild?.parentIndex || null);
}

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

return createFractionalIndex(lastChild?.parentIndex || null, null);
}

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

if (!parent) {
throw new Error(`Invariant: 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 createFractionalIndex(nodeBefore?.parentIndex || null, node.parentIndex);
}

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

if (!parent) {
throw new Error(`Invariant: 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 createFractionalIndex(node.parentIndex, nodeAfter?.parentIndex || null);
}

/**
* We need to make sure no secrets end up in the frontend html, so let's only send the
* nodes that we need to build frontend, and that we know don't contain secrets.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import * as appDom from '../../../appDom';
import { useDom } from '../../DomLoader';
import { usePageEditorApi } from './PageEditorProvider';
import { useToolpadComponents } from '../toolpadComponents';
import { PAGE_ROW_COMPONENT_ID } from '../../../toolpadComponents';
import {
PAGE_COLUMN_COMPONENT_ID,
PAGE_ROW_COMPONENT_ID,
STACK_COMPONENT_ID,
} from '../../../toolpadComponents';

const WIDTH_COLLAPSED = 50;

Expand Down Expand Up @@ -94,7 +98,12 @@ export default function ComponentCatalog({ className }: ComponentCatalogProps) {
<Box sx={{ width: 300, height: '100%', overflow: 'auto' }}>
<Box display="grid" gridTemplateColumns="1fr" gap={1} padding={1}>
{Object.entries(toolpadComponents)
.filter(([componentId]) => componentId !== PAGE_ROW_COMPONENT_ID)
.filter(
([componentId]) =>
![PAGE_ROW_COMPONENT_ID, PAGE_COLUMN_COMPONENT_ID, STACK_COMPONENT_ID].includes(
componentId,
),
)
.map(([componentId, componentType]) => {
if (!componentType) {
throw new Error(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
import { NodeId, LiveBindings } from '@mui/toolpad-core';
import * as React from 'react';
import * as appDom from '../../../appDom';
import { SlotLocation, PageViewState } from '../../../types';
import { update, updateOrCreate } from '../../../utils/immutability';
import { PageViewState } from '../../../types';
import { update } from '../../../utils/immutability';

export type ComponentPanelTab = 'component' | 'theme';

export const DROP_ZONE_TOP = 'top';
export const DROP_ZONE_BOTTOM = 'bottom';
export const DROP_ZONE_LEFT = 'left';
export const DROP_ZONE_RIGHT = 'right';
export const DROP_ZONE_CENTER = 'center';
export type DropZone =
| typeof DROP_ZONE_TOP
| typeof DROP_ZONE_BOTTOM
| typeof DROP_ZONE_LEFT
| typeof DROP_ZONE_RIGHT
| typeof DROP_ZONE_CENTER;

export interface PageEditorState {
readonly appId: string;
readonly type: 'page';
Expand All @@ -14,7 +26,9 @@ export interface PageEditorState {
readonly componentPanelTab: ComponentPanelTab;
readonly newNode: appDom.ElementNode | null;
readonly highlightLayout: boolean;
readonly highlightedSlot: SlotLocation | null;
readonly dragOverNodeId: NodeId | null;
readonly dragOverSlotParentProp: string | null;
readonly dragOverZone: DropZone | null;
readonly viewState: PageViewState;
readonly pageState: Record<string, unknown>;
readonly bindings: LiveBindings;
Expand Down Expand Up @@ -42,7 +56,11 @@ export type PageEditorAction =
}
| {
type: 'PAGE_NODE_DRAG_OVER';
slot: SlotLocation | null;
dragOverState: {
nodeId: NodeId | null;
parentProp: string | null;
zone: DropZone | null;
};
}
| {
type: 'PAGE_NODE_DRAG_END';
Expand All @@ -69,7 +87,9 @@ export function createPageEditorState(appId: string, nodeId: NodeId): PageEditor
componentPanelTab: 'component',
newNode: null,
highlightLayout: false,
highlightedSlot: null,
dragOverNodeId: null,
dragOverSlotParentProp: null,
dragOverZone: null,
viewState: { nodes: {} },
pageState: {},
bindings: {},
Expand Down Expand Up @@ -111,12 +131,16 @@ export function pageEditorReducer(
return update(state, {
newNode: null,
highlightLayout: false,
highlightedSlot: null,
dragOverNodeId: null,
});
case 'PAGE_NODE_DRAG_OVER': {
const { nodeId, parentProp, zone } = action.dragOverState;

return update(state, {
highlightLayout: true,
highlightedSlot: action.slot ? updateOrCreate(state.highlightedSlot, action.slot) : null,
dragOverNodeId: nodeId,
dragOverSlotParentProp: parentProp,
dragOverZone: zone,
});
}
case 'PAGE_VIEW_STATE_UPDATE': {
Expand Down Expand Up @@ -156,10 +180,18 @@ function createPageEditorApi(dispatch: React.Dispatch<PageEditorAction>) {
nodeDragEnd() {
dispatch({ type: 'PAGE_NODE_DRAG_END' });
},
nodeDragOver(slot: SlotLocation | null) {
nodeDragOver({
nodeId,
parentProp,
zone,
}: {
nodeId: NodeId | null;
parentProp: string | null;
zone: DropZone | null;
}) {
dispatch({
type: 'PAGE_NODE_DRAG_OVER',
slot,
dragOverState: { nodeId, parentProp, zone },
});
},
pageViewStateUpdate(viewState: PageViewState) {
Expand Down
Loading

0 comments on commit 7622368

Please sign in to comment.