diff --git a/app/client/jest.config.js b/app/client/jest.config.js index b40121f623..1b38ae9af7 100644 --- a/app/client/jest.config.js +++ b/app/client/jest.config.js @@ -17,7 +17,7 @@ module.exports = { moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node", "css"], moduleDirectories: ["node_modules", "src", "test"], transformIgnorePatterns: [ - "/node_modules/(?!codemirror|design-system|design-system-old|react-dnd|dnd-core|@babel|(@blueprintjs)|@github|lodash-es|@draft-js-plugins|react-documents|linkedom|assert-never)", + "/node_modules/(?!codemirror|konva|design-system|design-system-old|react-dnd|dnd-core|@babel|(@blueprintjs)|@github|lodash-es|@draft-js-plugins|react-documents|linkedom|assert-never)", ], moduleNameMapper: { "\\.(css|less)$": "/test/__mocks__/styleMock.js", diff --git a/app/client/package.json b/app/client/package.json index de934e1b83..6ac102d603 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -288,6 +288,7 @@ "@typescript-eslint/parser": "^6.7.4", "babel-plugin-lodash": "^3.3.4", "babel-plugin-module-resolver": "^4.1.0", + "canvas": "^2.11.2", "chalk": "^4.1.1", "compression-webpack-plugin": "^10.0.0", "cra-bundle-analyzer": "^0.1.0", diff --git a/app/client/src/layoutSystems/common/WidgetNamesCanvas/WidgetNameTypes.ts b/app/client/src/layoutSystems/common/WidgetNamesCanvas/WidgetNameTypes.ts index a1e7b45838..f6ea0ad15c 100644 --- a/app/client/src/layoutSystems/common/WidgetNamesCanvas/WidgetNameTypes.ts +++ b/app/client/src/layoutSystems/common/WidgetNamesCanvas/WidgetNameTypes.ts @@ -31,3 +31,23 @@ export interface CanvasPositions { yDiff: number; height: number; } + +export interface WidgetNamePositionType { + selected: WidgetNamePositionData | undefined; + focused: WidgetNamePositionData | undefined; +} + +// TODO(abhinav): Update this at the source of the setDraggingState function +export type SetDragginStateFnType = ({ + draggedOn, + draggingGroupCenter, + dragGroupActualParent, + isDragging, + startPoints, +}: { + isDragging: boolean; + dragGroupActualParent?: string | undefined; + draggingGroupCenter?: Record | undefined; + startPoints?: any; + draggedOn?: string | undefined; +}) => void; diff --git a/app/client/src/layoutSystems/common/WidgetNamesCanvas/eventHandlers.ts b/app/client/src/layoutSystems/common/WidgetNamesCanvas/eventHandlers.ts new file mode 100644 index 0000000000..9caaddc0e6 --- /dev/null +++ b/app/client/src/layoutSystems/common/WidgetNamesCanvas/eventHandlers.ts @@ -0,0 +1,179 @@ +import type { DragEventHandler, MutableRefObject, DragEvent } from "react"; +import type { + CanvasPositions, + SetDragginStateFnType, + WidgetNamePositionType, +} from "./WidgetNameTypes"; +import { throttle } from "lodash"; +import { getMainContainerAnvilCanvasDOMElement } from "./widgetNameRenderUtils"; + +/** + * This returns a callback for scroll event on the MainContainer + * + * This callback does the following: + * 1. Sets the scrolling state to 1 if it is not already set to 0. + * A value of 0 signifies that we've only just started scrolling and this event has triggered + * So, we set it to 1 after we've reset the canvas. + * We reset the canvas as we donot want to show any widget names while scrolling. + * + * 2. We update the scrollTop in a ref. This is used to calculate the position of the widget name + * We also wrap this in a requestAnimationFrame to ensure that we get the latest scrollTop value and it doesn't cause layout thrashing + * + * 3. If there is actually a scroll ofset, we set hasScroll to true + * + * @returns void + */ +export function getScrollHandler( + isScrolling: MutableRefObject, + hasScroll: MutableRefObject, + resetCanvas: () => void, + scrollTop: MutableRefObject, +) { + return function handleScroll() { + const scrollParent: HTMLDivElement | null = + getMainContainerAnvilCanvasDOMElement(); + if (!scrollParent) return; + + if (isScrolling.current === 0) { + isScrolling.current = 1; + resetCanvas(); + } + + window.requestAnimationFrame(() => { + scrollTop.current = scrollParent.scrollTop; + if (scrollParent.scrollHeight > scrollParent.clientHeight) { + hasScroll.current = true; + } + }); + }; +} + +/** + * + * This returns a callback for scroll end event on the MainContainer + * + * This callback does the following: + * 1. Sets the scrolling state to 0 (see handleScroll) + * 2. If there is a scroll offset, we update the positions of the selected and focused widget names + */ +export function getScrollEndHandler( + isScrolling: MutableRefObject, + hasScroll: MutableRefObject, + updateSelectedWidgetPositions: () => void, +) { + return function handleScrollEnd() { + isScrolling.current = 0; + if (hasScroll.current) { + updateSelectedWidgetPositions(); + } + }; +} + +/** + * This Method verifies if the mouse position coincides with any widget name drawn on canvas + * and returns details regarding the widget + * @param e Mouse event + * @returns Mainly isMouseOver indicating if the mouse is on any one of the widget name + * if true also returns data regarding the widget + */ +export function getMouseOverDetails( + e: MouseEvent, + canvasPositions: MutableRefObject, + widgetNamePositions: MutableRefObject, +) { + const x = e.clientX - canvasPositions.current.left; + const y = e.clientY - canvasPositions.current.top; + const widgetNamePositionsArray = Object.values(widgetNamePositions.current); + + //for selected and focused widget names check the widget name positions with respect to mouse positions + for (const widgetNamePosition of widgetNamePositionsArray) { + if (widgetNamePosition) { + const { height, left, top, widgetNameData, width } = widgetNamePosition; + if (x > left && x < left + width && y > top && y < top + height) { + return { isMouseOver: true, cursor: "pointer", widgetNameData }; + } + } + } + return { isMouseOver: false }; +} + +export function getMouseMoveHandler( + wrapperRef: MutableRefObject, + canvasPositions: MutableRefObject, + widgetNamePositions: MutableRefObject, +) { + /** + * Mouse Move event function, this tracks every mouse move on canvas such that + * if the mouse position coincides with the positions of widget name, it makes the canvas intractable + * This is throttled since it tracks every single mouse move + */ + return throttle((e: MouseEvent) => { + const wrapper = wrapperRef?.current as HTMLDivElement; + if (!wrapper) return; + + //check if the mouse is coinciding with the widget name drawing on canvas + const { cursor, isMouseOver } = getMouseOverDetails( + e, + canvasPositions, + widgetNamePositions, + ); + + //if mouse over make the canvas intractable + if (isMouseOver) { + if (wrapper.style.pointerEvents === "none") { + wrapper.style.pointerEvents = "auto"; + } + } // if not mouse over then keep it default + else if (wrapper.style.pointerEvents !== "none") { + wrapper.style.pointerEvents = "none"; + wrapper.style.cursor = "default"; + } + + //set cursor based on intractability + if (!cursor) { + wrapper.style.cursor = "default"; + } else if (wrapper.style.cursor !== cursor) { + wrapper.style.cursor = cursor; + } + }, 20); +} + +/** + * on Drag Start event handler to enable drag of widget from the widget name component drawing on canvas + */ +export function getDragStartHandler( + showTableFilterPane: () => void, + setDraggingState: SetDragginStateFnType, + shouldAllowDrag: boolean, + canvasPositions: MutableRefObject, + widgetNamePositions: MutableRefObject, +): DragEventHandler { + return (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + + //checks if the mouse is over the widget name, if so return it's details + const { isMouseOver, widgetNameData } = getMouseOverDetails( + e as unknown as MouseEvent, + canvasPositions, + widgetNamePositions, + ); + + if (!isMouseOver || !shouldAllowDrag || widgetNameData?.dragDisabled) + return; + + //set dragging state + const startPoints = { + top: 0, + left: 0, + }; + showTableFilterPane(); + setDraggingState({ + isDragging: true, + dragGroupActualParent: widgetNameData?.parentId, + draggingGroupCenter: { widgetId: widgetNameData?.id }, + startPoints, + draggedOn: widgetNameData?.parentId, + }); + }; +} diff --git a/app/client/src/layoutSystems/common/WidgetNamesCanvas/index.tsx b/app/client/src/layoutSystems/common/WidgetNamesCanvas/index.tsx index 26f6a95055..72bc313446 100644 --- a/app/client/src/layoutSystems/common/WidgetNamesCanvas/index.tsx +++ b/app/client/src/layoutSystems/common/WidgetNamesCanvas/index.tsx @@ -1,10 +1,7 @@ -import type { DragEventHandler, DragEvent } from "react"; import React, { useEffect, useRef } from "react"; import { useSelector } from "react-redux"; -import { throttle } from "lodash"; import { Layer, Stage } from "react-konva/lib/ReactKonvaCore"; import { useWidgetSelection } from "utils/hooks/useWidgetSelection"; -import { SelectionRequestType } from "sagas/WidgetSelectUtils"; import { useShowTableFilterPane, @@ -13,12 +10,10 @@ import { import type { CanvasPositions, WidgetNameData, - WidgetNamePositionData, - WIDGET_NAME_TYPE, + WidgetNamePositionType, } from "./WidgetNameTypes"; import { DEFAULT_WIDGET_NAME_CANVAS_HEIGHT, - WIDGET_NAME_CANVAS_PADDING, widgetNameWrapperStyle, WIDGET_NAME_CANVAS, } from "./WidgetNameConstants"; @@ -26,11 +21,20 @@ import { getFocusedWidgetNameData, getSelectedWidgetNameData, } from "../selectors"; -import type { LayoutElementPosition } from "layoutSystems/common/types"; + import { getShouldAllowDrag } from "selectors/widgetDragSelectors"; import type { Stage as CanvasStageType } from "konva/lib/Stage"; -import type { Layer as KonvaLayer } from "konva/lib/Layer"; -import { getWidgetNameComponent } from "./utils"; +import { + getMainContainerAnvilCanvasDOMElement, + resetCanvas, + updateSelectedWidgetPositions, +} from "./widgetNameRenderUtils"; +import { + getDragStartHandler, + getMouseMoveHandler, + getScrollEndHandler, + getScrollHandler, +} from "./eventHandlers"; /** * This Component contains logic to draw widget name on canvas @@ -38,34 +42,31 @@ import { getWidgetNameComponent } from "./utils"; * @param props Object that contains * @prop canvasWidth width of canvas in pixels * @prop containerRef ref of PageViewWrapper component - * @prop parentRef ref of the MainContainerWrapper component i.e, the parent of the canvas component */ const OverlayCanvasContainer = (props: { canvasWidth: number; - containerRef: React.RefObject; - parentRef: React.RefObject; + containerRef: React.RefObject; }) => { //widget name data of widgets - const selectedWidgetNameData: WidgetNameData | undefined = useSelector( + const selectedWidgetNameData: WidgetNameData[] | undefined = useSelector( getSelectedWidgetNameData, ); const focusedWidgetNameData: WidgetNameData | undefined = useSelector( getFocusedWidgetNameData, ); - + // should we allow dragging of widgets const shouldAllowDrag = useSelector(getShouldAllowDrag); - - const wrapperRef = useRef(null); - - // used to keep track of positions of widgetName drawn on canvas to make it intractable - const widgetNamePositions = useRef<{ - selected: WidgetNamePositionData | undefined; - focused: WidgetNamePositionData | undefined; - }>({ selected: undefined, focused: undefined }); - + // When we begin dragging, the drag and resize hooks need a few details to take over const { setDraggingState } = useWidgetDragResize(); const showTableFilterPane = useShowTableFilterPane(); + const { selectWidget } = useWidgetSelection(); + const wrapperRef = useRef(null); + // used to keep track of positions of widgetName drawn on canvas to make it intractable + const widgetNamePositions = useRef({ + selected: undefined, + focused: undefined, + }); //Positions of canvas const canvasPositions = useRef({ top: 0, @@ -77,19 +78,28 @@ const OverlayCanvasContainer = (props: { }); const scrollTop = useRef(0); - const isScrolling = useRef(0); + const isScrolling = useRef(0); const hasScroll = useRef(false); - const stageRef = useRef(null); + const stageRef = useRef(null); - const { selectWidget } = useWidgetSelection(); + // Pre bind arguments to the updateSelectedWidgetPositions function + // This makes it easier to use the function later in the code + const updateFn = updateSelectedWidgetPositions.bind(this, { + stageRef, + selectedWidgetNameData, + focusedWidgetNameData, + selectWidget, + scrollTop, + widgetNamePositions, + canvasPositions, + }); - //used to set canvasPositions, which is used further to calculate the exact positions of widgets + // Used to set canvasPositions, which is used further to calculate the exact positions of widgets useEffect(() => { if (!stageRef?.current?.content || !wrapperRef?.current) return; const HTMLCanvas: HTMLDivElement = stageRef?.current?.content; const rect: DOMRect = HTMLCanvas.getBoundingClientRect(); - const wrapper: HTMLDivElement = wrapperRef?.current as HTMLDivElement; const wrapperRect: DOMRect = wrapper.getBoundingClientRect(); @@ -99,266 +109,86 @@ const OverlayCanvasContainer = (props: { height: wrapperRect.height, left: rect.left, top: rect.top, - width: rect.width, + width: wrapperRect.width, }; } }, [wrapperRef?.current, props.canvasWidth]); /** - * Method used to add widget name to the Konva canvas' layer - * @param layer Konva layer onto which the widget name is to be added - * @param widgetNameData widget name data contains more information regarding the widget that is used in drawing the name - * @param position position of widget in pixels - * @param type if it's either selected or focused widget name + * Adds 3 event listeners. + * 1. Mouse Move: On the container, to check if the mouse is over a widget, so that we can focus it + * 2. Scroll: On the MainContainer, to check if the user is scrolling. This is so that we can hide the widget names + * Also, this tells us that we need to compute and store scroll offset values to correctly position the widget name components. + * 3. Scroll End: On the MainContainer, to check if the user has stopped scrolling. This is so that we can show the widget names again */ - const addWidgetNameToCanvas = ( - layer: KonvaLayer, - widgetNameData: WidgetNameData, - position: LayoutElementPosition, - type: WIDGET_NAME_TYPE, - ) => { - if (!position) return; - - const { id: widgetId, widgetName } = widgetNameData; - - //Get Widget Name - if (widgetName) { - const { - canvasLeftOffset, - canvasTopOffset, - widgetNameComponent, - widgetNamePosition, - } = getWidgetNameComponent( - position, - widgetName, - widgetNameData, - props?.parentRef?.current, - stageRef?.current?.content, - scrollTop.current, - ); - - widgetNamePositions.current[type] = { ...widgetNamePosition }; - - canvasPositions.current = { - ...canvasPositions.current, - xDiff: canvasLeftOffset, - yDiff: canvasTopOffset, - }; - - //Make widget name clickable - widgetNameComponent.on("click", () => { - selectWidget(SelectionRequestType.One, [widgetId]); - }); - - //Add widget name to canvas - layer.add(widgetNameComponent); - } - }; - - /** - * This method is called whenever there is a change in state of canvas, - * i.e, widget position is changed, canvas resized, selected widget changes - * @param widgetPosition - */ - const updateSelectedWidgetPositions = ( - widgetPosition?: LayoutElementPosition, - ) => { - if (!stageRef?.current) return; - - const stage = stageRef.current; - const layer = stage.getLayers()[0]; - //destroy all drawings on canvas - layer.destroyChildren(); - - //Check and draw selected Widget - if (selectedWidgetNameData) { - const { position: selectedWidgetPosition } = selectedWidgetNameData; - - const position = widgetPosition || selectedWidgetPosition; - - addWidgetNameToCanvas( - layer, - selectedWidgetNameData, - position, - "selected", - ); - } - - //Check and draw focused Widget - if (focusedWidgetNameData) { - const { position } = focusedWidgetNameData; - - addWidgetNameToCanvas(layer, focusedWidgetNameData, position, "focused"); - } - - layer.draw(); - }; - - /** - * Mouse Move event function, this tracks every mouse move on canvas such that - * if the mouse position coincides with the positions of widget name, it makes the canvas intractable - * This is throttled since it tracks every single mouse move - */ - const handleMouseMove = throttle((e: MouseEvent) => { - const wrapper = wrapperRef?.current as HTMLDivElement; - if (!wrapper) return; - - //check if the mouse is coinciding with the widget name drawing on canvas - const { cursor, isMouseOver } = getMouseOverDetails(e); - - //if mouse over make the canvas intractable - if (isMouseOver) { - if (wrapper.style.pointerEvents === "none") { - wrapper.style.pointerEvents = "auto"; - } - } // if not mouse over then keep it default - else if (wrapper.style.pointerEvents !== "none") { - wrapper.style.pointerEvents = "none"; - wrapper.style.cursor = "default"; - } - - //set cursor based on intractability - if (!cursor) { - wrapper.style.cursor = "default"; - } else if (wrapper.style.cursor !== cursor) { - wrapper.style.cursor = cursor; - } - }, 20); - - /** - * on Drag Start event handler to enable drag of widget from the widget name component drawing on canvas - * @param e - */ - const handleDragStart: DragEventHandler = (e: DragEvent) => { - e.preventDefault(); - e.stopPropagation(); - - //checks if the mouse is over the widget name, if so return it's details - const { isMouseOver, widgetNameData } = getMouseOverDetails( - e as unknown as MouseEvent, - ); - - if (!isMouseOver || !shouldAllowDrag || widgetNameData?.dragDisabled) - return; - - //set dragging state - const startPoints = { - top: 0, - left: 0, - }; - showTableFilterPane(); - setDraggingState({ - isDragging: true, - dragGroupActualParent: widgetNameData?.parentId, - draggingGroupCenter: { widgetId: widgetNameData?.id }, - startPoints, - draggedOn: widgetNameData?.parentId, - }); - }; - - /** - * handle Scroll of the canvas, this helps in keeping track og canvas scroll - * so that the widget name remains accurately placed even when the canvas is scrolled - */ - const handleScroll = () => { - if (!props.parentRef?.current) return; - - const currentScrollTop: number = props.parentRef?.current?.scrollTop; - - if (!isScrolling.current) { - resetCanvas(); - } - - clearTimeout(isScrolling.current); - isScrolling.current = setTimeout(() => { - scrollTop.current = currentScrollTop; - //while scrolling update the widget name position - updateSelectedWidgetPositions(); - isScrolling.current = 0; - if ( - (props.parentRef?.current?.scrollHeight || 0) > - (props.parentRef?.current?.clientHeight || 0) - ) - hasScroll.current = true; - }, 100); - }; - - //Add event listeners useEffect(() => { - if ( - !props.containerRef?.current || - !props.parentRef?.current || - !wrapperRef?.current - ) + const scrollParent: HTMLDivElement | null = + getMainContainerAnvilCanvasDOMElement(); + + if (!props.containerRef?.current || !wrapperRef?.current || !scrollParent) return; const container: HTMLDivElement = props.containerRef ?.current as HTMLDivElement; - const parent: HTMLDivElement = props.parentRef?.current as HTMLDivElement; - container.addEventListener("mousemove", handleMouseMove); - parent.addEventListener("scroll", handleScroll); + const reset = resetCanvas.bind(this, widgetNamePositions, stageRef); + + const scrollHandler = getScrollHandler( + isScrolling, + hasScroll, + reset, + scrollTop, + ); + + const scrollEndHandler = getScrollEndHandler( + isScrolling, + hasScroll, + updateFn, + ); + + const mouseMoveHandler = getMouseMoveHandler( + wrapperRef, + canvasPositions, + widgetNamePositions, + ); + + container.addEventListener("mousemove", mouseMoveHandler); + scrollParent.addEventListener("scroll", scrollHandler); + scrollParent.addEventListener("scrollend", scrollEndHandler); + return () => { - container.removeEventListener("mousemove", handleMouseMove); - parent.removeEventListener("scroll", handleScroll); + container.removeEventListener("mousemove", mouseMoveHandler); + scrollParent.removeEventListener("scroll", scrollHandler); + scrollParent.removeEventListener("scrollend", scrollEndHandler); }; }, [ props.containerRef?.current, - props.parentRef?.current, wrapperRef?.current, widgetNamePositions.current, canvasPositions.current, ]); - /** - * This Method verifies if the mouse position coincides with any widget name drawn on canvas - * and returns details regarding the widget - * @param e Mouse event - * @returns Mainly isMouseOver indicating if the mouse is on any one of the widget name - * if true also returns data regarding the widget - */ - const getMouseOverDetails = (e: MouseEvent) => { - const x = e.clientX - canvasPositions.current.left; - const y = e.clientY - canvasPositions.current.top; - const widgetNamePositionsArray = Object.values(widgetNamePositions.current); + // Reset the canvas if no widgets are focused or selected + // Update the widget name positions if there are widgets focused or selected + // and they've changed. - //for selected and focused widget names check the widget name positions with respect to mouse positions - for (const widgetNamePosition of widgetNamePositionsArray) { - if (widgetNamePosition) { - const { height, left, top, widgetNameData, width } = widgetNamePosition; - if (x > left && x < left + width && y > top && y < top + height) { - return { isMouseOver: true, cursor: "pointer", widgetNameData }; - } - } - } - - return { isMouseOver: false }; - }; - - //Used when the position of selected or focused widget changes + // Note: If the selector for `selectWidgetNameData` reference changes + // Then this will run on every render. We should be careful about this. useEffect(() => { if (!selectedWidgetNameData && !focusedWidgetNameData) { - resetCanvas(); + resetCanvas(widgetNamePositions, stageRef); } else { - updateSelectedWidgetPositions(); + updateFn(); } }, [selectedWidgetNameData, focusedWidgetNameData]); - /** - * Resets canvas when there is nothing to be drawn on canvas - */ - const resetCanvas = () => { - // Resets stored widget position names - widgetNamePositions.current = { selected: undefined, focused: undefined }; - - // clears all drawings on canvas - const stage = stageRef.current; - if (!stage) return; - const layer = stage.getLayers()[0]; - if (!layer) return; - layer.destroyChildren(); - layer.draw(); - }; + const handleDragStart = getDragStartHandler( + showTableFilterPane, + setDraggingState, + shouldAllowDrag, + canvasPositions, + widgetNamePositions, + ); return (
diff --git a/app/client/src/layoutSystems/common/WidgetNamesCanvas/utils.ts b/app/client/src/layoutSystems/common/WidgetNamesCanvas/utils.ts index b1ba5111fe..a491ce27b9 100644 --- a/app/client/src/layoutSystems/common/WidgetNamesCanvas/utils.ts +++ b/app/client/src/layoutSystems/common/WidgetNamesCanvas/utils.ts @@ -26,7 +26,6 @@ import { * widgetName Group on Konva, position of widgetName on canvas and canvas offsets */ export const getWidgetNameComponent = ( - position: LayoutElementPosition, widgetName: string, widgetNameData: WidgetNameData, parentDOM: HTMLDivElement | null, @@ -64,8 +63,14 @@ export const getWidgetNameComponent = ( canvasTopOffset, left: widgetLeft, top: widgetTop, - } = getPositionsForBoundary(parentDOM, htmlCanvasDOM, position, scrollTop); - const left: number = widgetLeft + position.width - componentWidth; + } = getPositionsForBoundary( + parentDOM, + htmlCanvasDOM, + widgetNameData.position, + scrollTop, + ); + const left: number = + widgetLeft + widgetNameData.position.width - componentWidth; const top: number = widgetTop - WIDGET_NAME_HEIGHT; //Store the widget name positions for future use diff --git a/app/client/src/layoutSystems/common/WidgetNamesCanvas/widgetNameRenderUtils.ts b/app/client/src/layoutSystems/common/WidgetNamesCanvas/widgetNameRenderUtils.ts new file mode 100644 index 0000000000..8df12964aa --- /dev/null +++ b/app/client/src/layoutSystems/common/WidgetNamesCanvas/widgetNameRenderUtils.ts @@ -0,0 +1,198 @@ +import type { MutableRefObject } from "react"; +import type { Stage as CanvasStageType } from "konva/lib/Stage"; +import type { Layer as KonvaLayer } from "konva/lib/Layer"; + +import type { + CanvasPositions, + WIDGET_NAME_TYPE, + WidgetNameData, + WidgetNamePositionType, +} from "./WidgetNameTypes"; +import { SelectionRequestType } from "sagas/WidgetSelectUtils"; +import { getWidgetNameComponent } from "./utils"; +import type { KonvaEventListener } from "konva/lib/Node"; +import type { Group } from "konva/lib/Group"; +import { MAIN_CONTAINER_WIDGET_ID } from "constants/WidgetConstants"; +import { getAnvilCanvasId } from "layoutSystems/anvil/canvas/utils"; + +export function getMainContainerAnvilCanvasDOMElement() { + const mainContainerAnvilCanvasDOMId = getAnvilCanvasId( + MAIN_CONTAINER_WIDGET_ID, + ); + + return document.getElementById( + mainContainerAnvilCanvasDOMId, + ) as HTMLDivElement | null; +} + +/** + * Resets canvas when there is nothing to be drawn on canvas + */ +export function resetCanvas( + widgetNamePositions: MutableRefObject, + stageRef: MutableRefObject, +) { + // Resets stored widget position names + widgetNamePositions.current = { selected: undefined, focused: undefined }; + + // clears all drawings on canvas + const stage = stageRef.current; + if (!stage) return; + const layer = stage.getLayers()[0]; + if (!layer) return; + layer.destroyChildren(); + layer.draw(); +} + +/** + * This method is used to draw the widget name components on the canvas for the + * selected and focused widgets. + * 1. It loops through all the selected widgets and draws the names for all of them + * 2. It draws the name for the focused widget + * + * ALL of the arguments are passed down to the `addWidgetNameToCanvas` method + * except for `stageRef` which is used to get the Konva stage and layer and the + * selectedWidgetNameData and focusedWidgetNameData which are used to call individual + * `addWidgetNameToCanvas` methods. + * + * This method finally draws the layer to commit the changes computed by `addWidgetNameToCanvas` calls + * + */ +export const updateSelectedWidgetPositions = (props: { + stageRef: MutableRefObject; + selectedWidgetNameData: WidgetNameData[] | undefined; + focusedWidgetNameData: WidgetNameData | undefined; + selectWidget: ( + type: SelectionRequestType, + payload?: string[] | undefined, + ) => void; + scrollTop: MutableRefObject; + widgetNamePositions: MutableRefObject; + canvasPositions: MutableRefObject; +}) => { + const { + canvasPositions, + focusedWidgetNameData, + scrollTop, + selectedWidgetNameData, + selectWidget, + stageRef, + widgetNamePositions, + } = props; + if (!stageRef?.current) return; + + const stage = stageRef.current; + const layer = stage.getLayers()[0]; + // Clean up the layer so that we can update all the widget names + layer.destroyChildren(); + + // For each selected widget, draw the widget name + if (selectedWidgetNameData && selectedWidgetNameData.length > 0) { + for (const widgetNameData of selectedWidgetNameData) { + addWidgetNameToCanvas( + layer, + widgetNameData, + "selected", + selectWidget, + scrollTop, + stageRef, + widgetNamePositions, + canvasPositions, + ); + } + } + + // Draw the focused widget name + if (focusedWidgetNameData) { + addWidgetNameToCanvas( + layer, + focusedWidgetNameData, + "focused", + selectWidget, + scrollTop, + stageRef, + widgetNamePositions, + canvasPositions, + ); + } + + layer.draw(); +}; + +/** + * This method adds the widget name on the canvas and adds the click event handler to the widget name component + * + * @param layer : The KonvaLayer on which to draw + * @param widgetNameData : the WidgetName data for the widget + * @param type: Whether we need to draw the selected or focused widget name + * @param selectWidget: The selectWidget method to call when the widget name is clicked + * @param scrollTop: The amount of pixels scrolled by the canvas + * @param stageRef: The Konva stage reference + * @param widgetNamePositions: The widget name positions (selected and focused) + * @param canvasPositions: The canvas positions + * @returns void + */ +export const addWidgetNameToCanvas = ( + layer: KonvaLayer, + widgetNameData: WidgetNameData, + type: WIDGET_NAME_TYPE, + selectWidget: ( + type: SelectionRequestType, + payload?: string[] | undefined, + ) => void, + scrollTop: MutableRefObject, + stageRef: MutableRefObject, + widgetNamePositions: MutableRefObject, + canvasPositions: MutableRefObject, +) => { + // If we don't have the positions, return + if (!widgetNameData.position) return; + + const { id: widgetId, widgetName } = widgetNameData; + + // Get the scroll parent to calculate the offsets + const scrollParent = getMainContainerAnvilCanvasDOMElement(); + + // If we have a widget name + // Use Konva APIs to draw the text (see `getWidgetNameComponent`) + if (widgetName) { + const { + canvasLeftOffset, + canvasTopOffset, + widgetNameComponent, + widgetNamePosition, + } = getWidgetNameComponent( + widgetName, + widgetNameData, + scrollParent, + stageRef?.current?.content, + scrollTop.current, + ); + + // Store the drawn widget name position + widgetNamePositions.current[type] = { ...widgetNamePosition }; + + // Update the Canvas positions' x and y diffs + canvasPositions.current = { + ...canvasPositions.current, + xDiff: canvasLeftOffset, + yDiff: canvasTopOffset, + }; + + // Create Konva event handler + // Note: The stopPropagation() doesn't seem to be working, so another workaround has been added to the WidgetsEditor component + const eventHandler: KonvaEventListener = ( + konvaEvent, + ) => { + selectWidget(SelectionRequestType.One, [widgetId]); + konvaEvent.cancelBubble = true; + konvaEvent.evt.stopPropagation(); + }; + + //Make widget name clickable + widgetNameComponent.on("click", eventHandler); + + //Add widget name to canvas + layer.add(widgetNameComponent); + } +}; diff --git a/app/client/src/layoutSystems/common/selectors.ts b/app/client/src/layoutSystems/common/selectors.ts index f7683cc328..7caac10131 100644 --- a/app/client/src/layoutSystems/common/selectors.ts +++ b/app/client/src/layoutSystems/common/selectors.ts @@ -77,21 +77,21 @@ export const getSelectedWidgetNameData = createSelector( widgets, dataTree, shouldShowWidgetName, - ): WidgetNameData | undefined => { + ): WidgetNameData[] | undefined => { if ( !selectedWidgets || - selectedWidgets.length !== 1 || + selectedWidgets.length === 0 || !shouldShowWidgetName ) return; - - const selectedWidgetId = selectedWidgets[0]; - - const selectedWidget = widgets[selectedWidgetId]; - - if (!selectedWidget) return; - - return getWidgetNameState(selectedWidget, dataTree, positions); + const result: WidgetNameData[] = []; + for (const selectedWidgetId of selectedWidgets) { + const selectedWidget = widgets[selectedWidgetId]; + if (!selectedWidget) continue; + result.push(getWidgetNameState(selectedWidget, dataTree, positions)); + } + if (result.length > 0) return result; + else return; }, ); diff --git a/app/client/src/layoutSystems/common/useLayoutSystemFeatures.ts b/app/client/src/layoutSystems/common/useLayoutSystemFeatures.ts index 44ea6b6938..4556befafb 100644 --- a/app/client/src/layoutSystems/common/useLayoutSystemFeatures.ts +++ b/app/client/src/layoutSystems/common/useLayoutSystemFeatures.ts @@ -3,27 +3,31 @@ import { LayoutSystemTypes } from "layoutSystems/types"; import { getLayoutSystemType } from "selectors/layoutSystemSelectors"; export enum LayoutSystemFeatures { - ENABLE_MAIN_CONTAINER_RESIZER = "ENABLE_MAIN_CONTAINER_RESIZER", //enable main canvas resizer - ENABLE_FORKING_FROM_TEMPLATES = "ENABLE_FORKING_FROM_TEMPLATES", //enable forking pages from template directly inside apps - ENABLE_CANVAS_LAYOUT_CONTROL = "ENABLE_CANVAS_LAYOUT_CONTROL", //enables layout control option in property pane + ENABLE_MAIN_CONTAINER_RESIZER = "ENABLE_MAIN_CONTAINER_RESIZER", + ENABLE_FORKING_FROM_TEMPLATES = "ENABLE_FORKING_FROM_TEMPLATES", + ENABLE_CANVAS_LAYOUT_CONTROL = "ENABLE_CANVAS_LAYOUT_CONTROL", + ENABLE_CANVAS_OVERLAY_FOR_EDITOR_UI = "ENABLE_CANVAS_OVERLAY_FOR_EDITOR_UI", } const FIXED_LAYOUT_FEATURES: Record = { [LayoutSystemFeatures.ENABLE_FORKING_FROM_TEMPLATES]: true, [LayoutSystemFeatures.ENABLE_CANVAS_LAYOUT_CONTROL]: true, [LayoutSystemFeatures.ENABLE_MAIN_CONTAINER_RESIZER]: false, + [LayoutSystemFeatures.ENABLE_CANVAS_OVERLAY_FOR_EDITOR_UI]: false, }; const AUTO_LAYOUT_FEATURES: Record = { [LayoutSystemFeatures.ENABLE_FORKING_FROM_TEMPLATES]: false, [LayoutSystemFeatures.ENABLE_CANVAS_LAYOUT_CONTROL]: false, [LayoutSystemFeatures.ENABLE_MAIN_CONTAINER_RESIZER]: true, + [LayoutSystemFeatures.ENABLE_CANVAS_OVERLAY_FOR_EDITOR_UI]: false, }; const ANVIL_FEATURES: Record = { [LayoutSystemFeatures.ENABLE_FORKING_FROM_TEMPLATES]: false, [LayoutSystemFeatures.ENABLE_CANVAS_LAYOUT_CONTROL]: false, [LayoutSystemFeatures.ENABLE_MAIN_CONTAINER_RESIZER]: true, + [LayoutSystemFeatures.ENABLE_CANVAS_OVERLAY_FOR_EDITOR_UI]: true, }; /** diff --git a/app/client/src/pages/Editor/WidgetsEditor/MainContainerWrapper.tsx b/app/client/src/pages/Editor/WidgetsEditor/MainContainerWrapper.tsx index ab954c83e3..1c6f7c2c4e 100644 --- a/app/client/src/pages/Editor/WidgetsEditor/MainContainerWrapper.tsx +++ b/app/client/src/pages/Editor/WidgetsEditor/MainContainerWrapper.tsx @@ -39,6 +39,7 @@ import { } from "../../../layoutSystems/common/useLayoutSystemFeatures"; import { CANVAS_VIEWPORT } from "constants/componentClassNameConstants"; import { MainContainerResizer } from "layoutSystems/common/mainContainerResizer/MainContainerResizer"; +import OverlayCanvasContainer from "layoutSystems/common/WidgetNamesCanvas"; interface MainCanvasWrapperProps { isPreviewMode: boolean; @@ -46,6 +47,7 @@ interface MainCanvasWrapperProps { navigationHeight?: number; isAppSettingsPaneWithNavigationTabOpen?: boolean; currentPageId: string; + parentRef: React.RefObject; } const Wrapper = styled.section<{ @@ -141,9 +143,11 @@ function MainContainerWrapper(props: MainCanvasWrapperProps) { const isWDSV2Enabled = useFeatureFlag("ab_wds_enabled"); const checkLayoutSystemFeatures = useLayoutSystemFeatures(); - const [enableMainContainerResizer] = checkLayoutSystemFeatures([ - LayoutSystemFeatures.ENABLE_MAIN_CONTAINER_RESIZER, - ]); + const [enableMainContainerResizer, enableOverlayCanvas] = + checkLayoutSystemFeatures([ + LayoutSystemFeatures.ENABLE_MAIN_CONTAINER_RESIZER, + LayoutSystemFeatures.ENABLE_CANVAS_OVERLAY_FOR_EDITOR_UI, + ]); useEffect(() => { return () => { @@ -250,6 +254,12 @@ function MainContainerWrapper(props: MainCanvasWrapperProps) {
)} {node} + {enableOverlayCanvas && ( + + )} (null); + useEffect(() => { if (navigationPreviewRef?.current) { const { offsetHeight } = navigationPreviewRef.current; @@ -117,22 +119,34 @@ function WidgetsEditor() { const allowDragToSelect = useAllowEditorDragToSelect(); const { isAutoHeightWithLimitsChanging } = useAutoHeightUIState(); - const handleWrapperClick = useCallback(() => { - // Making sure that we don't deselect the widget - // after we are done dragging the limits in auto height with limits - if (allowDragToSelect && !isAutoHeightWithLimitsChanging) { - focusWidget && focusWidget(); - deselectAll && deselectAll(); - dispatch(closePropertyPane()); - dispatch(closeTableFilterPane()); - dispatch(setCanvasSelectionFromEditor(false)); - } - }, [ - allowDragToSelect, - focusWidget, - deselectAll, - isAutoHeightWithLimitsChanging, - ]); + const handleWrapperClick = useCallback( + (e: any) => { + // This is a hack for widget name component clicks on Canvas. + // For some reason the stopPropagation in the konva event listener isn't working + // Also, the nodeName is available only for the konva event, so standard type definition + // for onClick handlers don't work. Hence leaving the event type as any. + const isCanvasWrapperClicked = e.target?.nodeName === "CANVAS"; + // Making sure that we don't deselect the widget + // after we are done dragging the limits in auto height with limits + if ( + allowDragToSelect && + !isAutoHeightWithLimitsChanging && + !isCanvasWrapperClicked + ) { + focusWidget && focusWidget(); + deselectAll && deselectAll(); + dispatch(closePropertyPane()); + dispatch(closeTableFilterPane()); + dispatch(setCanvasSelectionFromEditor(false)); + } + }, + [ + allowDragToSelect, + focusWidget, + deselectAll, + isAutoHeightWithLimitsChanging, + ], + ); /** * drag event handler for selection drawing @@ -210,6 +224,7 @@ function WidgetsEditor() { } isPreviewMode={isPreviewMode} isPublished={isPublished} + ref={ref} sidebarWidth={isPreviewingNavigation ? sidebarWidth : 0} > {shouldShowSnapShotBanner && ( @@ -224,6 +239,7 @@ function WidgetsEditor() { } isPreviewMode={isPreviewMode} navigationHeight={navigationHeight} + parentRef={ref} shouldShowSnapShotBanner={shouldShowSnapShotBanner} /> diff --git a/app/client/yarn.lock b/app/client/yarn.lock index dcb96e034d..48fdb8db7c 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -4033,6 +4033,25 @@ __metadata: languageName: node linkType: hard +"@mapbox/node-pre-gyp@npm:^1.0.0": + version: 1.0.11 + resolution: "@mapbox/node-pre-gyp@npm:1.0.11" + dependencies: + detect-libc: ^2.0.0 + https-proxy-agent: ^5.0.0 + make-dir: ^3.1.0 + node-fetch: ^2.6.7 + nopt: ^5.0.0 + npmlog: ^5.0.1 + rimraf: ^3.0.2 + semver: ^7.3.5 + tar: ^6.1.11 + bin: + node-pre-gyp: bin/node-pre-gyp + checksum: b848f6abc531a11961d780db813cc510ca5a5b6bf3184d72134089c6875a91c44d571ba6c1879470020803f7803609e7b2e6e429651c026fe202facd11d444b8 + languageName: node + linkType: hard + "@mdx-js/react@npm:^2.1.5": version: 2.3.0 resolution: "@mdx-js/react@npm:2.3.0" @@ -10461,6 +10480,7 @@ __metadata: axios: ^0.27.2 babel-plugin-lodash: ^3.3.4 babel-plugin-module-resolver: ^4.1.0 + canvas: ^2.11.2 chalk: ^4.1.1 classnames: ^2.3.1 clsx: ^1.2.1 @@ -10684,6 +10704,16 @@ __metadata: languageName: node linkType: hard +"are-we-there-yet@npm:^2.0.0": + version: 2.0.0 + resolution: "are-we-there-yet@npm:2.0.0" + dependencies: + delegates: ^1.0.0 + readable-stream: ^3.6.0 + checksum: 6c80b4fd04ecee6ba6e737e0b72a4b41bdc64b7d279edfc998678567ff583c8df27e27523bc789f2c99be603ffa9eaa612803da1d886962d2086e7ff6fa90c7c + languageName: node + linkType: hard + "are-we-there-yet@npm:^3.0.0": version: 3.0.1 resolution: "are-we-there-yet@npm:3.0.1" @@ -12358,6 +12388,18 @@ __metadata: languageName: node linkType: hard +"canvas@npm:^2.11.2": + version: 2.11.2 + resolution: "canvas@npm:2.11.2" + dependencies: + "@mapbox/node-pre-gyp": ^1.0.0 + nan: ^2.17.0 + node-gyp: latest + simple-get: ^3.0.3 + checksum: 61e554aef80022841dc836964534082ec21435928498032562089dfb7736215f039c7d99ee546b0cf10780232d9bf310950f8b4d489dc394e0fb6f6adfc97994 + languageName: node + linkType: hard + "capital-case@npm:^1.0.4": version: 1.0.4 resolution: "capital-case@npm:1.0.4" @@ -12916,7 +12958,7 @@ __metadata: languageName: node linkType: hard -"color-support@npm:^1.1.3": +"color-support@npm:^1.1.2, color-support@npm:^1.1.3": version: 1.1.3 resolution: "color-support@npm:1.1.3" bin: @@ -14287,6 +14329,15 @@ __metadata: languageName: node linkType: hard +"decompress-response@npm:^4.2.0": + version: 4.2.1 + resolution: "decompress-response@npm:4.2.1" + dependencies: + mimic-response: ^2.0.0 + checksum: 4e783ca4dfe9417354d61349750fe05236f565a4415a6ca20983a311be2371debaedd9104c0b0e7b36e5f167aeaae04f84f1a0b3f8be4162f1d7d15598b8fdba + languageName: node + linkType: hard + "decompress-response@npm:^6.0.0": version: 6.0.0 resolution: "decompress-response@npm:6.0.0" @@ -14636,6 +14687,13 @@ __metadata: languageName: node linkType: hard +"detect-libc@npm:^2.0.0": + version: 2.0.2 + resolution: "detect-libc@npm:2.0.2" + checksum: 2b2cd3649b83d576f4be7cc37eb3b1815c79969c8b1a03a40a4d55d83bc74d010753485753448eacb98784abf22f7dbd3911fd3b60e29fda28fed2d1a997944d + languageName: node + linkType: hard + "detect-newline@npm:^3.0.0": version: 3.1.0 resolution: "detect-newline@npm:3.1.0" @@ -17410,6 +17468,23 @@ __metadata: languageName: node linkType: hard +"gauge@npm:^3.0.0": + version: 3.0.2 + resolution: "gauge@npm:3.0.2" + dependencies: + aproba: ^1.0.3 || ^2.0.0 + color-support: ^1.1.2 + console-control-strings: ^1.0.0 + has-unicode: ^2.0.1 + object-assign: ^4.1.1 + signal-exit: ^3.0.0 + string-width: ^4.2.3 + strip-ansi: ^6.0.1 + wide-align: ^1.1.2 + checksum: 81296c00c7410cdd48f997800155fbead4f32e4f82109be0719c63edc8560e6579946cc8abd04205297640691ec26d21b578837fd13a4e96288ab4b40b1dc3e9 + languageName: node + linkType: hard + "gauge@npm:^4.0.3": version: 4.0.4 resolution: "gauge@npm:4.0.4" @@ -22104,6 +22179,13 @@ __metadata: languageName: node linkType: hard +"mimic-response@npm:^2.0.0": + version: 2.1.0 + resolution: "mimic-response@npm:2.1.0" + checksum: 014fad6ab936657e5f2f48bd87af62a8e928ebe84472aaf9e14fec4fcb31257a5edff77324d8ac13ddc6685ba5135cf16e381efac324e5f174fb4ddbf902bf07 + languageName: node + linkType: hard + "mimic-response@npm:^3.1.0": version: 3.1.0 resolution: "mimic-response@npm:3.1.0" @@ -22516,6 +22598,15 @@ __metadata: languageName: node linkType: hard +"nan@npm:^2.17.0": + version: 2.18.0 + resolution: "nan@npm:2.18.0" + dependencies: + node-gyp: latest + checksum: 4fe42f58456504eab3105c04a5cffb72066b5f22bd45decf33523cb17e7d6abc33cca2a19829407b9000539c5cb25f410312d4dc5b30220167a3594896ea6a0a + languageName: node + linkType: hard + "nanoid@npm:^2.0.4": version: 2.1.11 resolution: "nanoid@npm:2.1.11" @@ -22801,6 +22892,18 @@ __metadata: languageName: node linkType: hard +"npmlog@npm:^5.0.1": + version: 5.0.1 + resolution: "npmlog@npm:5.0.1" + dependencies: + are-we-there-yet: ^2.0.0 + console-control-strings: ^1.1.0 + gauge: ^3.0.0 + set-blocking: ^2.0.0 + checksum: 516b2663028761f062d13e8beb3f00069c5664925871a9b57989642ebe09f23ab02145bf3ab88da7866c4e112cafff72401f61a672c7c8a20edc585a7016ef5f + languageName: node + linkType: hard + "npmlog@npm:^6.0.0": version: 6.0.2 resolution: "npmlog@npm:6.0.2" @@ -28463,6 +28566,17 @@ __metadata: languageName: node linkType: hard +"simple-get@npm:^3.0.3": + version: 3.1.1 + resolution: "simple-get@npm:3.1.1" + dependencies: + decompress-response: ^4.2.0 + once: ^1.3.1 + simple-concat: ^1.0.0 + checksum: 80195e70bf171486e75c31e28e5485468195cc42f85940f8b45c4a68472160144d223eb4d07bc82ef80cb974b7c401db021a540deb2d34ac4b3b8883da2d6401 + languageName: node + linkType: hard + "simple-update-notifier@npm:^2.0.0": version: 2.0.0 resolution: "simple-update-notifier@npm:2.0.0" @@ -31837,7 +31951,7 @@ __metadata: languageName: node linkType: hard -"wide-align@npm:^1.1.0, wide-align@npm:^1.1.5": +"wide-align@npm:^1.1.0, wide-align@npm:^1.1.2, wide-align@npm:^1.1.5": version: 1.1.5 resolution: "wide-align@npm:1.1.5" dependencies: