From 74ee90d816ebb0b8f53fa9ec64c41c6b59b03e3d Mon Sep 17 00:00:00 2001 From: Abhinav Jha Date: Tue, 8 Oct 2019 11:49:10 +0530 Subject: [PATCH] Fix editor stacking context --- app/client/public/index.html | 2 +- app/client/src/constants/DefaultTheme.tsx | 9 +++ .../editorComponents/DragLayerComponent.tsx | 5 +- .../editorComponents/DraggableComponent.tsx | 6 +- .../editorComponents/DropTargetComponent.tsx | 14 ++-- .../src/editorComponents/DropTargetMask.tsx | 1 - app/client/src/editorComponents/Dropzone.tsx | 3 +- .../editorComponents/ResizableComponent.tsx | 77 +++++++++++++++---- app/client/src/pages/Editor/Canvas.tsx | 1 + app/client/src/pages/Editor/index.tsx | 1 - app/client/src/utils/WidgetPropsUtils.tsx | 22 +++++- app/client/src/widgets/ContainerWidget.tsx | 13 +++- 12 files changed, 117 insertions(+), 37 deletions(-) diff --git a/app/client/public/index.html b/app/client/public/index.html index a33d992710..fd226bd343 100755 --- a/app/client/public/index.html +++ b/app/client/public/index.html @@ -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`. --> - React App + Appsmith | Editor diff --git a/app/client/src/constants/DefaultTheme.tsx b/app/client/src/constants/DefaultTheme.tsx index 34b6872f51..45bb80c526 100644 --- a/app/client/src/constants/DefaultTheme.tsx +++ b/app/client/src/constants/DefaultTheme.tsx @@ -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], diff --git a/app/client/src/editorComponents/DragLayerComponent.tsx b/app/client/src/editorComponents/DragLayerComponent.tsx index 454d68b40a..727b604862 100644 --- a/app/client/src/editorComponents/DragLayerComponent.tsx +++ b/app/client/src/editorComponents/DragLayerComponent.tsx @@ -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, ), }), ); diff --git a/app/client/src/editorComponents/DraggableComponent.tsx b/app/client/src/editorComponents/DraggableComponent.tsx index 11892cfbfb..689efd4f8e 100644 --- a/app/client/src/editorComponents/DraggableComponent.tsx +++ b/app/client/src/editorComponents/DraggableComponent.tsx @@ -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} {moveControlIcon} {deleteControlIcon} - {props.children} ); diff --git a/app/client/src/editorComponents/DropTargetComponent.tsx b/app/client/src/editorComponents/DropTargetComponent.tsx index c3d96b7881..d0123e7977 100644 --- a/app/client/src/editorComponents/DropTargetComponent.tsx +++ b/app/client/src/editorComponents/DropTargetComponent.tsx @@ -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 (
{ ? props.style.componentWidth + (props.style.widthUnit || "px") : "100%", top: 0, - background: "white", }} > { visible={isOver} isOver={isExactlyOver} dropTargetOffset={dropTargetOffset} - occupiedSpaces={props.occupiedSpaces} + occupiedSpaces={occupiedSpaces} onBoundsUpdate={handleBoundsUpdate} + parentRows={props.snapRows} + parentCols={props.snapColumns} /> {props.children} diff --git a/app/client/src/editorComponents/DropTargetMask.tsx b/app/client/src/editorComponents/DropTargetMask.tsx index fe0b07c986..5e6c3ee84e 100644 --- a/app/client/src/editorComponents/DropTargetMask.tsx +++ b/app/client/src/editorComponents/DropTargetMask.tsx @@ -16,7 +16,6 @@ export const DropTargetMaskWrapper = styled.div` right: 0; width: 100%; height: 100%; - background: white; ${props => props.showGrid && css` diff --git a/app/client/src/editorComponents/Dropzone.tsx b/app/client/src/editorComponents/Dropzone.tsx index 8a49b268b8..313e44d1d5 100644 --- a/app/client/src/editorComponents/Dropzone.tsx +++ b/app/client/src/editorComponents/Dropzone.tsx @@ -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 = { diff --git a/app/client/src/editorComponents/ResizableComponent.tsx b/app/client/src/editorComponents/ResizableComponent.tsx index 02484f584e..1589eb170e 100644 --- a/app/client/src/editorComponents/ResizableComponent.tsx +++ b/app/client/src/editorComponents/ResizableComponent.tsx @@ -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 ( { 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); }} diff --git a/app/client/src/pages/Editor/Canvas.tsx b/app/client/src/pages/Editor/Canvas.tsx index a5e9350d9a..a16b54aac3 100644 --- a/app/client/src/pages/Editor/Canvas.tsx +++ b/app/client/src/pages/Editor/Canvas.tsx @@ -16,6 +16,7 @@ const ArtBoard = styled.div` height: 100%; position: relative; overflow: auto; + background: white; `; interface CanvasProps { diff --git a/app/client/src/pages/Editor/index.tsx b/app/client/src/pages/Editor/index.tsx index 086ad989b5..02591f4716 100644 --- a/app/client/src/pages/Editor/index.tsx +++ b/app/client/src/pages/Editor/index.tsx @@ -33,7 +33,6 @@ const CanvasContainer = styled.section` right: 0; bottom: 0; left: 0; - z-index: 11; pointer-events: none; } `; diff --git a/app/client/src/utils/WidgetPropsUtils.tsx b/app/client/src/utils/WidgetPropsUtils.tsx index 3e974980f8..9a84e3d1f6 100644 --- a/app/client/src/utils/WidgetPropsUtils.tsx +++ b/app/client/src/utils/WidgetPropsUtils.tsx @@ -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, 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; }; diff --git a/app/client/src/widgets/ContainerWidget.tsx b/app/client/src/widgets/ContainerWidget.tsx index ec1ded6bb3..c4e0af788b 100644 --- a/app/client/src/widgets/ContainerWidget.tsx +++ b/app/client/src/widgets/ContainerWidget.tsx @@ -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, @@ -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< ); - return this.props.parentId ? renderDraggableComponent : renderComponent; + return ( + + {this.props.parentId ? renderDraggableComponent : renderComponent} + + ); } getWidgetType(): WidgetType {