diff --git a/packages/block-editor/src/components/block-mover/index.js b/packages/block-editor/src/components/block-mover/index.js
index a143ab391a43f..6d4e461e6416b 100644
--- a/packages/block-editor/src/components/block-mover/index.js
+++ b/packages/block-editor/src/components/block-mover/index.js
@@ -25,7 +25,14 @@ function BlockMover( {
isBlockMoverUpButtonDisabled,
isBlockMoverDownButtonDisabled,
} ) {
- const { canMove, rootClientId, isFirst, isLast, orientation } = useSelect(
+ const {
+ canMove,
+ rootClientId,
+ isFirst,
+ isLast,
+ orientation,
+ isManualGrid,
+ } = useSelect(
( select ) => {
const {
getBlockIndex,
@@ -33,6 +40,7 @@ function BlockMover( {
canMoveBlocks,
getBlockOrder,
getBlockRootClientId,
+ getBlockAttributes,
} = select( blockEditorStore );
const normalizedClientIds = Array.isArray( clientIds )
? clientIds
@@ -44,6 +52,7 @@ function BlockMover( {
normalizedClientIds[ normalizedClientIds.length - 1 ]
);
const blockOrder = getBlockOrder( _rootClientId );
+ const { layout = {} } = getBlockAttributes( _rootClientId ) ?? {};
return {
canMove: canMoveBlocks( clientIds, _rootClientId ),
@@ -51,6 +60,10 @@ function BlockMover( {
isFirst: firstIndex === 0,
isLast: lastIndex === blockOrder.length - 1,
orientation: getBlockListSettings( _rootClientId )?.orientation,
+ isManualGrid:
+ layout.type === 'grid' &&
+ !! layout.columnCount &&
+ window.__experimentalEnableGridInteractivity,
};
},
[ clientIds ]
@@ -60,8 +73,6 @@ function BlockMover( {
return null;
}
- const dragHandleLabel = __( 'Drag' );
-
return (
) }
-
-
- { ( itemProps ) => (
-
- ) }
-
-
- { ( itemProps ) => (
-
- ) }
-
-
+ { ! isManualGrid && (
+
+
+ { ( itemProps ) => (
+
+ ) }
+
+
+ { ( itemProps ) => (
+
+ ) }
+
+
+ ) }
);
}
diff --git a/packages/block-editor/src/components/child-layout-control/index.js b/packages/block-editor/src/components/child-layout-control/index.js
index dfc4ee69437f6..b27d6cacd0697 100644
--- a/packages/block-editor/src/components/child-layout-control/index.js
+++ b/packages/block-editor/src/components/child-layout-control/index.js
@@ -14,6 +14,13 @@ import {
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useEffect } from '@wordpress/element';
+import { useSelect, useDispatch } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import { useGetNumberOfBlocksBeforeCell } from '../grid-visualizer/use-get-number-of-blocks-before-cell';
+import { store as blockEditorStore } from '../../store';
function helpText( selfStretch, parentLayout ) {
const { orientation = 'horizontal' } = parentLayout;
@@ -48,21 +55,46 @@ export default function ChildLayoutControl( {
isShownByDefault,
panelId,
} ) {
- const {
- selfStretch,
- flexSize,
- columnStart,
- rowStart,
- columnSpan,
- rowSpan,
- } = childLayout;
const {
type: parentType,
default: { type: defaultParentType = 'default' } = {},
- orientation = 'horizontal',
} = parentLayout ?? {};
const parentLayoutType = parentType || defaultParentType;
+ if ( parentLayoutType === 'flex' ) {
+ return (
+
+ );
+ } else if ( parentLayoutType === 'grid' ) {
+ return (
+
+ );
+ }
+
+ return null;
+}
+
+function FlexControls( {
+ childLayout,
+ onChange,
+ parentLayout,
+ isShownByDefault,
+ panelId,
+} ) {
+ const { selfStretch, flexSize } = childLayout;
+ const { orientation = 'horizontal' } = parentLayout ?? {};
const hasFlexValue = () => !! selfStretch;
const flexResetLabel =
orientation === 'horizontal' ? __( 'Width' ) : __( 'Height' );
@@ -73,6 +105,96 @@ export default function ChildLayoutControl( {
} );
};
+ useEffect( () => {
+ if ( selfStretch === 'fixed' && ! flexSize ) {
+ onChange( {
+ ...childLayout,
+ selfStretch: 'fit',
+ } );
+ }
+ }, [] );
+
+ return (
+
+ {
+ const newFlexSize = value !== 'fixed' ? null : flexSize;
+ onChange( {
+ selfStretch: value,
+ flexSize: newFlexSize,
+ } );
+ } }
+ isBlock
+ >
+
+
+
+
+ { selfStretch === 'fixed' && (
+ {
+ onChange( {
+ selfStretch,
+ flexSize: value,
+ } );
+ } }
+ value={ flexSize }
+ />
+ ) }
+
+ );
+}
+
+export function childLayoutOrientation( parentLayout ) {
+ const { orientation = 'horizontal' } = parentLayout;
+ return orientation === 'horizontal' ? __( 'Width' ) : __( 'Height' );
+}
+
+function GridControls( {
+ childLayout,
+ onChange,
+ parentLayout,
+ isShownByDefault,
+ panelId,
+} ) {
+ const { columnStart, rowStart, columnSpan, rowSpan } = childLayout;
+ const { columnCount } = parentLayout ?? {};
+ const gridColumnNumber = parseInt( columnCount, 10 ) || 3;
+ const rootClientId = useSelect( ( select ) =>
+ select( blockEditorStore ).getBlockRootClientId( panelId )
+ );
+ const { moveBlocksToPosition, __unstableMarkNextChangeAsNotPersistent } =
+ useDispatch( blockEditorStore );
+ const getNumberOfBlocksBeforeCell = useGetNumberOfBlocksBeforeCell(
+ rootClientId,
+ gridColumnNumber
+ );
const hasStartValue = () => !! columnStart || !! rowStart;
const hasSpanValue = () => !! columnSpan || !! rowSpan;
const resetGridStarts = () => {
@@ -88,184 +210,127 @@ export default function ChildLayoutControl( {
} );
};
- useEffect( () => {
- if ( selfStretch === 'fixed' && ! flexSize ) {
- onChange( {
- ...childLayout,
- selfStretch: 'fit',
- } );
- }
- }, [] );
-
return (
<>
- { parentLayoutType === 'flex' && (
-
+ {
+ onChange( {
+ columnStart,
+ rowStart,
+ rowSpan,
+ columnSpan: value,
+ } );
+ } }
+ value={ columnSpan }
+ min={ 1 }
+ />
+ {
+ onChange( {
+ columnStart,
+ rowStart,
+ columnSpan,
+ rowSpan: value,
+ } );
+ } }
+ value={ rowSpan }
+ min={ 1 }
+ />
+
+ { window.__experimentalEnableGridInteractivity && columnCount && (
+ // Use Flex with an explicit width on the FlexItem instead of HStack to
+ // work around an issue in webkit where inputs with a max attribute are
+ // sized incorrectly.
+
- {
- const newFlexSize =
- value !== 'fixed' ? null : flexSize;
- onChange( {
- selfStretch: value,
- flexSize: newFlexSize,
- } );
- } }
- isBlock
- >
-
-
-
-
- { selfStretch === 'fixed' && (
- {
- onChange( {
- selfStretch,
- flexSize: value,
- } );
- } }
- value={ flexSize }
- />
- ) }
-
- ) }
- { parentLayoutType === 'grid' && (
- <>
-
+
{
onChange( {
- columnStart,
+ columnStart: value,
rowStart,
+ columnSpan,
rowSpan,
- columnSpan: value,
} );
+ __unstableMarkNextChangeAsNotPersistent();
+ moveBlocksToPosition(
+ [ panelId ],
+ rootClientId,
+ rootClientId,
+ getNumberOfBlocksBeforeCell(
+ value,
+ rowStart
+ )
+ );
} }
- value={ columnSpan }
+ value={ columnStart }
min={ 1 }
+ max={
+ gridColumnNumber
+ ? gridColumnNumber - ( columnSpan ?? 1 ) + 1
+ : undefined
+ }
/>
+
+
{
onChange( {
columnStart,
- rowStart,
+ rowStart: value,
columnSpan,
- rowSpan: value,
+ rowSpan,
} );
+ __unstableMarkNextChangeAsNotPersistent();
+ moveBlocksToPosition(
+ [ panelId ],
+ rootClientId,
+ rootClientId,
+ getNumberOfBlocksBeforeCell(
+ columnStart,
+ value
+ )
+ );
} }
- value={ rowSpan }
+ value={ rowStart }
min={ 1 }
+ max={
+ parentLayout?.rowCount
+ ? parentLayout.rowCount -
+ ( rowSpan ?? 1 ) +
+ 1
+ : undefined
+ }
/>
-
- { window.__experimentalEnableGridInteractivity && (
- // Use Flex with an explicit width on the FlexItem instead of HStack to
- // work around an issue in webkit where inputs with a max attribute are
- // sized incorrectly.
-
-
- {
- onChange( {
- columnStart: value,
- rowStart,
- columnSpan,
- rowSpan,
- } );
- } }
- value={ columnStart }
- min={ 1 }
- max={
- parentLayout?.columnCount
- ? parentLayout.columnCount -
- ( columnSpan ?? 1 ) +
- 1
- : undefined
- }
- />
-
-
- {
- onChange( {
- columnStart,
- rowStart: value,
- columnSpan,
- rowSpan,
- } );
- } }
- value={ rowStart }
- min={ 1 }
- max={
- parentLayout?.rowCount
- ? parentLayout.rowCount -
- ( rowSpan ?? 1 ) +
- 1
- : undefined
- }
- />
-
-
- ) }
- >
+
+
) }
>
);
}
-
-export function childLayoutOrientation( parentLayout ) {
- const { orientation = 'horizontal' } = parentLayout;
-
- return orientation === 'horizontal' ? __( 'Width' ) : __( 'Height' );
-}
diff --git a/packages/block-editor/src/components/grid-visualizer/grid-item-movers.js b/packages/block-editor/src/components/grid-visualizer/grid-item-movers.js
new file mode 100644
index 0000000000000..4f1d3853568fd
--- /dev/null
+++ b/packages/block-editor/src/components/grid-visualizer/grid-item-movers.js
@@ -0,0 +1,128 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { ToolbarButton } from '@wordpress/components';
+import { arrowLeft, arrowUp, arrowDown, arrowRight } from '@wordpress/icons';
+import { useDispatch } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import BlockControls from '../block-controls';
+import { useGetNumberOfBlocksBeforeCell } from './use-get-number-of-blocks-before-cell';
+import { store as blockEditorStore } from '../../store';
+
+export function GridItemMovers( {
+ layout,
+ parentLayout,
+ onChange,
+ gridClientId,
+ blockClientId,
+} ) {
+ const { moveBlocksToPosition, __unstableMarkNextChangeAsNotPersistent } =
+ useDispatch( blockEditorStore );
+
+ const columnStart = layout?.columnStart ?? 1;
+ const rowStart = layout?.rowStart ?? 1;
+ const columnSpan = layout?.columnSpan ?? 1;
+ const rowSpan = layout?.rowSpan ?? 1;
+ const columnEnd = columnStart + columnSpan - 1;
+ const rowEnd = rowStart + rowSpan - 1;
+ const columnCount = parentLayout?.columnCount;
+ const rowCount = parentLayout?.rowCount;
+
+ const columnCountNumber = parseInt( columnCount, 10 );
+ const rowStartNumber = parseInt( rowStart, 10 );
+ const columnStartNumber = parseInt( columnStart, 10 );
+
+ const getNumberOfBlocksBeforeCell = useGetNumberOfBlocksBeforeCell(
+ gridClientId,
+ columnCountNumber
+ );
+
+ return (
+
+ {
+ onChange( {
+ rowStart: rowStart - 1,
+ } );
+ __unstableMarkNextChangeAsNotPersistent();
+ moveBlocksToPosition(
+ [ blockClientId ],
+ gridClientId,
+ gridClientId,
+ getNumberOfBlocksBeforeCell(
+ columnStartNumber,
+ rowStartNumber - 1
+ )
+ );
+ } }
+ />
+ = rowCount }
+ onClick={ () => {
+ onChange( {
+ rowStart: rowStart + 1,
+ } );
+ __unstableMarkNextChangeAsNotPersistent();
+ moveBlocksToPosition(
+ [ blockClientId ],
+ gridClientId,
+ gridClientId,
+ getNumberOfBlocksBeforeCell(
+ columnStartNumber,
+ rowStartNumber + 1
+ )
+ );
+ } }
+ />
+ {
+ onChange( {
+ columnStart: columnStartNumber - 1,
+ } );
+ __unstableMarkNextChangeAsNotPersistent();
+ moveBlocksToPosition(
+ [ blockClientId ],
+ gridClientId,
+ gridClientId,
+ getNumberOfBlocksBeforeCell(
+ columnStartNumber - 1,
+ rowStartNumber
+ )
+ );
+ } }
+ />
+ = columnCount }
+ onClick={ () => {
+ onChange( {
+ columnStart: columnStartNumber + 1,
+ } );
+ __unstableMarkNextChangeAsNotPersistent();
+ moveBlocksToPosition(
+ [ blockClientId ],
+ gridClientId,
+ gridClientId,
+ getNumberOfBlocksBeforeCell(
+ columnStartNumber + 1,
+ rowStartNumber
+ )
+ );
+ } }
+ />
+
+ );
+}
diff --git a/packages/block-editor/src/components/grid-visualizer/grid-item-resizer.js b/packages/block-editor/src/components/grid-visualizer/grid-item-resizer.js
index 21e9bfccee754..a5847d852e0a9 100644
--- a/packages/block-editor/src/components/grid-visualizer/grid-item-resizer.js
+++ b/packages/block-editor/src/components/grid-visualizer/grid-item-resizer.js
@@ -9,11 +9,17 @@ import { useState, useEffect } from '@wordpress/element';
*/
import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs';
import BlockPopoverCover from '../block-popover/cover';
-import { getComputedCSS } from './utils';
+import { getComputedCSS, getGridTracks, getClosestTrack } from './utils';
-export function GridItemResizer( { clientId, bounds, onChange } ) {
+export function GridItemResizer( {
+ clientId,
+ bounds,
+ onChange,
+ parentLayout,
+} ) {
const blockElement = useBlockElement( clientId );
const rootBlockElement = blockElement?.parentElement;
+ const { columnCount } = parentLayout;
if ( ! blockElement || ! rootBlockElement ) {
return null;
@@ -26,6 +32,9 @@ export function GridItemResizer( { clientId, bounds, onChange } ) {
blockElement={ blockElement }
rootBlockElement={ rootBlockElement }
onChange={ onChange }
+ isManualGrid={
+ !! columnCount && window.__experimentalEnableGridInteractivity
+ }
/>
);
}
@@ -36,6 +45,7 @@ function GridItemResizerInner( {
blockElement,
rootBlockElement,
onChange,
+ isManualGrid,
} ) {
const [ resizeDirection, setResizeDirection ] = useState( null );
const [ enableSide, setEnableSide ] = useState( {
@@ -171,59 +181,11 @@ function GridItemResizerInner( {
onChange( {
columnSpan: columnEnd - columnStart + 1,
rowSpan: rowEnd - rowStart + 1,
+ columnStart: isManualGrid ? columnStart : undefined,
+ rowStart: isManualGrid ? rowStart : undefined,
} );
} }
/>
);
}
-
-/**
- * Given a grid-template-columns or grid-template-rows CSS property value, gets the start and end
- * position in pixels of each grid track.
- *
- * https://css-tricks.com/snippets/css/complete-guide-grid/#aa-grid-track
- *
- * @param {string} template The grid-template-columns or grid-template-rows CSS property value.
- * Only supports fixed sizes in pixels.
- * @param {number} gap The gap between grid tracks in pixels.
- *
- * @return {Array<{start: number, end: number}>} An array of objects with the start and end
- * position in pixels of each grid track.
- */
-function getGridTracks( template, gap ) {
- const tracks = [];
- for ( const size of template.split( ' ' ) ) {
- const previousTrack = tracks[ tracks.length - 1 ];
- const start = previousTrack ? previousTrack.end + gap : 0;
- const end = start + parseFloat( size );
- tracks.push( { start, end } );
- }
- return tracks;
-}
-
-/**
- * Given an array of grid tracks and a position in pixels, gets the index of the closest track to
- * that position.
- *
- * https://css-tricks.com/snippets/css/complete-guide-grid/#aa-grid-track
- *
- * @param {Array<{start: number, end: number}>} tracks An array of objects with the start and end
- * position in pixels of each grid track.
- * @param {number} position The position in pixels.
- * @param {string} edge The edge of the track to compare the
- * position to. Either 'start' or 'end'.
- *
- * @return {number} The index of the closest track to the position. 0-based, unlike CSS grid which
- * is 1-based.
- */
-function getClosestTrack( tracks, position, edge = 'start' ) {
- return tracks.reduce(
- ( closest, track, index ) =>
- Math.abs( track[ edge ] - position ) <
- Math.abs( tracks[ closest ][ edge ] - position )
- ? index
- : closest,
- 0
- );
-}
diff --git a/packages/block-editor/src/components/grid-visualizer/grid-visualizer.js b/packages/block-editor/src/components/grid-visualizer/grid-visualizer.js
index cff5efc5218e1..5e639615a062c 100644
--- a/packages/block-editor/src/components/grid-visualizer/grid-visualizer.js
+++ b/packages/block-editor/src/components/grid-visualizer/grid-visualizer.js
@@ -1,101 +1,267 @@
+/**
+ * External dependencies
+ */
+import clsx from 'clsx';
+
/**
* WordPress dependencies
*/
import { useState, useEffect, forwardRef } from '@wordpress/element';
-import { useSelect } from '@wordpress/data';
+import { useSelect, useDispatch } from '@wordpress/data';
+import { __experimentalUseDropZone as useDropZone } from '@wordpress/compose';
/**
* Internal dependencies
*/
import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs';
import BlockPopoverCover from '../block-popover/cover';
+import { range, GridRect, getGridInfo } from './utils';
import { store as blockEditorStore } from '../../store';
-import { getComputedCSS } from './utils';
+import { useGetNumberOfBlocksBeforeCell } from './use-get-number-of-blocks-before-cell';
-export function GridVisualizer( { clientId, contentRef } ) {
+export function GridVisualizer( { clientId, contentRef, parentLayout } ) {
const isDistractionFree = useSelect(
( select ) =>
select( blockEditorStore ).getSettings().isDistractionFree,
[]
);
- const blockElement = useBlockElement( clientId );
+ const gridElement = useBlockElement( clientId );
- if ( isDistractionFree || ! blockElement ) {
+ if ( isDistractionFree || ! gridElement ) {
return null;
}
+ const isManualGrid =
+ parentLayout?.columnCount &&
+ window.__experimentalEnableGridInteractivity;
return (
-
-
-
+ gridElement={ gridElement }
+ isManualGrid={ isManualGrid }
+ ref={ contentRef }
+ />
);
}
-const GridVisualizerGrid = forwardRef( ( { blockElement }, ref ) => {
- const [ gridInfo, setGridInfo ] = useState( () =>
- getGridInfo( blockElement )
- );
- useEffect( () => {
- const observers = [];
- for ( const element of [ blockElement, ...blockElement.children ] ) {
- const observer = new window.ResizeObserver( () => {
- setGridInfo( getGridInfo( blockElement ) );
- } );
- observer.observe( element );
- observers.push( observer );
- }
- return () => {
- for ( const observer of observers ) {
- observer.disconnect();
+const GridVisualizerGrid = forwardRef(
+ ( { clientId, gridElement, isManualGrid }, ref ) => {
+ const [ gridInfo, setGridInfo ] = useState( () =>
+ getGridInfo( gridElement )
+ );
+ const [ isDroppingAllowed, setIsDroppingAllowed ] = useState( false );
+ const [ highlightedRect, setHighlightedRect ] = useState( null );
+
+ useEffect( () => {
+ const observers = [];
+ for ( const element of [ gridElement, ...gridElement.children ] ) {
+ const observer = new window.ResizeObserver( () => {
+ setGridInfo( getGridInfo( gridElement ) );
+ } );
+ observer.observe( element );
+ observers.push( observer );
+ }
+ return () => {
+ for ( const observer of observers ) {
+ observer.disconnect();
+ }
+ };
+ }, [ gridElement ] );
+
+ useEffect( () => {
+ function onGlobalDrag() {
+ setIsDroppingAllowed( true );
}
- };
- }, [ blockElement ] );
+ function onGlobalDragEnd() {
+ setIsDroppingAllowed( false );
+ }
+ document.addEventListener( 'drag', onGlobalDrag );
+ document.addEventListener( 'dragend', onGlobalDragEnd );
+ return () => {
+ document.removeEventListener( 'drag', onGlobalDrag );
+ document.removeEventListener( 'dragend', onGlobalDragEnd );
+ };
+ }, [] );
+
+ return (
+
+
+ { isManualGrid
+ ? range( 1, gridInfo.numRows ).map( ( row ) =>
+ range( 1, gridInfo.numColumns ).map(
+ ( column ) => (
+
+
+
+ )
+ )
+ )
+ : Array.from(
+ { length: gridInfo.numItems },
+ ( _, i ) => (
+
+ )
+ ) }
+
+
+ );
+ }
+);
+
+function GridVisualizerCell( { color, children } ) {
return (
- { Array.from( { length: gridInfo.numItems }, ( _, i ) => (
-
- ) ) }
+ { children }
);
-} );
+}
+
+function GridVisualizerDropZone( {
+ column,
+ row,
+ gridClientId,
+ gridInfo,
+ highlightedRect,
+ setHighlightedRect,
+} ) {
+ const { getBlockAttributes } = useSelect( blockEditorStore );
+ const {
+ updateBlockAttributes,
+ moveBlocksToPosition,
+ __unstableMarkNextChangeAsNotPersistent,
+ } = useDispatch( blockEditorStore );
-function getGridInfo( blockElement ) {
- const gridTemplateColumns = getComputedCSS(
- blockElement,
- 'grid-template-columns'
+ const getNumberOfBlocksBeforeCell = useGetNumberOfBlocksBeforeCell(
+ gridClientId,
+ gridInfo.numColumns
);
- const gridTemplateRows = getComputedCSS(
- blockElement,
- 'grid-template-rows'
+
+ const ref = useDropZoneWithValidation( {
+ validateDrag( srcClientId ) {
+ const attributes = getBlockAttributes( srcClientId );
+ const rect = new GridRect( {
+ columnStart: column,
+ rowStart: row,
+ columnSpan: attributes.style?.layout?.columnSpan,
+ rowSpan: attributes.style?.layout?.rowSpan,
+ } );
+ const isInBounds = new GridRect( {
+ columnSpan: gridInfo.numColumns,
+ rowSpan: gridInfo.numRows,
+ } ).containsRect( rect );
+ return isInBounds;
+ },
+ onDragEnter( srcClientId ) {
+ const attributes = getBlockAttributes( srcClientId );
+ setHighlightedRect(
+ new GridRect( {
+ columnStart: column,
+ rowStart: row,
+ columnSpan: attributes.style?.layout?.columnSpan,
+ rowSpan: attributes.style?.layout?.rowSpan,
+ } )
+ );
+ },
+ onDragLeave() {
+ // onDragEnter can be called before onDragLeave if the user moves
+ // their mouse quickly, so only clear the highlight if it was set
+ // by this cell.
+ setHighlightedRect( ( prevHighlightedRect ) =>
+ prevHighlightedRect?.columnStart === column &&
+ prevHighlightedRect?.rowStart === row
+ ? null
+ : prevHighlightedRect
+ );
+ },
+ onDrop( srcClientId ) {
+ setHighlightedRect( null );
+ const attributes = getBlockAttributes( srcClientId );
+ updateBlockAttributes( srcClientId, {
+ style: {
+ ...attributes.style,
+ layout: {
+ ...attributes.style?.layout,
+ columnStart: column,
+ rowStart: row,
+ },
+ },
+ } );
+ __unstableMarkNextChangeAsNotPersistent();
+ moveBlocksToPosition(
+ [ srcClientId ],
+ gridClientId,
+ gridClientId,
+ getNumberOfBlocksBeforeCell( column, row )
+ );
+ },
+ } );
+
+ const isHighlighted = highlightedRect?.contains( column, row ) ?? false;
+
+ return (
+
);
- const numColumns = gridTemplateColumns.split( ' ' ).length;
- const numRows = gridTemplateRows.split( ' ' ).length;
- const numItems = numColumns * numRows;
- return {
- numItems,
- currentColor: getComputedCSS( blockElement, 'color' ),
- style: {
- gridTemplateColumns,
- gridTemplateRows,
- gap: getComputedCSS( blockElement, 'gap' ),
- padding: getComputedCSS( blockElement, 'padding' ),
+}
+
+function useDropZoneWithValidation( {
+ validateDrag,
+ onDragEnter,
+ onDragLeave,
+ onDrop,
+} ) {
+ const { getDraggedBlockClientIds } = useSelect( blockEditorStore );
+ return useDropZone( {
+ onDragEnter() {
+ const [ srcClientId ] = getDraggedBlockClientIds();
+ if ( srcClientId && validateDrag( srcClientId ) ) {
+ onDragEnter( srcClientId );
+ }
+ },
+ onDragLeave() {
+ onDragLeave();
+ },
+ onDrop() {
+ const [ srcClientId ] = getDraggedBlockClientIds();
+ if ( srcClientId && validateDrag( srcClientId ) ) {
+ onDrop( srcClientId );
+ }
},
- };
+ } );
}
diff --git a/packages/block-editor/src/components/grid-visualizer/index.js b/packages/block-editor/src/components/grid-visualizer/index.js
index add845d702203..a1552610102a0 100644
--- a/packages/block-editor/src/components/grid-visualizer/index.js
+++ b/packages/block-editor/src/components/grid-visualizer/index.js
@@ -1,2 +1,4 @@
export { GridVisualizer } from './grid-visualizer';
export { GridItemResizer } from './grid-item-resizer';
+export { GridItemMovers } from './grid-item-movers';
+export { useGridLayoutSync } from './use-grid-layout-sync';
diff --git a/packages/block-editor/src/components/grid-visualizer/style.scss b/packages/block-editor/src/components/grid-visualizer/style.scss
index 2adaf18f52470..dfb3e57f84ae0 100644
--- a/packages/block-editor/src/components/grid-visualizer/style.scss
+++ b/packages/block-editor/src/components/grid-visualizer/style.scss
@@ -1,34 +1,63 @@
-// TODO: Specificity hacks to get rid of all these darn !importants.
-
.block-editor-grid-visualizer {
- z-index: z-index(".block-editor-grid-visualizer") !important;
-}
-
-.block-editor-grid-visualizer .components-popover__content * {
- pointer-events: none !important;
+ // Specificity to override the z-index and pointer-events set by .components-popover.
+ &.block-editor-grid-visualizer.block-editor-grid-visualizer {
+ z-index: z-index(".block-editor-grid-visualizer");
+
+ .components-popover__content * {
+ pointer-events: none;
+ }
+
+ &.is-dropping-allowed {
+ .block-editor-grid-visualizer__drop-zone {
+ pointer-events: all;
+ }
+ }
+ }
}
.block-editor-grid-visualizer__grid {
display: grid;
}
-.block-editor-grid-visualizer__item {
- outline: 1px solid transparent;
- border-radius: $radius-block-ui;
+.block-editor-grid-visualizer__cell {
+ align-items: center;
+ display: flex;
+ justify-content: center;
}
-.block-editor-grid-item-resizer {
- z-index: z-index(".block-editor-grid-visualizer") !important;
+.block-editor-grid-visualizer__drop-zone {
+ background: rgba($gray-400, 0.1);
+ border: $border-width dotted $gray-300;
+ width: 100%;
+ height: 100%;
+
+ // Make drop zone 8x8 at minimum so that it's easier to drag into. This will overflow the parent.
+ min-width: $grid-unit-10;
+ min-height: $grid-unit-10;
+
+ &.is-highlighted {
+ background: var(--wp-admin-theme-color);
+ }
}
-.block-editor-grid-item-resizer .components-popover__content * {
- pointer-events: none !important;
+.block-editor-grid-item-resizer {
+ // Specificity to override the z-index and pointer-events set by .components-popover.
+ &.block-editor-grid-item-resizer.block-editor-grid-item-resizer {
+ z-index: z-index(".block-editor-grid-visualizer");
+
+ .components-popover__content * {
+ pointer-events: none;
+ }
+ }
}
.block-editor-grid-item-resizer__box {
border: $border-width solid var(--wp-admin-theme-color);
.components-resizable-box__handle {
- pointer-events: all !important;
+ // Specificity to override the pointer-events set by .components-popover.
+ &.components-resizable-box__handle.components-resizable-box__handle {
+ pointer-events: all;
+ }
}
}
diff --git a/packages/block-editor/src/components/grid-visualizer/use-get-number-of-blocks-before-cell.js b/packages/block-editor/src/components/grid-visualizer/use-get-number-of-blocks-before-cell.js
new file mode 100644
index 0000000000000..11e991c432080
--- /dev/null
+++ b/packages/block-editor/src/components/grid-visualizer/use-get-number-of-blocks-before-cell.js
@@ -0,0 +1,30 @@
+/**
+ * WordPress dependencies
+ */
+import { useSelect } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import { store as blockEditorStore } from '../../store';
+
+export function useGetNumberOfBlocksBeforeCell( gridClientId, numColumns ) {
+ const { getBlockOrder, getBlockAttributes } = useSelect( blockEditorStore );
+
+ const getNumberOfBlocksBeforeCell = ( column, row ) => {
+ const targetIndex = ( row - 1 ) * numColumns + column - 1;
+
+ let count = 0;
+ for ( const clientId of getBlockOrder( gridClientId ) ) {
+ const { columnStart, rowStart } =
+ getBlockAttributes( clientId ).style?.layout ?? {};
+ const cellIndex = ( rowStart - 1 ) * numColumns + columnStart - 1;
+ if ( cellIndex < targetIndex ) {
+ count++;
+ }
+ }
+ return count;
+ };
+
+ return getNumberOfBlocksBeforeCell;
+}
diff --git a/packages/block-editor/src/components/grid-visualizer/use-grid-layout-sync.js b/packages/block-editor/src/components/grid-visualizer/use-grid-layout-sync.js
new file mode 100644
index 0000000000000..6a3a05e52fcb9
--- /dev/null
+++ b/packages/block-editor/src/components/grid-visualizer/use-grid-layout-sync.js
@@ -0,0 +1,167 @@
+/**
+ * WordPress dependencies
+ */
+import { useDispatch, useSelect } from '@wordpress/data';
+import { useEffect } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { store as blockEditorStore } from '../../store';
+import { GridRect } from './utils';
+
+export function useGridLayoutSync( { clientId: gridClientId } ) {
+ const { gridLayout, blockOrder } = useSelect(
+ ( select ) => {
+ const { getBlockAttributes, getBlockOrder } =
+ select( blockEditorStore );
+ return {
+ gridLayout: getBlockAttributes( gridClientId ).layout ?? {},
+ blockOrder: getBlockOrder( gridClientId ),
+ };
+ },
+ [ gridClientId ]
+ );
+
+ const { getBlockAttributes } = useSelect( blockEditorStore );
+ const { updateBlockAttributes, __unstableMarkNextChangeAsNotPersistent } =
+ useDispatch( blockEditorStore );
+
+ useEffect( () => {
+ const updates = {};
+
+ const { columnCount, rowCount = 2 } = gridLayout;
+ const isManualGrid = !! columnCount;
+
+ if ( isManualGrid ) {
+ const rects = [];
+ let cellsTaken = 0;
+
+ // Respect the position of blocks that already have a columnStart and rowStart value.
+ for ( const clientId of blockOrder ) {
+ const attributes = getBlockAttributes( clientId );
+ const {
+ columnStart,
+ rowStart,
+ columnSpan = 1,
+ rowSpan = 1,
+ } = attributes.style?.layout || {};
+ cellsTaken += columnSpan * rowSpan;
+ if ( ! columnStart || ! rowStart ) {
+ continue;
+ }
+ rects.push(
+ new GridRect( {
+ columnStart,
+ rowStart,
+ columnSpan,
+ rowSpan,
+ } )
+ );
+ }
+
+ // Ensure there's enough rows to fit all blocks.
+ const minimumNeededRows = Math.ceil( cellsTaken / columnCount );
+ if ( rowCount < minimumNeededRows ) {
+ updates[ gridClientId ] = {
+ layout: {
+ ...gridLayout,
+ rowCount: minimumNeededRows,
+ },
+ };
+ }
+
+ // When in manual mode, ensure that every block has a columnStart and rowStart value.
+ for ( const clientId of blockOrder ) {
+ const attributes = getBlockAttributes( clientId );
+ const { columnStart, rowStart, columnSpan, rowSpan } =
+ attributes.style?.layout || {};
+ if ( columnStart && rowStart ) {
+ continue;
+ }
+ const [ newColumnStart, newRowStart ] = getFirstEmptyCell(
+ rects,
+ columnCount,
+ minimumNeededRows,
+ columnSpan,
+ rowSpan
+ );
+ rects.push(
+ new GridRect( {
+ columnStart: newColumnStart,
+ rowStart: newRowStart,
+ columnSpan,
+ rowSpan,
+ } )
+ );
+ updates[ clientId ] = {
+ style: {
+ ...attributes.style,
+ layout: {
+ ...attributes.style?.layout,
+ columnStart: newColumnStart,
+ rowStart: newRowStart,
+ },
+ },
+ };
+ }
+ } else {
+ // When in auto mode, remove all of the columnStart and rowStart values.
+ for ( const clientId of blockOrder ) {
+ const attributes = getBlockAttributes( clientId );
+ const { columnStart, rowStart, ...layout } =
+ attributes.style?.layout || {};
+ // Only update attributes if columnStart or rowStart are set.
+ if ( columnStart || rowStart ) {
+ updates[ clientId ] = {
+ style: {
+ ...attributes.style,
+ layout,
+ },
+ };
+ }
+ }
+ }
+
+ if ( Object.keys( updates ).length ) {
+ __unstableMarkNextChangeAsNotPersistent();
+ updateBlockAttributes(
+ Object.keys( updates ),
+ updates,
+ /* uniqueByBlock: */ true
+ );
+ }
+ }, [
+ // Actual deps to sync:
+ gridClientId,
+ gridLayout,
+ blockOrder,
+ // Needed for linter:
+ __unstableMarkNextChangeAsNotPersistent,
+ getBlockAttributes,
+ updateBlockAttributes,
+ ] );
+}
+
+function getFirstEmptyCell(
+ rects,
+ columnCount,
+ rowCount,
+ columnSpan = 1,
+ rowSpan = 1
+) {
+ for ( let row = 1; row <= rowCount; row++ ) {
+ for ( let column = 1; column <= columnCount; column++ ) {
+ const rect = new GridRect( {
+ columnStart: column,
+ rowStart: row,
+ columnSpan,
+ rowSpan,
+ } );
+ if ( ! rects.some( ( r ) => r.intersectsRect( rect ) ) ) {
+ return [ column, row ];
+ }
+ }
+ }
+ return [ 1, 1 ];
+}
diff --git a/packages/block-editor/src/components/grid-visualizer/utils.js b/packages/block-editor/src/components/grid-visualizer/utils.js
index a100e596a4e24..fc012c645f091 100644
--- a/packages/block-editor/src/components/grid-visualizer/utils.js
+++ b/packages/block-editor/src/components/grid-visualizer/utils.js
@@ -1,5 +1,178 @@
+export function range( start, length ) {
+ return Array.from( { length }, ( _, i ) => start + i );
+}
+
+export class GridRect {
+ constructor( {
+ columnStart,
+ rowStart,
+ columnEnd,
+ rowEnd,
+ columnSpan,
+ rowSpan,
+ } = {} ) {
+ this.columnStart = columnStart ?? 1;
+ this.rowStart = rowStart ?? 1;
+ if ( columnSpan !== undefined ) {
+ this.columnEnd = this.columnStart + columnSpan - 1;
+ } else {
+ this.columnEnd = columnEnd ?? this.columnStart;
+ }
+ if ( rowSpan !== undefined ) {
+ this.rowEnd = this.rowStart + rowSpan - 1;
+ } else {
+ this.rowEnd = rowEnd ?? this.rowStart;
+ }
+ }
+
+ get columnSpan() {
+ return this.columnEnd - this.columnStart + 1;
+ }
+
+ get rowSpan() {
+ return this.rowEnd - this.rowStart + 1;
+ }
+
+ contains( column, row ) {
+ return (
+ column >= this.columnStart &&
+ column <= this.columnEnd &&
+ row >= this.rowStart &&
+ row <= this.rowEnd
+ );
+ }
+
+ containsRect( rect ) {
+ return (
+ this.contains( rect.columnStart, rect.rowStart ) &&
+ this.contains( rect.columnEnd, rect.rowEnd )
+ );
+ }
+
+ intersectsRect( rect ) {
+ return (
+ this.columnStart <= rect.columnEnd &&
+ this.columnEnd >= rect.columnStart &&
+ this.rowStart <= rect.rowEnd &&
+ this.rowEnd >= rect.rowStart
+ );
+ }
+}
+
export function getComputedCSS( element, property ) {
return element.ownerDocument.defaultView
.getComputedStyle( element )
.getPropertyValue( property );
}
+
+/**
+ * Given a grid-template-columns or grid-template-rows CSS property value, gets the start and end
+ * position in pixels of each grid track.
+ *
+ * https://css-tricks.com/snippets/css/complete-guide-grid/#aa-grid-track
+ *
+ * @param {string} template The grid-template-columns or grid-template-rows CSS property value.
+ * Only supports fixed sizes in pixels.
+ * @param {number} gap The gap between grid tracks in pixels.
+ *
+ * @return {Array<{start: number, end: number}>} An array of objects with the start and end
+ * position in pixels of each grid track.
+ */
+export function getGridTracks( template, gap ) {
+ const tracks = [];
+ for ( const size of template.split( ' ' ) ) {
+ const previousTrack = tracks[ tracks.length - 1 ];
+ const start = previousTrack ? previousTrack.end + gap : 0;
+ const end = start + parseFloat( size );
+ tracks.push( { start, end } );
+ }
+ return tracks;
+}
+
+/**
+ * Given an array of grid tracks and a position in pixels, gets the index of the closest track to
+ * that position.
+ *
+ * https://css-tricks.com/snippets/css/complete-guide-grid/#aa-grid-track
+ *
+ * @param {Array<{start: number, end: number}>} tracks An array of objects with the start and end
+ * position in pixels of each grid track.
+ * @param {number} position The position in pixels.
+ * @param {string} edge The edge of the track to compare the
+ * position to. Either 'start' or 'end'.
+ *
+ * @return {number} The index of the closest track to the position. 0-based, unlike CSS grid which
+ * is 1-based.
+ */
+export function getClosestTrack( tracks, position, edge = 'start' ) {
+ return tracks.reduce(
+ ( closest, track, index ) =>
+ Math.abs( track[ edge ] - position ) <
+ Math.abs( tracks[ closest ][ edge ] - position )
+ ? index
+ : closest,
+ 0
+ );
+}
+
+export function getGridRect( gridElement, rect ) {
+ const columnGap = parseFloat( getComputedCSS( gridElement, 'column-gap' ) );
+ const rowGap = parseFloat( getComputedCSS( gridElement, 'row-gap' ) );
+ const gridColumnTracks = getGridTracks(
+ getComputedCSS( gridElement, 'grid-template-columns' ),
+ columnGap
+ );
+ const gridRowTracks = getGridTracks(
+ getComputedCSS( gridElement, 'grid-template-rows' ),
+ rowGap
+ );
+ const columnStart = getClosestTrack( gridColumnTracks, rect.left ) + 1;
+ const rowStart = getClosestTrack( gridRowTracks, rect.top ) + 1;
+ const columnEnd =
+ getClosestTrack( gridColumnTracks, rect.right, 'end' ) + 1;
+ const rowEnd = getClosestTrack( gridRowTracks, rect.bottom, 'end' ) + 1;
+ return new GridRect( {
+ columnStart,
+ columnEnd,
+ rowStart,
+ rowEnd,
+ } );
+}
+
+export function getGridItemRect( gridItemElement ) {
+ return getGridRect(
+ gridItemElement.parentElement,
+ new window.DOMRect(
+ gridItemElement.offsetLeft,
+ gridItemElement.offsetTop,
+ gridItemElement.offsetWidth,
+ gridItemElement.offsetHeight
+ )
+ );
+}
+
+export function getGridInfo( gridElement ) {
+ const gridTemplateColumns = getComputedCSS(
+ gridElement,
+ 'grid-template-columns'
+ );
+ const gridTemplateRows = getComputedCSS(
+ gridElement,
+ 'grid-template-rows'
+ );
+ const numColumns = gridTemplateColumns.split( ' ' ).length;
+ const numRows = gridTemplateRows.split( ' ' ).length;
+ const numItems = numColumns * numRows;
+ return {
+ numColumns,
+ numRows,
+ numItems,
+ currentColor: getComputedCSS( gridElement, 'color' ),
+ style: {
+ gridTemplateColumns,
+ gridTemplateRows,
+ gap: getComputedCSS( gridElement, 'gap' ),
+ padding: getComputedCSS( gridElement, 'padding' ),
+ },
+ };
+}
diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js
index 49ab36499692b..d46f28d1e19f2 100644
--- a/packages/block-editor/src/components/inner-blocks/index.js
+++ b/packages/block-editor/src/components/inner-blocks/index.js
@@ -267,7 +267,9 @@ export function useInnerBlocksProps( props = {}, options = {} ) {
const ref = useMergeRefs( [
props.ref,
- __unstableDisableDropZone || isDropZoneDisabled
+ __unstableDisableDropZone ||
+ isDropZoneDisabled ||
+ ( layout?.columnCount && window.__experimentalEnableGridInteractivity )
? null
: blockDropZoneRef,
] );
diff --git a/packages/block-editor/src/hooks/layout-child.js b/packages/block-editor/src/hooks/layout-child.js
index 860b4aaf04179..d92d1069c2e33 100644
--- a/packages/block-editor/src/hooks/layout-child.js
+++ b/packages/block-editor/src/hooks/layout-child.js
@@ -11,7 +11,11 @@ import { useState } from '@wordpress/element';
import { store as blockEditorStore } from '../store';
import { useStyleOverride } from './utils';
import { useLayout } from '../components/block-list/layout';
-import { GridVisualizer, GridItemResizer } from '../components/grid-visualizer';
+import {
+ GridVisualizer,
+ GridItemResizer,
+ GridItemMovers,
+} from '../components/grid-visualizer';
function useBlockPropsChildLayoutStyles( { style } ) {
const shouldRenderChildLayoutStyles = useSelect( ( select ) => {
@@ -135,10 +139,12 @@ function useBlockPropsChildLayoutStyles( { style } ) {
}
function ChildLayoutControlsPure( { clientId, style, setAttributes } ) {
+ const parentLayout = useLayout() || {};
const {
type: parentLayoutType = 'default',
allowSizingOnChildren = false,
- } = useLayout() || {};
+ columnCount,
+ } = parentLayout;
const rootClientId = useSelect(
( select ) => {
@@ -154,29 +160,43 @@ function ChildLayoutControlsPure( { clientId, style, setAttributes } ) {
return null;
}
+ const isManualGrid = !! columnCount;
+
+ function updateLayout( layout ) {
+ setAttributes( {
+ style: {
+ ...style,
+ layout: {
+ ...style?.layout,
+ ...layout,
+ },
+ },
+ } );
+ }
+
return (
<>
{ allowSizingOnChildren && (
{
- setAttributes( {
- style: {
- ...style,
- layout: {
- ...style?.layout,
- columnSpan,
- rowSpan,
- },
- },
- } );
- } }
+ onChange={ updateLayout }
+ parentLayout={ parentLayout }
+ />
+ ) }
+ { isManualGrid && window.__experimentalEnableGridInteractivity && (
+
) }
>
diff --git a/packages/block-editor/src/layouts/grid.js b/packages/block-editor/src/layouts/grid.js
index 4528de117c45b..6a42d6898697f 100644
--- a/packages/block-editor/src/layouts/grid.js
+++ b/packages/block-editor/src/layouts/grid.js
@@ -23,7 +23,10 @@ import { appendSelectors, getBlockGapCSS } from './utils';
import { getGapCSSValue } from '../hooks/gap';
import { shouldSkipSerialization } from '../hooks/utils';
import { LAYOUT_DEFINITIONS } from './definitions';
-import { GridVisualizer } from '../components/grid-visualizer';
+import {
+ GridVisualizer,
+ useGridLayoutSync,
+} from '../components/grid-visualizer';
const RANGE_CONTROL_MAX_VALUES = {
px: 600,
@@ -93,7 +96,14 @@ export default {
);
},
toolBarControls: function GridLayoutToolbarControls( { clientId } ) {
- return ;
+ return (
+ <>
+ { window.__experimentalEnableGridInteractivity && (
+
+ ) }
+
+ >
+ );
},
getLayoutStyle: function getLayoutStyle( {
selector,
@@ -245,9 +255,6 @@ function GridLayoutColumnsAndRowsControl( {
return (
<>
- { allowSizingOnChildren &&
- window.__experimentalEnableGridInteractivity && (
-
- ) }
>
);
}
@@ -366,10 +348,19 @@ function GridLayoutTypeControl( { layout, onChange } ) {
return (
);
}
+
+function GridLayoutSync( props ) {
+ useGridLayoutSync( props );
+}