From 85df996db6df4964ee651855e1bc9a354ec2aff0 Mon Sep 17 00:00:00 2001 From: Balint Gabor <127662+gbalint@users.noreply.github.com> Date: Mon, 18 Nov 2024 10:53:50 +0100 Subject: [PATCH] Grid component support (#6640) **Problem:** Followup to https://github.com/concrete-utopia/utopia/pull/6636 The goal is to support interaction with grid item in the following scenario: - the grid is in a component - the grid renders the component's children - the grid is not the root element of the component **Fix:** In https://github.com/concrete-utopia/utopia/pull/6636 I introduced a way to indentify a grid using the element path of its item (which is necessary because when the grid is in a non-focused component). - `GridMeasurementHelper` collects the grids from the metadata. These grids in non-focused components are not in the metadata, so they are not collected. I fixed this by collecting component items from the grids too. - To get the measurement data for these grids which identified by their items, I added a new parameter to `getGridMeasurementHelperData` so it can get the styling of the parentElement of the item's element. - The measurement helper was identified by its element path, and the dom-sampler could retrieve the helper from the dom using that. However, this is again not a good solution for these grids in non-focused components, so I added global WeakMap which stores the pairing between HTMLElements of grids and the id attribute of their GridMeasurementHelper. - I updated the `path` fiels names of `GridContainerIdentifier` and `GridItemIdentifier`: it was not a good idea to use the same name in the two paths, because it was possible to not check the type and use the path field. **Missing:** `gridMoveStrategiesExtraCommands` still tries to set grid props on the parent, and when it is a component, those can be absolutely useless. We should detect when that is the case, and not generate these commands. **Manual Tests:** I hereby swear that: - [x] I opened a hydrogen project and it loaded - [x] I could navigate to various routes in Play mode --- ...change-location-strategy.spec.browser2.tsx | 89 ++++----- .../strategies/grid-helpers.ts | 38 ++++ .../grid-reorder-strategy.spec.browser2.tsx | 120 ++++++++++++ ...-resize-element-strategy.spec.browser2.tsx | 24 ++- .../controls/grid-controls-for-strategies.tsx | 95 +++++++--- .../canvas/controls/grid-controls.tsx | 172 +++++++++++------- .../select-mode/subdued-grid-gap-controls.tsx | 6 +- .../components/canvas/direct-dom-lookups.ts | 22 ++- editor/src/components/canvas/dom-walker.ts | 24 +-- .../components/editor/store/editor-state.ts | 16 +- .../store/store-deep-equality-instances.ts | 4 +- .../src/core/model/element-metadata-utils.ts | 5 + 12 files changed, 429 insertions(+), 186 deletions(-) diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-element-change-location-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/grid-element-change-location-strategy.spec.browser2.tsx index cca92c289c76..293e304d1a18 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-element-change-location-strategy.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-element-change-location-strategy.spec.browser2.tsx @@ -97,26 +97,28 @@ describe('grid element change location strategy', () => { }) }) - it('can change the location of elements in a grid component', async () => { - const editor = await renderTestEditorWithCode( - ProjectCodeGridComponent, - 'await-first-dom-report', - ) + describe('grid component', () => { + it('can change the location of elements in a grid component', async () => { + const editor = await renderTestEditorWithCode( + ProjectCodeGridComponent, + 'await-first-dom-report', + ) - const testId = 'aaa' - const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } = await runGridMoveTest( - editor, - { - scale: 1, - pathString: `sb/scene/grid/${testId}`, - testId: testId, - }, - ) - expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({ - gridColumnEnd: '7', - gridColumnStart: '3', - gridRowEnd: '4', - gridRowStart: '2', + const testId = 'aaa' + const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd } = await runGridMoveTest( + editor, + { + scale: 1, + pathString: `sb/scene/grid/${testId}`, + testId: testId, + }, + ) + expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({ + gridColumnEnd: '7', + gridColumnStart: '3', + gridRowEnd: '4', + gridRowStart: '2', + }) }) }) @@ -1013,13 +1015,6 @@ export var storyboard = ( >
- {props.children} +
+ {props.children} +
+
) } diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts b/editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts index 1bf896aac5d7..a8e2b35a4b1f 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-helpers.ts @@ -45,6 +45,7 @@ import { getGridChildCellCoordBoundsFromCanvas, gridCellCoordinates, } from './grid-cell-bounds' +import type { GridIdentifier } from '../../../editor/store/editor-state' export function gridPositionToValue( p: GridPositionOrSpan | null | undefined, @@ -684,3 +685,40 @@ export function gridMoveStrategiesExtraCommands( return { midInteractionCommands, onCompleteCommands } } + +export function getGridIdentifierContainerOrComponentPath(identifier: GridIdentifier): ElementPath { + switch (identifier.type) { + case 'GRID_CONTAINER': + return identifier.container + case 'GRID_ITEM': + return EP.parentPath(identifier.item) + default: + assertNever(identifier) + } +} + +export function gridIdentifiersSimilar(a: GridIdentifier, b: GridIdentifier): boolean { + switch (a.type) { + case 'GRID_CONTAINER': + return b.type === 'GRID_CONTAINER' + ? EP.pathsEqual(a.container, b.container) + : EP.isParentOf(a.container, b.item) + case 'GRID_ITEM': + return b.type === 'GRID_ITEM' + ? EP.pathsEqual(a.item, b.item) + : EP.isParentOf(b.container, a.item) + default: + assertNever(a) + } +} + +export function gridIdentifierToString(identifier: GridIdentifier): string { + switch (identifier.type) { + case 'GRID_CONTAINER': + return `${identifier.type}-${EP.toString(identifier.container)}` + case 'GRID_ITEM': + return `${identifier.type}-${EP.toString(identifier.item)}` + default: + assertNever(identifier) + } +} diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-reorder-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/grid-reorder-strategy.spec.browser2.tsx index 9fffc31d25bd..8361c3668036 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-reorder-strategy.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-reorder-strategy.spec.browser2.tsx @@ -22,6 +22,23 @@ describe('grid reorder', () => { expect(cells).toEqual(['pink', 'cyan', 'orange', 'blue']) }) + it('reorders an element in a grid component without setting positioning (inside contiguous area)', async () => { + const editor = await renderTestEditorWithCode( + ProjectCodeReorderGridComponent, + 'await-first-dom-report', + ) + const { gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd, cells } = + await runReorderTest(editor, 'sb/scene/grid', 'orange', { row: 1, column: 3 }) + + expect({ gridRowStart, gridRowEnd, gridColumnStart, gridColumnEnd }).toEqual({ + gridColumnEnd: '', + gridColumnStart: '', + gridRowEnd: '', + gridRowStart: '', + }) + + expect(cells).toEqual(['pink', 'cyan', 'orange', 'blue']) + }) it('reorders a component (which does not take style props) inside contiguous area', async () => { const editor = await renderTestEditorWithCode( ProjectCodeReorderWithComponentItem, @@ -339,6 +356,109 @@ export var storyboard = ( ) ` +const ProjectCodeReorderGridComponent = `import * as React from 'react' +import { Scene, Storyboard } from 'utopia-api' + +export var storyboard = ( + + + +
+
+
+
+ + + +) + +export function Grid(props) { + return ( +
+
+ {props.children} +
+
+
+ ) +} +` + const ProjectCodeReorderWithComponentItem = `import * as React from 'react' import { Scene, Storyboard } from 'utopia-api' diff --git a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.spec.browser2.tsx b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.spec.browser2.tsx index 44de26cbb02c..d4f8319e350a 100644 --- a/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.spec.browser2.tsx +++ b/editor/src/components/canvas/canvas-strategies/strategies/grid-resize-element-strategy.spec.browser2.tsx @@ -867,18 +867,26 @@ export var storyboard = ( export function Grid(props) { return (
- {props.children} +
+ {props.children} +
+
) } diff --git a/editor/src/components/canvas/controls/grid-controls-for-strategies.tsx b/editor/src/components/canvas/controls/grid-controls-for-strategies.tsx index fc98962db0b2..6c04372ff5fb 100644 --- a/editor/src/components/canvas/controls/grid-controls-for-strategies.tsx +++ b/editor/src/components/canvas/controls/grid-controls-for-strategies.tsx @@ -43,9 +43,15 @@ import Utils from '../../../utils/utils' import { useMonitorChangesToEditor } from '../../../components/editor/store/store-monitor' import { useKeepReferenceEqualityIfPossible } from '../../../utils/react-performance' import type { CSSProperties } from 'react' -import { gridContainerIdentifier, type GridIdentifier } from '../../editor/store/editor-state' +import { + gridContainerIdentifier, + gridItemIdentifier, + type GridIdentifier, +} from '../../editor/store/editor-state' import { findOriginalGrid } from '../canvas-strategies/strategies/grid-helpers' +export const GridMeasurementHelperMap = { current: new WeakMap() } + export const GridCellTestId = (elementPath: ElementPath) => `grid-cell-${EP.toString(elementPath)}` function getCellsCount(template: GridAutoOrTemplateBase | null): number { @@ -90,13 +96,17 @@ export type GridMeasurementHelperData = { rowGap: number | null columnGap: number | null padding: Sides + element: HTMLElement } +export type ElementOrParent = 'parent' | 'element' + export function getGridMeasurementHelperData( elementPath: ElementPath, scale: number, + source: ElementOrParent, ): GridMeasurementHelperData | undefined { - return getFromElement(elementPath, gridMeasurementHelperDataFromElement(scale)) + return getFromElement(elementPath, gridMeasurementHelperDataFromElement(scale), source) } function getStylingSubset(styling: CSSStyleDeclaration): CSSProperties { @@ -207,12 +217,14 @@ export function gridMeasurementHelperDataFromElement( columns: columns, cells: columns * rows, computedStyling: computedStyling, + element: element, } } } export function useGridMeasurementHelperData( elementPath: ElementPath, + source: ElementOrParent, ): GridMeasurementHelperData | undefined { const scale = useEditorState( Substores.canvas, @@ -222,7 +234,9 @@ export function useGridMeasurementHelperData( useMonitorChangesToEditor() - return useKeepReferenceEqualityIfPossible(getGridMeasurementHelperData(elementPath, scale)) + return useKeepReferenceEqualityIfPossible( + getGridMeasurementHelperData(elementPath, scale, source), + ) } export type GridData = GridMeasurementHelperData & { @@ -240,33 +254,56 @@ export function useGridData(gridIdentifiers: GridIdentifier[]): GridData[] { Substores.metadata, (store) => { return mapDropNulls((view) => { - const originalGridPath = findOriginalGrid( - store.editor.jsxMetadata, - view.type === 'GRID_ITEM' ? EP.parentPath(view.path) : view.path, // TODO: this is temporary, we will need to handle showing a grid control on the parent dom element of a grid item - ) - if (originalGridPath == null) { - return null - } - const element = MetadataUtils.findElementByElementPath( - store.editor.jsxMetadata, - originalGridPath, - ) - - const targetGridContainer = MetadataUtils.isGridLayoutedContainer(element) ? element : null - if (targetGridContainer == null) { - return null - } - - const helperData = getGridMeasurementHelperData(originalGridPath, scale) - if (helperData == null) { - return null - } - - const gridData: GridData = { - ...helperData, - identifier: gridContainerIdentifier(originalGridPath), + switch (view.type) { + case 'GRID_CONTAINER': { + const originalGridPath = findOriginalGrid(store.editor.jsxMetadata, view.container) + if (originalGridPath == null) { + return null + } + const element = MetadataUtils.findElementByElementPath( + store.editor.jsxMetadata, + originalGridPath, + ) + + const targetGridContainer = MetadataUtils.isGridLayoutedContainer(element) + ? element + : null + if (targetGridContainer == null) { + return null + } + + const helperData = getGridMeasurementHelperData(originalGridPath, scale, 'element') + if (helperData == null) { + return null + } + + const gridData: GridData = { + ...helperData, + identifier: gridContainerIdentifier(originalGridPath), + } + return gridData + } + case 'GRID_ITEM': + const item = MetadataUtils.isGridItem(store.editor.jsxMetadata, view.item) + ? MetadataUtils.findElementByElementPath(store.editor.jsxMetadata, view.item) + : null + if (item == null) { + return null + } + + const helperData = getGridMeasurementHelperData(view.item, scale, 'parent') + if (helperData == null) { + return null + } + + const gridData: GridData = { + ...helperData, + identifier: gridItemIdentifier(view.item), + } + return gridData + default: + assertNever(view) } - return gridData }, gridIdentifiers) }, 'useGridData', diff --git a/editor/src/components/canvas/controls/grid-controls.tsx b/editor/src/components/canvas/controls/grid-controls.tsx index b47891511c1e..2940f9cf3b9e 100644 --- a/editor/src/components/canvas/controls/grid-controls.tsx +++ b/editor/src/components/canvas/controls/grid-controls.tsx @@ -2,6 +2,7 @@ /** @jsx jsx */ /** @jsxFrag React.Fragment */ import { jsx } from '@emotion/react' +import { v4 as UUID } from 'uuid' import type { AnimationControls } from 'framer-motion' import { motion, useAnimationControls } from 'framer-motion' import type { CSSProperties } from 'react' @@ -72,6 +73,9 @@ import { getGridRelatedIndexes, getGridElementPinState, gridPositionToValue, + getGridIdentifierContainerOrComponentPath, + gridIdentifierToString, + gridIdentifiersSimilar, } from '../canvas-strategies/strategies/grid-helpers' import { canResizeGridTemplate } from '../canvas-strategies/strategies/resize-grid-strategy' import { resizeBoundingBoxFromSide } from '../canvas-strategies/strategies/resize-helpers' @@ -93,6 +97,7 @@ import { gridEdgeToEdgePosition, GridElementContainingBlockKey, GridMeasurementHelperKey, + GridMeasurementHelperMap, GridMeasurementHelpersKey, useGridData, useGridMeasurementHelperData, @@ -105,11 +110,7 @@ import type { PinOutlineProps } from './position-outline' import { PinOutline, usePropsOrJSXAttributes } from './position-outline' import { getLayoutProperty } from '../../../core/layout/getLayoutProperty' import { styleStringInArray } from '../../../utils/common-constants' -import { - gridContainerIdentifier, - gridIdentifierSimilar, - type GridIdentifier, -} from '../../editor/store/editor-state' +import { gridContainerIdentifier, type GridIdentifier } from '../../editor/store/editor-state' const CELL_ANIMATION_DURATION = 0.15 // seconds @@ -572,7 +573,7 @@ export const GridRowColumnResizingControlsComponent = ({ {gridsWithVisibleResizeControls.flatMap((grid) => { return ( { return ( (({ grid, controlsVisible }) => ) const cells = React.useMemo(() => { - const children = - grid.identifier.type === 'GRID_CONTAINER' - ? MetadataUtils.getChildrenUnordered(jsxMetadata, grid.identifier.path) - : MetadataUtils.getSiblingsUnordered(jsxMetadata, grid.identifier.path) + const children = (() => { + switch (grid.identifier.type) { + case 'GRID_CONTAINER': + return MetadataUtils.getChildrenUnordered(jsxMetadata, grid.identifier.container) + case 'GRID_ITEM': + return MetadataUtils.getSiblingsUnordered(jsxMetadata, grid.identifier.item) + default: + assertNever(grid.identifier) + } + })() return mapDropNulls((cell, index) => { if (cell == null || cell.globalFrame == null || !isFiniteRectangle(cell.globalFrame)) { return null @@ -900,25 +907,27 @@ const GridControl = React.memo(({ grid, controlsVisible }) => const dontShowActiveCellHighlight = (!targetsAreCellsWithPositioning && anyTargetAbsolute) || (anyTargetNotPinned && !targetRootCellIsValidTarget) + + const gridContainerOrComponentPath = getGridIdentifierContainerOrComponentPath(grid.identifier) return ( {/* grid lines */}
{placeholders.map((cell) => { const countedRow = Math.floor(cell / grid.columns) + 1 const countedColumn = Math.floor(cell % grid.columns) + 1 - const id = gridCellTargetId(grid.identifier.path, countedRow, countedColumn) + const id = gridCellTargetId(gridContainerOrComponentPath, countedRow, countedColumn) const borderID = `${id}-border` const isActiveGrid = - (dragging != null && EP.isParentOf(grid.identifier.path, dragging)) || + (dragging != null && EP.isParentOf(gridContainerOrComponentPath, dragging)) || (currentHoveredGrid != null && - EP.pathsEqual(grid.identifier.path, currentHoveredGrid.path)) + gridIdentifiersSimilar(grid.identifier, currentHoveredGrid)) const isActiveCell = isActiveGrid && countedColumn === currentHoveredCell?.column && @@ -1035,13 +1044,24 @@ export const GridMeasurementHelpers = React.memo(() => { 'GridMeasurementHelpers metadata', ) - const grids = useAllGrids(metadata) + const { grids, gridItems } = useAllGrids(metadata) return ( - {grids.map((grid) => { - return - })} + {grids.map((grid) => ( + + ))} + {gridItems.map((gridItem) => ( + + ))} ) }) @@ -1049,51 +1069,54 @@ GridMeasurementHelpers.displayName = 'GridMeasurementHelpers' export interface GridMeasurementHelperProps { elementPath: ElementPath + source: 'parent' | 'element' } -const GridMeasurementHelper = React.memo<{ elementPath: ElementPath }>(({ elementPath }) => { - const gridData = useGridMeasurementHelperData(elementPath) - if (gridData == null) { - return null - } +export const GridMeasurementHelper = React.memo( + ({ elementPath, source }) => { + const gridData = useGridMeasurementHelperData(elementPath, source) + if (gridData == null) { + return null + } - const placeholders = range(0, gridData.cells) + const placeholders = range(0, gridData.cells) - const style: CSSProperties = { - ...getGridHelperStyleMatchingTargetGrid(gridData), - opacity: 1, - } + const style: CSSProperties = { + ...getGridHelperStyleMatchingTargetGrid(gridData), + opacity: 1, + } - return ( -
- {placeholders.map((cell) => { - const countedRow = Math.floor(cell / gridData.columns) + 1 - const countedColumn = Math.floor(cell % gridData.columns) + 1 - const id = `${GridMeasurementHelperKey(elementPath)}-${countedRow}-${countedColumn}` - return ( -
- ) - })} -
- ) -}) + const uid = UUID() + GridMeasurementHelperMap.current.set(gridData.element, uid) + return ( +
+ {placeholders.map((cell) => { + const countedRow = Math.floor(cell / gridData.columns) + 1 + const countedColumn = Math.floor(cell % gridData.columns) + 1 + const id = `${GridMeasurementHelperKey(elementPath)}-${countedRow}-${countedColumn}` + return ( +
+ ) + })} +
+ ) + }, +) GridMeasurementHelper.displayName = 'GridMeasurementHelper' export const GridControlsComponent = ({ targets }: GridControlsProps) => { const ancestorPaths = React.useMemo(() => { - return targets.flatMap((target) => EP.getAncestors(target.path)) + return targets.flatMap((target) => + EP.getAncestors(getGridIdentifierContainerOrComponentPath(target)), + ) }, [targets]) const ancestorGrids: Array = useEditorState( Substores.metadata, @@ -1129,9 +1152,11 @@ export const GridControlsComponent = ({ targets }: GridControlsProps) => { // With the lowest depth grid at the end so that it renders on top and catches the events // before those above it in the hierarchy. const grids = useGridData( - uniqBy([...gridsWithVisibleControls, ...ancestorGrids], gridIdentifierSimilar).sort((a, b) => { - const aDepth = a.type === 'GRID_CONTAINER' ? EP.fullDepth(a.path) : EP.fullDepth(a.path) - 1 - const bDepth = a.type === 'GRID_CONTAINER' ? EP.fullDepth(b.path) : EP.fullDepth(b.path) - 1 + uniqBy([...gridsWithVisibleControls, ...ancestorGrids], gridIdentifiersSimilar).sort((a, b) => { + const aDepth = + a.type === 'GRID_CONTAINER' ? EP.fullDepth(a.container) : EP.fullDepth(a.item) - 1 + const bDepth = + b.type === 'GRID_CONTAINER' ? EP.fullDepth(b.container) : EP.fullDepth(b.item) - 1 return aDepth - bDepth }), ) @@ -1144,18 +1169,17 @@ export const GridControlsComponent = ({ targets }: GridControlsProps) => {
{grids.map((grid) => { - const gridPath = - grid.identifier.type === 'GRID_CONTAINER' - ? grid.identifier.path - : EP.parentPath(grid.identifier.path) + const gridContainerOrComponentPath = getGridIdentifierContainerOrComponentPath( + grid.identifier, + ) const shouldHaveVisibleControls = gridsWithVisibleControls.some((g) => { - const visibleControlPath = g.type === 'GRID_CONTAINER' ? g.path : EP.parentPath(g.path) - return EP.pathsEqual(gridPath, visibleControlPath) + const visibleControlPath = getGridIdentifierContainerOrComponentPath(g) + return EP.pathsEqual(gridContainerOrComponentPath, visibleControlPath) }) return ( @@ -1601,7 +1625,7 @@ interface GridResizeControlProps { } export const GridResizeControlsComponent = ({ target }: GridResizeControlProps) => { - const gridTarget = target.type === 'GRID_CONTAINER' ? target.path : EP.parentPath(target.path) + const gridTarget = getGridIdentifierContainerOrComponentPath(target) const colorTheme = useColorTheme() const element = useEditorState( @@ -1904,7 +1928,7 @@ export interface GridElementContainingBlockProps { } const GridElementContainingBlock = React.memo((props) => { - const gridData = useGridMeasurementHelperData(props.gridPath) + const gridData = useGridMeasurementHelperData(props.gridPath, 'element') const scale = useEditorState( Substores.canvas, (store) => store.editor.canvas.scale, @@ -2075,6 +2099,14 @@ function gridPlaceholderWidthOrHeight(scale: number): string { function useAllGrids(metadata: ElementInstanceMetadataMap) { return React.useMemo(() => { - return MetadataUtils.getAllGrids(metadata) + const gridPaths = MetadataUtils.getAllGrids(metadata) + const gridItemPaths = MetadataUtils.getAllGridItems(metadata) + const gridItemPathsWithoutGridPaths = gridItemPaths.filter( + (path) => !gridPaths.some((gridPath) => EP.isParentOf(gridPath, path)), + ) + return { + grids: gridPaths, + gridItems: gridItemPathsWithoutGridPaths, + } }, [metadata]) } diff --git a/editor/src/components/canvas/controls/select-mode/subdued-grid-gap-controls.tsx b/editor/src/components/canvas/controls/select-mode/subdued-grid-gap-controls.tsx index 079cfb32238f..a0d08fe9d7f0 100644 --- a/editor/src/components/canvas/controls/select-mode/subdued-grid-gap-controls.tsx +++ b/editor/src/components/canvas/controls/select-mode/subdued-grid-gap-controls.tsx @@ -9,6 +9,7 @@ import { NO_OP } from '../../../../core/shared/utils' import * as EP from '../../../../core/shared/element-path' import { GridPaddingOutlineForDimension } from './grid-gap-control-component' import { gridContainerIdentifier } from '../../../editor/store/editor-state' +import { getGridIdentifierContainerOrComponentPath } from '../../canvas-strategies/strategies/grid-helpers' export interface SubduedGridGapControlProps { hoveredOrFocused: 'hovered' | 'focused' @@ -33,8 +34,11 @@ export const SubduedGridGapControl = React.memo((pro return ( {gridRowColumnInfo.map((gridData) => { + const gridContainerOrComponent = getGridIdentifierContainerOrComponentPath( + gridData.identifier, + ) return ( - + = (element: HTMLElement) => T -export function getFromElement(path: ElementPath, fromElement: FromElement): T | undefined { +export function getFromElement( + path: ElementPath, + fromElement: FromElement, + elementOrParent: ElementOrParent, +): T | undefined { const pathString = EP.toString(path) const elements = document.querySelectorAll( `#${CanvasContainerID} [${UTOPIA_PATH_KEY}^="${pathString}"]`, @@ -19,8 +25,18 @@ export function getFromElement(path: ElementPath, fromElement: FromElement !EP.isRootElementOfInstance(pathFromElement)) || EP.isRootElementOf(pathFromElement, path) ) { - if (element instanceof HTMLElement) { - return fromElement(element) + const realElement = (() => { + switch (elementOrParent) { + case 'element': + return element + case 'parent': + return element.parentElement + default: + assertNever(elementOrParent) + } + })() + if (realElement instanceof HTMLElement) { + return fromElement(realElement) } } } diff --git a/editor/src/components/canvas/dom-walker.ts b/editor/src/components/canvas/dom-walker.ts index b96f1a660948..ab3f2962f950 100644 --- a/editor/src/components/canvas/dom-walker.ts +++ b/editor/src/components/canvas/dom-walker.ts @@ -62,11 +62,7 @@ import { import type { UtopiaStoreAPI } from '../editor/store/store-hook' import { UTOPIA_SCENE_ID_KEY, UTOPIA_UID_KEY } from '../../core/model/utopia-constants' import { emptySet } from '../../core/shared/set-utils' -import { - getDeepestPathOnDomElement, - getPathsOnDomElement, - getPathStringsOnDomElement, -} from '../../core/shared/uid-utils' +import { getDeepestPathOnDomElement, getPathStringsOnDomElement } from '../../core/shared/uid-utils' import { forceNotNull } from '../../core/shared/optional-utils' import { fastForEach } from '../../core/shared/utils' import type { EditorState, EditorStorePatched } from '../editor/store/editor-state' @@ -83,7 +79,7 @@ import { runDOMWalker } from '../editor/actions/action-creators' import { CanvasContainerOuterId } from './canvas-component-entry' import { ElementsToRerenderGLOBAL } from './ui-jsx-canvas' import type { GridCellGlobalFrames } from './canvas-strategies/strategies/grid-helpers' -import { GridMeasurementHelperKey } from './controls/grid-controls-for-strategies' +import { GridMeasurementHelperMap } from './controls/grid-controls-for-strategies' export const ResizeObserver = window.ResizeObserver ?? ResizeObserverSyntheticDefault.default ?? ResizeObserverSyntheticDefault @@ -1079,18 +1075,12 @@ function measureGlobalFramesOfGridCells( containerRectLazy: CanvasPoint | (() => CanvasPoint), elementCanvasRectangleCache: ElementCanvasRectangleCache, ): GridCellGlobalFrames | null { - const paths = getPathsOnDomElement(element) - let gridCellGlobalFrames: GridCellGlobalFrames | null = null - const gridControlElement = (() => { - for (let p of paths) { - const maybeGridControlElement = document.getElementById(GridMeasurementHelperKey(p)) - if (maybeGridControlElement != null) { - return maybeGridControlElement - } - } - return null - })() + + const gridMeasurementHelperId = GridMeasurementHelperMap.current.get(element) + + const gridControlElement = + gridMeasurementHelperId != null ? document.getElementById(gridMeasurementHelperId) : null if (gridControlElement != null) { gridCellGlobalFrames = [] diff --git a/editor/src/components/editor/store/editor-state.ts b/editor/src/components/editor/store/editor-state.ts index 53e39973849b..91b563ddc7e8 100644 --- a/editor/src/components/editor/store/editor-state.ts +++ b/editor/src/components/editor/store/editor-state.ts @@ -826,36 +826,28 @@ export type GridIdentifier = GridContainerIdentifier | GridItemIdentifier export interface GridContainerIdentifier { type: 'GRID_CONTAINER' - path: ElementPath + container: ElementPath } export function gridContainerIdentifier(path: ElementPath): GridContainerIdentifier { return { type: 'GRID_CONTAINER', - path: path, + container: path, } } export interface GridItemIdentifier { type: 'GRID_ITEM' - path: ElementPath + item: ElementPath } export function gridItemIdentifier(path: ElementPath): GridItemIdentifier { return { type: 'GRID_ITEM', - path: path, + item: path, } } -export function gridIdentifierSimilar(a: GridIdentifier, b: GridIdentifier): boolean { - return ( - (a.type === b.type && EP.pathsEqual(a.path, b.path)) || - (a.type === 'GRID_ITEM' && b.type === 'GRID_CONTAINER' && EP.isParentOf(b.path, a.path)) || - (a.type === 'GRID_CONTAINER' && b.type === 'GRID_ITEM' && EP.isParentOf(a.path, b.path)) - ) -} - export interface GridControlData { grid: GridIdentifier targetCell: GridCellCoordinates | null // the cell under the mouse diff --git a/editor/src/components/editor/store/store-deep-equality-instances.ts b/editor/src/components/editor/store/store-deep-equality-instances.ts index 36f987d95400..0879447458ca 100644 --- a/editor/src/components/editor/store/store-deep-equality-instances.ts +++ b/editor/src/components/editor/store/store-deep-equality-instances.ts @@ -2856,14 +2856,14 @@ export const GridCellCoordinatesKeepDeepEquality: KeepDeepEqualityCall = combine1EqualityCall( - (identifier) => identifier.path, + (identifier) => identifier.container, ElementPathKeepDeepEquality, gridContainerIdentifier, ) export const GridItemIdentifierKeepDeepEquality: KeepDeepEqualityCall = combine1EqualityCall( - (identifier) => identifier.path, + (identifier) => identifier.item, ElementPathKeepDeepEquality, gridItemIdentifier, ) diff --git a/editor/src/core/model/element-metadata-utils.ts b/editor/src/core/model/element-metadata-utils.ts index 062ec6344a04..7c3661bbcc77 100644 --- a/editor/src/core/model/element-metadata-utils.ts +++ b/editor/src/core/model/element-metadata-utils.ts @@ -416,6 +416,11 @@ export const MetadataUtils = { .filter((m) => MetadataUtils.isGridLayoutedContainer(m)) .map((m) => m.elementPath) }, + getAllGridItems(metadata: ElementInstanceMetadataMap): Array { + return Object.values(metadata) + .filter((m) => MetadataUtils.isGridItem(metadata, m.elementPath)) + .map((m) => m.elementPath) + }, isComponentInstanceFromMetadata( metadata: ElementInstanceMetadataMap, path: ElementPath,