Fix editor stacking context

This commit is contained in:
Abhinav Jha 2019-10-08 11:49:10 +05:30
parent 81272a0839
commit 74ee90d816
12 changed files with 117 additions and 37 deletions

View File

@ -37,7 +37,7 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>Appsmith | Editor</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -23,6 +23,15 @@ export type Theme = {
borders: { thickness: string; style: "dashed" | "solid"; color: Color }[]; borders: { thickness: string; style: "dashed" | "solid"; color: Color }[];
}; };
export const getColorWithOpacity = (color: Color, opacity: number) => {
color = color.slice(1);
const val = parseInt(color, 16);
const r = (val >> 16) & 255;
const g = (val >> 8) & 255;
const b = val & 255;
return `rgba(${r},${g},${b},${opacity})`;
};
export const theme: Theme = { export const theme: Theme = {
radii: [0, 4, 8, 10, 20, 50], radii: [0, 4, 8, 10, 20, 50],
fontSizes: [0, 10, 12, 14, 16, 18, 24, 28, 32, 48, 64], fontSizes: [0, 10, 12, 14, 16, 18, 24, 28, 32, 48, 64],

View File

@ -9,7 +9,6 @@ import DropTargetMask from "./DropTargetMask";
const WrappedDragLayer = styled.div` const WrappedDragLayer = styled.div`
position: absolute; position: absolute;
pointer-events: none; pointer-events: none;
z-index: 10;
left: 0; left: 0;
top: 0; top: 0;
width: 100%; width: 100%;
@ -26,6 +25,8 @@ type DragLayerProps = {
occupiedSpaces: OccupiedSpace[] | null; occupiedSpaces: OccupiedSpace[] | null;
onBoundsUpdate: Function; onBoundsUpdate: Function;
isOver: boolean; isOver: boolean;
parentRows?: number;
parentCols?: number;
}; };
const DragLayerComponent = (props: DragLayerProps) => { const DragLayerComponent = (props: DragLayerProps) => {
@ -41,6 +42,8 @@ const DragLayerComponent = (props: DragLayerProps) => {
monitor.getItem(), monitor.getItem(),
props.dropTargetOffset, props.dropTargetOffset,
props.occupiedSpaces, props.occupiedSpaces,
props.parentRows,
props.parentCols,
), ),
}), }),
); );

View File

@ -18,6 +18,8 @@ const DraggableWrapper = styled.div<{ show: boolean }>`
display: ${props => (props.show ? "block" : "none")}; display: ${props => (props.show ? "block" : "none")};
} }
display: block; display: block;
position: relative;
z-index: 1;
`; `;
const DragHandle = styled.div` const DragHandle = styled.div`
@ -27,7 +29,6 @@ const DragHandle = styled.div`
cursor: move; cursor: move;
display: none; display: none;
cursor: grab; cursor: grab;
z-index: 11;
`; `;
const DeleteControl = styled.div` const DeleteControl = styled.div`
@ -36,7 +37,6 @@ const DeleteControl = styled.div`
top: -${props => props.theme.fontSizes[CONTROL_THEME_FONTSIZE_INDEX] / 2}px; top: -${props => props.theme.fontSizes[CONTROL_THEME_FONTSIZE_INDEX] / 2}px;
display: none; display: none;
cursor: pointer; cursor: pointer;
z-index: 11;
`; `;
const moveControlIcon = ControlIcons.MOVE_CONTROL({ const moveControlIcon = ControlIcons.MOVE_CONTROL({
@ -100,13 +100,13 @@ const DraggableComponent = (props: DraggableComponentProps) => {
props.style.componentHeight + (props.style.heightUnit || "px"), props.style.componentHeight + (props.style.heightUnit || "px"),
}} }}
> >
{props.children}
<DragHandle className="control" ref={drag}> <DragHandle className="control" ref={drag}>
{moveControlIcon} {moveControlIcon}
</DragHandle> </DragHandle>
<DeleteControl className="control" onClick={deleteWidget}> <DeleteControl className="control" onClick={deleteWidget}>
{deleteControlIcon} {deleteControlIcon}
</DeleteControl> </DeleteControl>
{props.children}
</DraggableWrapper> </DraggableWrapper>
</ResizingContext.Provider> </ResizingContext.Provider>
); );

View File

@ -1,6 +1,6 @@
import React, { useState, useContext } from "react"; import React, { useState, useContext } from "react";
import { WidgetProps } from "../widgets/BaseWidget"; import { WidgetProps } from "../widgets/BaseWidget";
import { OccupiedSpace } from "../widgets/ContainerWidget"; import { OccupiedSpaceContext } from "../widgets/ContainerWidget";
import { WidgetConfigProps } from "../reducers/entityReducers/widgetConfigReducer"; import { WidgetConfigProps } from "../reducers/entityReducers/widgetConfigReducer";
import { useDrop, XYCoord } from "react-dnd"; import { useDrop, XYCoord } from "react-dnd";
import { ContainerProps } from "./ContainerComponent"; import { ContainerProps } from "./ContainerComponent";
@ -15,7 +15,6 @@ type DropTargetComponentProps = ContainerProps & {
snapRows?: number; snapRows?: number;
snapColumnSpace: number; snapColumnSpace: number;
snapRowSpace: number; snapRowSpace: number;
occupiedSpaces: OccupiedSpace[] | null;
}; };
type DropTargetBounds = { type DropTargetBounds = {
@ -29,6 +28,7 @@ export const DropTargetComponent = (props: DropTargetComponentProps) => {
// Hook to keep the offset of the drop target container in state // Hook to keep the offset of the drop target container in state
const [dropTargetOffset, setDropTargetOffset] = useState({ x: 0, y: 0 }); const [dropTargetOffset, setDropTargetOffset] = useState({ x: 0, y: 0 });
const { updateWidget } = useContext(WidgetFunctionsContext); const { updateWidget } = useContext(WidgetFunctionsContext);
const occupiedSpaces = useContext(OccupiedSpaceContext);
// Make this component a drop target // Make this component a drop target
const [{ isOver, isExactlyOver }, drop] = useDrop({ const [{ isOver, isExactlyOver }, drop] = useDrop({
accept: Object.values(WidgetFactory.getWidgetTypes()), accept: Object.values(WidgetFactory.getWidgetTypes()),
@ -69,7 +69,9 @@ export const DropTargetComponent = (props: DropTargetComponentProps) => {
props.snapRowSpace, props.snapRowSpace,
widget, widget,
dropTargetOffset, dropTargetOffset,
props.occupiedSpaces, occupiedSpaces,
props.snapRows,
props.snapColumns,
); );
} }
return false; return false;
@ -88,7 +90,6 @@ export const DropTargetComponent = (props: DropTargetComponentProps) => {
return ( return (
<div <div
ref={drop} ref={drop}
className="dropTarget"
style={{ style={{
position: "relative", position: "relative",
left: 0, left: 0,
@ -99,7 +100,6 @@ export const DropTargetComponent = (props: DropTargetComponentProps) => {
? props.style.componentWidth + (props.style.widthUnit || "px") ? props.style.componentWidth + (props.style.widthUnit || "px")
: "100%", : "100%",
top: 0, top: 0,
background: "white",
}} }}
> >
<DragLayerComponent <DragLayerComponent
@ -109,8 +109,10 @@ export const DropTargetComponent = (props: DropTargetComponentProps) => {
visible={isOver} visible={isOver}
isOver={isExactlyOver} isOver={isExactlyOver}
dropTargetOffset={dropTargetOffset} dropTargetOffset={dropTargetOffset}
occupiedSpaces={props.occupiedSpaces} occupiedSpaces={occupiedSpaces}
onBoundsUpdate={handleBoundsUpdate} onBoundsUpdate={handleBoundsUpdate}
parentRows={props.snapRows}
parentCols={props.snapColumns}
/> />
{props.children} {props.children}

View File

@ -16,7 +16,6 @@ export const DropTargetMaskWrapper = styled.div<DropTargetMaskProps>`
right: 0; right: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: white;
${props => ${props =>
props.showGrid && props.showGrid &&
css` css`

View File

@ -6,10 +6,9 @@ import { theme } from "../constants/DefaultTheme";
const DropZoneWrapper = styled.div` const DropZoneWrapper = styled.div`
position: absolute; position: absolute;
z-index: 10;
background: ${props => props.theme.colors.hover}; background: ${props => props.theme.colors.hover};
border: 1px dashed ${props => props.theme.colors.textAnchor}; border: 1px dashed ${props => props.theme.colors.textAnchor};
opacity: 0.7; opacity: 0.6;
`; `;
type DropZoneProps = { type DropZoneProps = {

View File

@ -1,11 +1,15 @@
import React, { useContext, CSSProperties } from "react"; import React, { useContext, CSSProperties, useState } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { Rnd } from "react-rnd"; import { Rnd } from "react-rnd";
import { XYCoord } from "react-dnd"; import { XYCoord } from "react-dnd";
import { WidgetProps, WidgetOperations } from "../widgets/BaseWidget"; import { WidgetProps, WidgetOperations } from "../widgets/BaseWidget";
import { OccupiedSpaceContext } from "../widgets/ContainerWidget";
import { ContainerProps, ParentBoundsContext } from "./ContainerComponent"; import { ContainerProps, ParentBoundsContext } from "./ContainerComponent";
import { isDropZoneOccupied } from "../utils/WidgetPropsUtils";
import { ResizingContext } from "./DraggableComponent"; import { ResizingContext } from "./DraggableComponent";
import { FocusContext } from "../pages/Editor/Canvas";
import { WidgetFunctionsContext } from "../pages/Editor"; import { WidgetFunctionsContext } from "../pages/Editor";
import { theme, getColorWithOpacity } from "../constants/DefaultTheme";
export type ResizableComponentProps = WidgetProps & ContainerProps; export type ResizableComponentProps = WidgetProps & ContainerProps;
@ -18,28 +22,23 @@ const handleStyles: {
top: { top: {
height: "30px", height: "30px",
top: "-15px", top: "-15px",
zIndex: 11,
}, },
bottom: { bottom: {
height: "30px", height: "30px",
bottom: "-15px", bottom: "-15px",
zIndex: 11,
}, },
left: { left: {
width: "30px", width: "30px",
left: "-15px", left: "-15px",
zIndex: 11,
}, },
right: { right: {
width: "30px", width: "30px",
right: "-15px", right: "-15px",
zIndex: 11,
}, },
}; };
const ResizableContainer = styled(Rnd)` const ResizableContainer = styled(Rnd)`
position: relative; position: relative;
z-index: 10;
border: ${props => { border: ${props => {
return Object.values(props.theme.borders[0]).join(" "); return Object.values(props.theme.borders[0]).join(" ");
}}; }};
@ -50,7 +49,6 @@ const ResizableContainer = styled(Rnd)`
width: ${props => props.theme.spaces[2]}px; width: ${props => props.theme.spaces[2]}px;
height: ${props => props.theme.spaces[2]}px; height: ${props => props.theme.spaces[2]}px;
border-radius: ${props => props.theme.radii[5]}%; border-radius: ${props => props.theme.radii[5]}%;
z-index: 9;
background: ${props => props.theme.colors.containerBorder}; background: ${props => props.theme.colors.containerBorder};
} }
&:after { &:after {
@ -68,10 +66,49 @@ export const ResizableComponent = (props: ResizableComponentProps) => {
const { setIsResizing } = useContext(ResizingContext); const { setIsResizing } = useContext(ResizingContext);
const { boundingParent } = useContext(ParentBoundsContext); const { boundingParent } = useContext(ParentBoundsContext);
const { updateWidget } = useContext(WidgetFunctionsContext); const { updateWidget } = useContext(WidgetFunctionsContext);
const { setFocus } = useContext(FocusContext);
const occupiedSpaces = useContext(OccupiedSpaceContext);
const [isColliding, setIsColliding] = useState(false);
let bounds = "body"; let bounds = "body";
if (boundingParent && boundingParent.current) { if (boundingParent && boundingParent.current) {
bounds = "." + boundingParent.current.className.split(" ")[1]; bounds = "." + boundingParent.current.className.split(" ")[1];
} }
const checkForCollision = (
e: Event,
dir: any,
ref: any,
delta: { width: number; height: number },
position: XYCoord,
) => {
const left = props.leftColumn + position.x / props.parentColumnSpace;
const top = props.topRow + position.y / props.parentRowSpace;
const right =
props.rightColumn + (delta.width + position.x) / props.parentColumnSpace;
const bottom =
props.bottomRow + (delta.height + position.y) / props.parentRowSpace;
if (
isDropZoneOccupied(
{
left,
top,
bottom,
right,
},
props.widgetId,
occupiedSpaces,
)
) {
setIsColliding(true);
} else {
if (!!isColliding) {
setIsColliding(false);
}
}
};
const updateSize = ( const updateSize = (
e: Event, e: Event,
dir: any, dir: any,
@ -80,6 +117,7 @@ export const ResizableComponent = (props: ResizableComponentProps) => {
position: XYCoord, position: XYCoord,
) => { ) => {
setIsResizing && setIsResizing(false); setIsResizing && setIsResizing(false);
setFocus && setFocus(props.widgetId);
const leftColumn = props.leftColumn + position.x / props.parentColumnSpace; const leftColumn = props.leftColumn + position.x / props.parentColumnSpace;
const topRow = props.topRow + position.y / props.parentRowSpace; const topRow = props.topRow + position.y / props.parentRowSpace;
@ -88,6 +126,7 @@ export const ResizableComponent = (props: ResizableComponentProps) => {
const bottomRow = const bottomRow =
props.bottomRow + (delta.height + position.y) / props.parentRowSpace; props.bottomRow + (delta.height + position.y) / props.parentRowSpace;
if (!isColliding) {
updateWidget && updateWidget &&
updateWidget(WidgetOperations.RESIZE, props.widgetId, { updateWidget(WidgetOperations.RESIZE, props.widgetId, {
leftColumn, leftColumn,
@ -95,6 +134,8 @@ export const ResizableComponent = (props: ResizableComponentProps) => {
topRow, topRow,
bottomRow, bottomRow,
}); });
}
setIsColliding(false);
}; };
return ( return (
<ResizableContainer <ResizableContainer
@ -109,8 +150,14 @@ export const ResizableComponent = (props: ResizableComponentProps) => {
disableDragging disableDragging
minWidth={props.parentColumnSpace} minWidth={props.parentColumnSpace}
minHeight={props.parentRowSpace} minHeight={props.parentRowSpace}
style={{ ...props.style }} style={{
...props.style,
background: isColliding
? getColorWithOpacity(theme.colors.error, 0.6)
: props.style.backgroundColor,
}}
onResizeStop={updateSize} onResizeStop={updateSize}
onResize={checkForCollision}
onResizeStart={() => { onResizeStart={() => {
setIsResizing && setIsResizing(true); setIsResizing && setIsResizing(true);
}} }}

View File

@ -16,6 +16,7 @@ const ArtBoard = styled.div`
height: 100%; height: 100%;
position: relative; position: relative;
overflow: auto; overflow: auto;
background: white;
`; `;
interface CanvasProps { interface CanvasProps {

View File

@ -33,7 +33,6 @@ const CanvasContainer = styled.section`
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
z-index: 11;
pointer-events: none; pointer-events: none;
} }
`; `;

View File

@ -66,14 +66,13 @@ const areIntersecting = (r1: Rect, r2: Rect) => {
export const isDropZoneOccupied = ( export const isDropZoneOccupied = (
offset: Rect, offset: Rect,
widget: WidgetProps, widgetId: string,
occupied: OccupiedSpace[] | null, occupied: OccupiedSpace[] | null,
) => { ) => {
if (occupied) { if (occupied) {
occupied = occupied.filter(widgetDetails => { occupied = occupied.filter(widgetDetails => {
return ( return (
widgetDetails.id !== widget.widgetId && widgetDetails.id !== widgetId && widgetDetails.parentId !== widgetId
widgetDetails.parentId !== widget.widgetId
); );
}); });
for (let i = 0; i < occupied.length; i++) { for (let i = 0; i < occupied.length; i++) {
@ -86,6 +85,16 @@ export const isDropZoneOccupied = (
return false; return false;
}; };
export const isWidgetOverflowingParentBounds = (
parentRowCols: { rows?: number; cols?: number },
offset: Rect,
) => {
return (
(parentRowCols.cols || GridDefaults.DEFAULT_GRID_COLUMNS) < offset.right ||
(parentRowCols.rows || GridDefaults.DEFAULT_GRID_ROWS) < offset.bottom
);
};
export const noCollision = ( export const noCollision = (
clientOffset: XYCoord, clientOffset: XYCoord,
colWidth: number, colWidth: number,
@ -93,6 +102,8 @@ export const noCollision = (
widget: WidgetProps & Partial<WidgetConfigProps>, widget: WidgetProps & Partial<WidgetConfigProps>,
dropTargetOffset: XYCoord, dropTargetOffset: XYCoord,
occupiedSpaces: OccupiedSpace[] | null, occupiedSpaces: OccupiedSpace[] | null,
rows?: number,
cols?: number,
): boolean => { ): boolean => {
if (clientOffset && dropTargetOffset && widget) { if (clientOffset && dropTargetOffset && widget) {
const [left, top] = getDropZoneOffsets( const [left, top] = getDropZoneOffsets(
@ -113,7 +124,10 @@ export const noCollision = (
top, top,
bottom: top + widgetHeight, bottom: top + widgetHeight,
}; };
return !isDropZoneOccupied(currentOffset, widget, occupiedSpaces); return (
!isDropZoneOccupied(currentOffset, widget.widgetId, occupiedSpaces) &&
!isWidgetOverflowingParentBounds({ rows, cols }, currentOffset)
);
} }
return false; return false;
}; };

View File

@ -1,4 +1,4 @@
import React from "react"; import React, { createContext, Context } from "react";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget"; import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import ContainerComponent from "../editorComponents/ContainerComponent"; import ContainerComponent from "../editorComponents/ContainerComponent";
import { ContainerOrientation, WidgetType } from "../constants/WidgetConstants"; import { ContainerOrientation, WidgetType } from "../constants/WidgetConstants";
@ -11,6 +11,9 @@ import DraggableComponent from "../editorComponents/DraggableComponent";
import ResizableComponent from "../editorComponents/ResizableComponent"; import ResizableComponent from "../editorComponents/ResizableComponent";
const { DEFAULT_GRID_COLUMNS, DEFAULT_GRID_ROW_HEIGHT } = GridDefaults; const { DEFAULT_GRID_COLUMNS, DEFAULT_GRID_ROW_HEIGHT } = GridDefaults;
export const OccupiedSpaceContext: Context<
OccupiedSpace[] | any
> = createContext(null);
class ContainerWidget extends BaseWidget< class ContainerWidget extends BaseWidget<
ContainerWidgetProps<WidgetProps>, ContainerWidgetProps<WidgetProps>,
@ -78,6 +81,7 @@ class ContainerWidget extends BaseWidget<
})) }))
: null; : null;
} }
getCanvasView() { getCanvasView() {
const style = this.getPositionStyle(); const style = this.getPositionStyle();
const occupiedSpaces = this.getOccupiedSpaces(); const occupiedSpaces = this.getOccupiedSpaces();
@ -85,7 +89,6 @@ class ContainerWidget extends BaseWidget<
<DropTargetComponent <DropTargetComponent
{...this.props} {...this.props}
{...this.state} {...this.state}
occupiedSpaces={occupiedSpaces}
style={{ style={{
...style, ...style,
}} }}
@ -106,7 +109,11 @@ class ContainerWidget extends BaseWidget<
</DraggableComponent> </DraggableComponent>
); );
return this.props.parentId ? renderDraggableComponent : renderComponent; return (
<OccupiedSpaceContext.Provider value={occupiedSpaces}>
{this.props.parentId ? renderDraggableComponent : renderComponent}
</OccupiedSpaceContext.Provider>
);
} }
getWidgetType(): WidgetType { getWidgetType(): WidgetType {