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.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>Appsmith | Editor</title>
</head>
<body>
<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 }[];
};
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 = {
radii: [0, 4, 8, 10, 20, 50],
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`
position: absolute;
pointer-events: none;
z-index: 10;
left: 0;
top: 0;
width: 100%;
@ -26,6 +25,8 @@ type DragLayerProps = {
occupiedSpaces: OccupiedSpace[] | null;
onBoundsUpdate: Function;
isOver: boolean;
parentRows?: number;
parentCols?: number;
};
const DragLayerComponent = (props: DragLayerProps) => {
@ -41,6 +42,8 @@ const DragLayerComponent = (props: DragLayerProps) => {
monitor.getItem(),
props.dropTargetOffset,
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: block;
position: relative;
z-index: 1;
`;
const DragHandle = styled.div`
@ -27,7 +29,6 @@ const DragHandle = styled.div`
cursor: move;
display: none;
cursor: grab;
z-index: 11;
`;
const DeleteControl = styled.div`
@ -36,7 +37,6 @@ const DeleteControl = styled.div`
top: -${props => props.theme.fontSizes[CONTROL_THEME_FONTSIZE_INDEX] / 2}px;
display: none;
cursor: pointer;
z-index: 11;
`;
const moveControlIcon = ControlIcons.MOVE_CONTROL({
@ -100,13 +100,13 @@ const DraggableComponent = (props: DraggableComponentProps) => {
props.style.componentHeight + (props.style.heightUnit || "px"),
}}
>
{props.children}
<DragHandle className="control" ref={drag}>
{moveControlIcon}
</DragHandle>
<DeleteControl className="control" onClick={deleteWidget}>
{deleteControlIcon}
</DeleteControl>
{props.children}
</DraggableWrapper>
</ResizingContext.Provider>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -66,14 +66,13 @@ const areIntersecting = (r1: Rect, r2: Rect) => {
export const isDropZoneOccupied = (
offset: Rect,
widget: WidgetProps,
widgetId: string,
occupied: OccupiedSpace[] | null,
) => {
if (occupied) {
occupied = occupied.filter(widgetDetails => {
return (
widgetDetails.id !== widget.widgetId &&
widgetDetails.parentId !== widget.widgetId
widgetDetails.id !== widgetId && widgetDetails.parentId !== widgetId
);
});
for (let i = 0; i < occupied.length; i++) {
@ -86,6 +85,16 @@ export const isDropZoneOccupied = (
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 = (
clientOffset: XYCoord,
colWidth: number,
@ -93,6 +102,8 @@ export const noCollision = (
widget: WidgetProps & Partial<WidgetConfigProps>,
dropTargetOffset: XYCoord,
occupiedSpaces: OccupiedSpace[] | null,
rows?: number,
cols?: number,
): boolean => {
if (clientOffset && dropTargetOffset && widget) {
const [left, top] = getDropZoneOffsets(
@ -113,7 +124,10 @@ export const noCollision = (
top,
bottom: top + widgetHeight,
};
return !isDropZoneOccupied(currentOffset, widget, occupiedSpaces);
return (
!isDropZoneOccupied(currentOffset, widget.widgetId, occupiedSpaces) &&
!isWidgetOverflowingParentBounds({ rows, cols }, currentOffset)
);
}
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 ContainerComponent from "../editorComponents/ContainerComponent";
import { ContainerOrientation, WidgetType } from "../constants/WidgetConstants";
@ -11,6 +11,9 @@ import DraggableComponent from "../editorComponents/DraggableComponent";
import ResizableComponent from "../editorComponents/ResizableComponent";
const { DEFAULT_GRID_COLUMNS, DEFAULT_GRID_ROW_HEIGHT } = GridDefaults;
export const OccupiedSpaceContext: Context<
OccupiedSpace[] | any
> = createContext(null);
class ContainerWidget extends BaseWidget<
ContainerWidgetProps<WidgetProps>,
@ -78,6 +81,7 @@ class ContainerWidget extends BaseWidget<
}))
: null;
}
getCanvasView() {
const style = this.getPositionStyle();
const occupiedSpaces = this.getOccupiedSpaces();
@ -85,7 +89,6 @@ class ContainerWidget extends BaseWidget<
<DropTargetComponent
{...this.props}
{...this.state}
occupiedSpaces={occupiedSpaces}
style={{
...style,
}}
@ -106,7 +109,11 @@ class ContainerWidget extends BaseWidget<
</DraggableComponent>
);
return this.props.parentId ? renderDraggableComponent : renderComponent;
return (
<OccupiedSpaceContext.Provider value={occupiedSpaces}>
{this.props.parentId ? renderDraggableComponent : renderComponent}
</OccupiedSpaceContext.Provider>
);
}
getWidgetType(): WidgetType {