import type { AppState } from "@appsmith/reducers"; import { GridDefaults, MAIN_CONTAINER_WIDGET_ID, } from "constants/WidgetConstants"; import equal from "fast-deep-equal/es6"; import type { Context, PropsWithChildren } from "react"; import React, { createContext, useCallback, useEffect, useMemo, useRef, } from "react"; import { useSelector } from "react-redux"; import styled from "styled-components"; import { getCanvasSnapRows } from "utils/WidgetPropsUtils"; import { calculateDropTargetRows } from "./DropTargetUtils"; import DragLayerComponent from "./DragLayerComponent"; import { useDispatch } from "react-redux"; import { useShowPropertyPane } from "utils/hooks/dragResizeHooks"; import { getOccupiedSpacesSelectorForContainer, isAutoLayoutEnabled, previewModeSelector, } from "selectors/editorSelectors"; import { useWidgetSelection } from "utils/hooks/useWidgetSelection"; import { getDragDetails } from "sagas/selectors"; import { useAutoHeightUIState } from "utils/hooks/autoHeightUIHooks"; import { checkContainersForAutoHeightAction, updateDOMDirectlyBasedOnAutoHeightAction, } from "actions/autoHeightActions"; import { isAutoHeightEnabledForWidget, isAutoHeightEnabledForWidgetWithLimits, } from "widgets/WidgetUtils"; import { getIsAppSettingsPaneWithNavigationTabOpen } from "selectors/appSettingsPaneSelectors"; type DropTargetComponentProps = PropsWithChildren<{ snapColumnSpace: number; widgetId: string; parentId?: string; noPad?: boolean; bottomRow: number; minHeight: number; useAutoLayout?: boolean; isMobile?: boolean; mobileBottomRow?: number; }>; const StyledDropTarget = styled.div` transition: height 100ms ease-in; width: 100%; position: relative; background: none; user-select: none; z-index: 1; `; function Onboarding() { return (

Drag and drop a widget here

); } /* This context will provide the function which will help the draglayer and resizablecomponents trigger an update of the main container's rows */ export const DropTargetContext: Context<{ updateDropTargetRows?: ( widgetIdsToExclude: string[], widgetBottomRow: number, ) => number | false; }> = createContext({}); /** * This function sets the height in pixels to the provided ref to the number of rows * @param ref : The ref to the dropTarget so that we can update the height * @param currentRows : Number of rows to set the height */ const updateHeight = ( ref: React.MutableRefObject, currentRows: number, ) => { if (ref.current) { const height = currentRows * GridDefaults.DEFAULT_GRID_ROW_HEIGHT; ref.current.style.height = `${height}px`; ref.current .closest(".scroll-parent") ?.scrollTo({ top: height, behavior: "smooth" }); } }; function useUpdateRows( bottomRow: number, widgetId: string, parentId?: string, mobileBottomRow?: number, isMobile?: boolean, isAutoLayoutActive?: boolean, ) { // This gives us the number of rows const snapRows = getCanvasSnapRows( bottomRow, mobileBottomRow, isMobile, isAutoLayoutActive, ); // Put the existing snap rows in a ref. const rowRef = useRef(snapRows); const dropTargetRef = useRef(null); // The occupied spaces in this canvas. It is a data structure which has the rect values of each child. const selectOccupiedSpaces = useCallback( getOccupiedSpacesSelectorForContainer(widgetId), [widgetId], ); // Call the selector above. const occupiedSpacesByChildren = useSelector(selectOccupiedSpaces, equal); /* * If the parent has auto height enabled, or if the current widget is the MAIN_CONTAINER_WIDGET_ID */ const isParentAutoHeightEnabled = useSelector((state: AppState) => { return parentId ? !isAutoHeightEnabledForWidgetWithLimits( state.entities.canvasWidgets[parentId], ) && isAutoHeightEnabledForWidget(state.entities.canvasWidgets[parentId]) : false; }); const dispatch = useDispatch(); // Function which computes and updates the height of the dropTarget // This is used in a context and hence in one of the children of this dropTarget const updateDropTargetRows = ( widgetIdsToExclude: string[], widgetBottomRow: number, ) => { // Compute expected number of rows this drop target must have const newRows = calculateDropTargetRows( widgetIdsToExclude, widgetBottomRow, occupiedSpacesByChildren, widgetId, ); // If the current number of rows in the drop target is less // than the expected number of rows in the drop target if (rowRef.current < newRows) { // Set the new value locally rowRef.current = newRows; // If the parent container like widget has auto height enabled // We'd like to immediately update the parent's height // based on the auto height computations // This also updates any "dropTargets" that need to change height // hence, this and the `updateHeight` function are mutually exclusive. if (isParentAutoHeightEnabled && parentId) { dispatch(updateDOMDirectlyBasedOnAutoHeightAction(parentId, newRows)); } else { // Basically, we don't have auto height triggering, so the dropTarget height should be updated using // the `updateHeight` function // The difference here is that the `updateHeight` function only updates the "canvas" or the "dropTarget" // and doesn't effect the parent container // We can't update the height of the "Canvas" or "dropTarget" using this function // in the previous if clause, because, there could be more "dropTargets" updating // and this information can only be computed using auto height updateHeight(dropTargetRef, rowRef.current); } return newRows; } return false; }; // memoizing context values const contextValue = useMemo(() => { return { updateDropTargetRows, }; }, [updateDropTargetRows, occupiedSpacesByChildren]); /** EO PREPARE CONTEXT */ return { contextValue, dropTargetRef, rowRef }; } export function DropTargetComponent(props: DropTargetComponentProps) { // Get if this is in preview mode. const isPreviewMode = useSelector(previewModeSelector); const isAppSettingsPaneWithNavigationTabOpen = useSelector( getIsAppSettingsPaneWithNavigationTabOpen, ); const isAutoLayoutActive = useSelector(isAutoLayoutEnabled); const { contextValue, dropTargetRef, rowRef } = useUpdateRows( props.bottomRow, props.widgetId, props.parentId, props.mobileBottomRow, props.isMobile, isAutoLayoutActive, ); // Are we currently resizing? const isResizing = useSelector( (state: AppState) => state.ui.widgetDragResize.isResizing, ); // Are we currently dragging? const isDragging = useSelector( (state: AppState) => state.ui.widgetDragResize.isDragging, ); // Are we changing the auto height limits by dragging the signifiers? const { isAutoHeightWithLimitsChanging } = useAutoHeightUIState(); const dispatch = useDispatch(); // dragDetails contains of info needed for a container jump: // which parent the dragging widget belongs, // which canvas is active(being dragged on), // which widget is grabbed while dragging started, // relative position of mouse pointer wrt to the last grabbed widget. const dragDetails = useSelector(getDragDetails); const { draggedOn } = dragDetails; // All the widgets in this canvas const childWidgets: string[] | undefined = useSelector( (state: AppState) => state.entities.canvasWidgets[props.widgetId]?.children, ); // This shows the property pane const showPropertyPane = useShowPropertyPane(); const { deselectAll, focusWidget } = useWidgetSelection(); // Everytime we get a new bottomRow, or we toggle shouldScrollContents // we call this effect useEffect(() => { const snapRows = getCanvasSnapRows( props.bottomRow, props.mobileBottomRow, props.isMobile, isAutoLayoutActive, ); // If the current ref is not set to the new snaprows we've received (based on bottomRow) if (rowRef.current !== snapRows && !isDragging && !isResizing) { rowRef.current = snapRows; updateHeight(dropTargetRef, snapRows); // If we're done dragging, and the parent has auto height enabled // It is possible that the auto height has not triggered yet // because the user has released the mouse button but not placed the widget // In these scenarios, the parent's height needs to be updated // in the same way as the auto height would have done if (props.parentId) { dispatch(checkContainersForAutoHeightAction()); } } }, [ props.widgetId, props.bottomRow, props.mobileBottomRow, props.isMobile, props.parentId, isDragging, isResizing, ]); const handleFocus = (e: any) => { // Making sure that we don't deselect the widget // after we are done dragging the limits in auto height with limits if (!isResizing && !isDragging && !isAutoHeightWithLimitsChanging) { if (!props.parentId) { deselectAll(); focusWidget && focusWidget(props.widgetId); showPropertyPane && showPropertyPane(); } } e.preventDefault(); }; // Get the height for the drop target const height = `${rowRef.current * GridDefaults.DEFAULT_GRID_ROW_HEIGHT}px`; const dropTargetStyles = { height, }; const shouldOnboard = !(childWidgets && childWidgets.length) && !isDragging && !props.parentId; // The drag layer is the one with the grid dots. // They need to show in certain scenarios const showDragLayer = ((isDragging && draggedOn === props.widgetId) || isResizing || isAutoHeightWithLimitsChanging) && !isPreviewMode && !isAppSettingsPaneWithNavigationTabOpen && !props.useAutoLayout; return ( {props.children} {shouldOnboard && } {showDragLayer && ( )} ); } export default DropTargetComponent;