fix: Restructure WidgetNameCanvas (#28202)

#### PR fixes following issue(s)
Fixes #28201

#### Type of change
- New feature (non-breaking change which adds functionality) - doesn't
effect user perception
#### How Has This Been Tested?
- [ ] Manual
- [ ] JUnit
- [ ] Jest
- [ ] Cypress
#### Test Plan

#### Issues raised during DP testing

## Checklist:
#### Dev activity
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] PR is being merged under a feature flag


#### QA activity:
- [ ] [Speedbreak
features](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#speedbreakers-)
have been covered
- [ ] Test plan covers all impacted features and [areas of
interest](https://github.com/appsmithorg/TestSmith/wiki/Guidelines-for-test-plans#areas-of-interest-)
- [ ] Test plan has been peer reviewed by project stakeholders and other
QA members
- [ ] Manually tested functionality on DP
- [ ] We had an implementation alignment call with stakeholders post QA
Round 2
- [ ] Cypress test cases have been added and approved by SDET/manual QA
- [ ] Added `Test Plan Approved` label after Cypress tests were reviewed
- [ ] Added `Test Plan Approved` label after JUnit tests were reviewed
This commit is contained in:
Abhinav Jha 2023-10-20 11:45:16 +05:30 committed by GitHub
parent af9e89d2a1
commit 49c222c4a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 676 additions and 299 deletions

View File

@ -17,7 +17,7 @@ module.exports = {
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node", "css"],
moduleDirectories: ["node_modules", "src", "test"],
transformIgnorePatterns: [
"<rootDir>/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)",
"<rootDir>/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)$": "<rootDir>/test/__mocks__/styleMock.js",

View File

@ -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",

View File

@ -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<string, any> | undefined;
startPoints?: any;
draggedOn?: string | undefined;
}) => void;

View File

@ -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<number>,
hasScroll: MutableRefObject<boolean>,
resetCanvas: () => void,
scrollTop: MutableRefObject<number>,
) {
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<number>,
hasScroll: MutableRefObject<boolean>,
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<CanvasPositions>,
widgetNamePositions: MutableRefObject<WidgetNamePositionType>,
) {
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<HTMLDivElement | null>,
canvasPositions: MutableRefObject<CanvasPositions>,
widgetNamePositions: MutableRefObject<WidgetNamePositionType>,
) {
/**
* 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<CanvasPositions>,
widgetNamePositions: MutableRefObject<WidgetNamePositionType>,
): DragEventHandler {
return (e: DragEvent<HTMLDivElement>) => {
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,
});
};
}

View File

@ -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<HTMLDivElement>;
parentRef: React.RefObject<HTMLDivElement>;
containerRef: React.RefObject<HTMLDivElement | null>;
}) => {
//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<HTMLDivElement>(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<HTMLDivElement>(null);
// used to keep track of positions of widgetName drawn on canvas to make it intractable
const widgetNamePositions = useRef<WidgetNamePositionType>({
selected: undefined,
focused: undefined,
});
//Positions of canvas
const canvasPositions = useRef<CanvasPositions>({
top: 0,
@ -77,19 +78,28 @@ const OverlayCanvasContainer = (props: {
});
const scrollTop = useRef<number>(0);
const isScrolling = useRef(0);
const isScrolling = useRef<number>(0);
const hasScroll = useRef<boolean>(false);
const stageRef = useRef<CanvasStageType>(null);
const stageRef = useRef<CanvasStageType | null>(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 (
<div
@ -373,7 +203,7 @@ const OverlayCanvasContainer = (props: {
canvasPositions?.current.height || DEFAULT_WIDGET_NAME_CANVAS_HEIGHT
}
ref={stageRef}
width={props.canvasWidth + WIDGET_NAME_CANVAS_PADDING}
width={canvasPositions?.current.width || 0}
>
<Layer />
</Stage>

View File

@ -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

View File

@ -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<WidgetNamePositionType>,
stageRef: MutableRefObject<CanvasStageType | null>,
) {
// 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<CanvasStageType | null>;
selectedWidgetNameData: WidgetNameData[] | undefined;
focusedWidgetNameData: WidgetNameData | undefined;
selectWidget: (
type: SelectionRequestType,
payload?: string[] | undefined,
) => void;
scrollTop: MutableRefObject<number>;
widgetNamePositions: MutableRefObject<WidgetNamePositionType>;
canvasPositions: MutableRefObject<CanvasPositions>;
}) => {
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<number>,
stageRef: MutableRefObject<CanvasStageType | null>,
widgetNamePositions: MutableRefObject<WidgetNamePositionType>,
canvasPositions: MutableRefObject<CanvasPositions>,
) => {
// 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<Group, MouseEvent> = (
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);
}
};

View File

@ -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;
},
);

View File

@ -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, boolean> = {
[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, boolean> = {
[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, boolean> = {
[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,
};
/**

View File

@ -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<HTMLDivElement | null>;
}
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) {
</div>
)}
{node}
{enableOverlayCanvas && (
<OverlayCanvasContainer
canvasWidth={canvasWidth}
containerRef={props.parentRef}
/>
)}
</Wrapper>
<MainContainerResizer
currentPageId={currentPageId}

View File

@ -83,6 +83,8 @@ function WidgetsEditor() {
const shouldShowSnapShotBanner =
!!readableSnapShotDetails && !isPreviewingNavigation;
const ref = useRef<HTMLDivElement>(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}
/>
</PageViewWrapper>

View File

@ -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: