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 e0af72c238ad5..eb2a02e5095d7 100644 --- a/packages/block-editor/src/components/child-layout-control/index.js +++ b/packages/block-editor/src/components/child-layout-control/index.js @@ -223,7 +223,13 @@ export default function ChildLayoutControl( { } } value={ columnStart } min={ 1 } - max={ parentLayout?.columnCount } + max={ + parentLayout?.columnCount + ? parentLayout.columnCount - + ( columnSpan ?? 1 ) + + 1 + : undefined + } /> @@ -241,7 +247,13 @@ export default function ChildLayoutControl( { } } value={ rowStart } min={ 1 } - max={ parentLayout?.columnCount } + max={ + parentLayout?.rowCount + ? parentLayout.rowCount - + ( rowSpan ?? 1 ) + + 1 + : undefined + } /> diff --git a/packages/block-editor/src/components/grid-interactivity/grid-item-pin-toolbar-item.js b/packages/block-editor/src/components/grid-interactivity/grid-item-pin-toolbar-item.js new file mode 100644 index 0000000000000..0f786738b06ff --- /dev/null +++ b/packages/block-editor/src/components/grid-interactivity/grid-item-pin-toolbar-item.js @@ -0,0 +1,50 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { ToolbarButton } from '@wordpress/components'; +import { pin as pinIcon } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import BlockControls from '../block-controls'; +import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; +import { getGridItemRect } from './utils'; + +export function GridItemPinToolbarItem( { clientId, layout, onChange } ) { + const blockElement = useBlockElement( clientId ); + if ( ! blockElement ) { + return null; + } + + const isPinned = !! layout?.columnStart || !! layout?.rowStart; + + function unpinBlock() { + onChange( { + columnStart: undefined, + rowStart: undefined, + } ); + } + + function pinBlock() { + const rect = getGridItemRect( blockElement ); + onChange( { + columnStart: rect.columnStart, + rowStart: rect.rowStart, + } ); + } + + return ( + + + + ); +} diff --git a/packages/block-editor/src/components/grid-interactivity/grid-item-resizer.js b/packages/block-editor/src/components/grid-interactivity/grid-item-resizer.js new file mode 100644 index 0000000000000..f628f081286f7 --- /dev/null +++ b/packages/block-editor/src/components/grid-interactivity/grid-item-resizer.js @@ -0,0 +1,58 @@ +/** + * WordPress dependencies + */ +import { ResizableBox } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; +import BlockPopoverCover from '../block-popover/cover'; +import { getGridRect } from './utils'; + +export function GridItemResizer( { clientId, onChange } ) { + const blockElement = useBlockElement( clientId ); + if ( ! blockElement ) { + return null; + } + return ( + + { + const rect = getGridRect( + blockElement.parentElement, + new window.DOMRect( + blockElement.offsetLeft, + blockElement.offsetTop, + boxElement.offsetWidth, + boxElement.offsetHeight + ) + ); + onChange( { + columnSpan: rect.columnSpan, + rowSpan: rect.rowSpan, + } ); + } } + /> + + ); +} diff --git a/packages/block-editor/src/components/grid-interactivity/grid-visualizer.js b/packages/block-editor/src/components/grid-interactivity/grid-visualizer.js new file mode 100644 index 0000000000000..3ab9cc0c84515 --- /dev/null +++ b/packages/block-editor/src/components/grid-interactivity/grid-visualizer.js @@ -0,0 +1,528 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { useState, useEffect } from '@wordpress/element'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { __experimentalUseDropZone as useDropZone } from '@wordpress/compose'; +import { Popover } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; +import BlockPopoverCover from '../block-popover/cover'; +import { + getComputedCSS, + range, + GridRect, + getGridItemRect, + getGridCell, + getClosestGridCell, +} from './utils'; +import { store as blockEditorStore } from '../../store'; +import QuickInserter from '../inserter/quick-inserter'; + +export function GridVisualizer( { clientId } ) { + const gridElement = useBlockElement( clientId ); + if ( ! gridElement ) { + return null; + } + return ( + + ); +} + +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, + style: { + gridTemplateColumns, + gridTemplateRows, + gap: getComputedCSS( gridElement, 'gap' ), + padding: getComputedCSS( gridElement, 'padding' ), + }, + }; +} + +// TODO: clean up these two funcs +// TODO: these two funcs don't properly handle the case where an item only has a row or a column set +// what we're supposed to do in that case is quite complicated: https://www.w3.org/TR/css-grid-1/#auto-placement-algo + +function getNaturalPosition( { + rects, + numColumns, + numRows, + columnStart, // if specified, constrain to this column + rowStart, // if specified, constrain to this row + columnSpan = 1, + rowSpan = 1, +} ) { + for ( let row = 1; row <= numRows; row++ ) { + for ( let column = 1; column <= numColumns; column++ ) { + const rect = new GridRect( { + columnStart: columnStart ?? column, + rowStart: rowStart ?? row, + columnSpan, + rowSpan, + } ); + if ( + ! rects.some( ( otherRect ) => + rect.intersectsRect( otherRect ) + ) + ) { + return { column, row }; + } + } + } + return null; +} + +function getRects( { innerBlocks, numColumns, numRows } ) { + const rects = []; + + for ( const block of innerBlocks ) { + const layout = block.attributes.style?.layout; + if ( layout.columnStart && layout.rowStart ) { + const rect = new GridRect( { + columnStart: layout.columnStart, + rowStart: layout.rowStart, + columnSpan: layout.columnSpan ?? 1, + rowSpan: layout.rowSpan ?? 1, + } ); + rects.push( rect ); + } + } + + for ( const block of innerBlocks ) { + const layout = block.attributes.style?.layout; + if ( ! layout.columnStart || ! layout.rowStart ) { + const naturalPosition = getNaturalPosition( { + rects, + numColumns, + numRows, + columnStart: layout.columnStart, + rowStart: layout.rowStart, + columnSpan: layout.columnSpan ?? 1, + rowSpan: layout.rowSpan ?? 1, + } ); + const rect = new GridRect( { + columnStart: naturalPosition.column, + rowStart: naturalPosition.row, + columnSpan: layout.columnSpan ?? 1, + rowSpan: layout.rowSpan ?? 1, + } ); + rects.push( rect ); + } + } + + return rects; +} + +function GridVisualizerGrid( { gridClientId, gridElement } ) { + const [ gridInfo, setGridInfo ] = useState( () => + getGridInfo( gridElement ) + ); + const [ isDroppingAllowed, setIsDroppingAllowed ] = useState( false ); + const [ highlightedRect, setHighlightedRect ] = useState( null ); + const [ insertion, setInsertion ] = useState( false ); + + const { getBlockAttributes, getBlocks } = useSelect( blockEditorStore ); + const { updateBlockAttributes, moveBlockToPosition } = + useDispatch( blockEditorStore ); + + useEffect( () => { + const observers = []; + for ( const element of [ gridElement, ...gridElement.children ] ) { + const observer = new window.ResizeObserver( () => { + setGridInfo( getGridInfo( gridElement ) ); + } ); + observer.observe( element ); + observers.push( observer ); + } + + let dragStartCell = null; + + function onMouseDown( event ) { + if ( event.target !== gridElement ) { + return; + } + const { column, row } = getGridCell( + gridElement, + event.offsetX, + event.offsetY + ); + if ( column && row ) { + dragStartCell = { + column, + row, + }; + } + } + function onMouseUp( event ) { + if ( event.target !== gridElement ) { + return; + } + const { column, row } = getGridCell( + gridElement, + event.offsetX, + event.offsetY + ); + if ( dragStartCell && column && row ) { + const rect = new GridRect( { + columnStart: Math.min( dragStartCell.column, column ), + rowStart: Math.min( dragStartCell.row, row ), + columnSpan: Math.abs( dragStartCell.column - column ) + 1, + rowSpan: Math.abs( dragStartCell.row - row ) + 1, + } ); + const anchor = { + getBoundingClientRect() { + return new window.DOMRect( + event.clientX, + event.clientY, + 0, + 0 + ); + }, + ownerDocument: event.target.ownerDocument, + }; + setInsertion( { rect, anchor } ); + } + dragStartCell = null; + } + function onMouseMove( event ) { + if ( event.target !== gridElement ) { + return; + } + if ( dragStartCell ) { + const { column, row } = getClosestGridCell( + gridElement, + event.offsetX, + event.offsetY + ); + setHighlightedRect( + column && row + ? new GridRect( { + columnStart: Math.min( + dragStartCell.column, + column + ), + rowStart: Math.min( dragStartCell.row, row ), + columnSpan: + Math.abs( dragStartCell.column - column ) + + 1, + rowSpan: + Math.abs( dragStartCell.row - row ) + 1, + } ) + : new GridRect( { + columnStart: dragStartCell.column, + rowStart: dragStartCell.row, + } ) + ); + } else { + const { column, row } = getGridCell( + gridElement, + event.offsetX, + event.offsetY + ); + setHighlightedRect( + column && row + ? new GridRect( { + columnStart: column, + rowStart: row, + } ) + : null + ); + } + } + gridElement.addEventListener( 'mousedown', onMouseDown ); + gridElement.addEventListener( 'mouseup', onMouseUp ); + gridElement.addEventListener( 'mousemove', onMouseMove ); + + return () => { + for ( const observer of observers ) { + observer.disconnect(); + } + + gridElement.removeEventListener( 'mousedown', onMouseDown ); + gridElement.removeEventListener( 'mouseup', onMouseUp ); + gridElement.removeEventListener( 'mousemove', onMouseMove ); + }; + }, [ gridElement ] ); + + useEffect( () => { + function onGlobalDrag() { + setIsDroppingAllowed( true ); + } + function onGlobalDragEnd() { + setIsDroppingAllowed( false ); + } + document.addEventListener( 'drag', onGlobalDrag ); + document.addEventListener( 'dragend', onGlobalDragEnd ); + return () => { + document.removeEventListener( 'drag', onGlobalDrag ); + document.removeEventListener( 'dragend', onGlobalDragEnd ); + }; + }, [] ); + + return ( + <> + { insertion && ( + + { + updateBlockAttributes( block.clientId, { + style: { + ...block.attributes.style, + layout: { + ...block.attributes.style?.layout, + columnStart: insertion.rect.columnStart, + rowStart: insertion.rect.rowStart, + columnSpan: insertion.rect.columnSpan, + rowSpan: insertion.rect.rowSpan, + }, + }, + } ); + setInsertion( null ); + } } + /> + + ) } + +
+ { range( 1, gridInfo.numRows ).map( ( row ) => + range( 1, gridInfo.numColumns ).map( ( column ) => ( + { + 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 ); + if ( ! isInBounds ) { + return false; + } + + const isOverlapping = Array.from( + gridElement.children + ).some( + ( child ) => + child.dataset.block !== + srcClientId && + rect.intersectsRect( + getGridItemRect( child ) + ) + ); + if ( isOverlapping ) { + return false; + } + + return true; + } } + 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 ) => { + // TODO: this is messy + + const attributes = + getBlockAttributes( srcClientId ); + + const blocks = getBlocks( + gridClientId + ).filter( + ( { clientId } ) => + clientId !== srcClientId + ); + const rects = getRects( { + innerBlocks: blocks, + numColumns: gridInfo.numColumns, + numRows: gridInfo.numRows, + } ); + const naturalPosition = getNaturalPosition( + { + rects, + numColumns: gridInfo.numColumns, + numRows: gridInfo.numRows, + columnSpan: + attributes.style?.layout + ?.columnSpan, + rowSpan: + attributes.style?.layout + ?.rowSpan, + } + ); + + if ( + column === naturalPosition?.column && + row === naturalPosition?.row + ) { + moveBlockToPosition( + srcClientId, + gridClientId, + gridClientId, + blocks.length + ); + if ( + attributes.style?.layout + ?.columnStart || + attributes.style?.layout?.rowStart + ) { + const { + columnStart, + rowStart, + ...layout + } = attributes.style.layout; + updateBlockAttributes( + srcClientId, + { + style: { + ...attributes.style, + layout, + }, + } + ); + } + } else { + updateBlockAttributes( srcClientId, { + style: { + ...attributes.style, + layout: { + ...attributes.style?.layout, + columnStart: column, + rowStart: row, + }, + }, + } ); + } + + setHighlightedRect( null ); + } } + /> + ) ) + ) } +
+
+ + ); +} + +function GridVisualizerCell( { + isHighlighted, + validateDrag, + onDragEnter, + onDragLeave, + onDrop, +} ) { + const { getDraggedBlockClientIds } = useSelect( blockEditorStore ); + + const ref = useDropZone( { + onDragEnter() { + const [ srcClientId ] = getDraggedBlockClientIds(); + if ( srcClientId && validateDrag( srcClientId ) ) { + onDragEnter( srcClientId ); + } + }, + onDragLeave() { + onDragLeave(); + }, + onDrop() { + const [ srcClientId ] = getDraggedBlockClientIds(); + if ( srcClientId && validateDrag( srcClientId ) ) { + onDrop( srcClientId ); + } + }, + } ); + + return ( +
+
+
+ ); +} diff --git a/packages/block-editor/src/components/grid-visualizer/index.js b/packages/block-editor/src/components/grid-interactivity/index.js similarity index 60% rename from packages/block-editor/src/components/grid-visualizer/index.js rename to packages/block-editor/src/components/grid-interactivity/index.js index add845d702203..663f89aac426c 100644 --- a/packages/block-editor/src/components/grid-visualizer/index.js +++ b/packages/block-editor/src/components/grid-interactivity/index.js @@ -1,2 +1,3 @@ export { GridVisualizer } from './grid-visualizer'; export { GridItemResizer } from './grid-item-resizer'; +export { GridItemPinToolbarItem } from './grid-item-pin-toolbar-item'; diff --git a/packages/block-editor/src/components/grid-interactivity/style.scss b/packages/block-editor/src/components/grid-interactivity/style.scss new file mode 100644 index 0000000000000..dfb3e57f84ae0 --- /dev/null +++ b/packages/block-editor/src/components/grid-interactivity/style.scss @@ -0,0 +1,63 @@ +.block-editor-grid-visualizer { + // 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__cell { + align-items: center; + display: flex; + justify-content: center; +} + +.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 { + // 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 { + // 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-interactivity/utils.js b/packages/block-editor/src/components/grid-interactivity/utils.js new file mode 100644 index 0000000000000..09d6cbe3fb45a --- /dev/null +++ b/packages/block-editor/src/components/grid-interactivity/utils.js @@ -0,0 +1,165 @@ +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 ); +} + +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 ); + const center = ( start + end ) / 2; + tracks.push( { start, end, center } ); + } + return tracks; +} + +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 getGridCell( gridElement, x, y ) { + 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 columnIndex = gridColumnTracks.findIndex( + ( track ) => x >= track.start && x < track.end + ); + const rowIndex = gridRowTracks.findIndex( + ( track ) => y >= track.start && y < track.end + ); + const column = columnIndex === -1 ? null : columnIndex + 1; + const row = rowIndex === -1 ? null : rowIndex + 1; + return { column, row }; +} + +export function getClosestGridCell( gridElement, x, y ) { + 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 columnIndex = getClosestTrack( gridColumnTracks, x, 'center' ); + const rowIndex = getClosestTrack( gridRowTracks, y, 'center' ); + const column = columnIndex === -1 ? null : columnIndex + 1; + const row = rowIndex === -1 ? null : rowIndex + 1; + return { column, row }; +} + +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 + ) + ); +} 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 deleted file mode 100644 index 54683e48beeea..0000000000000 --- a/packages/block-editor/src/components/grid-visualizer/grid-item-resizer.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * WordPress dependencies - */ -import { ResizableBox } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; -import BlockPopoverCover from '../block-popover/cover'; -import { getComputedCSS } from './utils'; - -export function GridItemResizer( { clientId, onChange } ) { - const blockElement = useBlockElement( clientId ); - if ( ! blockElement ) { - return null; - } - return ( - - { - const gridElement = blockElement.parentElement; - const columnGap = parseFloat( - getComputedCSS( gridElement, 'column-gap' ) - ); - const rowGap = parseFloat( - getComputedCSS( gridElement, 'row-gap' ) - ); - const gridColumnLines = getGridLines( - getComputedCSS( gridElement, 'grid-template-columns' ), - columnGap - ); - const gridRowLines = getGridLines( - getComputedCSS( gridElement, 'grid-template-rows' ), - rowGap - ); - const columnStart = getClosestLine( - gridColumnLines, - blockElement.offsetLeft - ); - const rowStart = getClosestLine( - gridRowLines, - blockElement.offsetTop - ); - const columnEnd = getClosestLine( - gridColumnLines, - blockElement.offsetLeft + boxElement.offsetWidth - ); - const rowEnd = getClosestLine( - gridRowLines, - blockElement.offsetTop + boxElement.offsetHeight - ); - onChange( { - columnSpan: Math.max( columnEnd - columnStart, 1 ), - rowSpan: Math.max( rowEnd - rowStart, 1 ), - } ); - } } - /> - - ); -} - -function getGridLines( template, gap ) { - const lines = [ 0 ]; - for ( const size of template.split( ' ' ) ) { - const line = parseFloat( size ); - lines.push( lines[ lines.length - 1 ] + line + gap ); - } - return lines; -} - -function getClosestLine( lines, position ) { - return lines.reduce( - ( closest, line, index ) => - Math.abs( line - position ) < - Math.abs( lines[ closest ] - 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 deleted file mode 100644 index 2ca65eb6722e4..0000000000000 --- a/packages/block-editor/src/components/grid-visualizer/grid-visualizer.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * WordPress dependencies - */ -import { useState, useEffect } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { __unstableUseBlockElement as useBlockElement } from '../block-list/use-block-props/use-block-refs'; -import BlockPopoverCover from '../block-popover/cover'; -import { getComputedCSS } from './utils'; - -export function GridVisualizer( { clientId } ) { - const blockElement = useBlockElement( clientId ); - if ( ! blockElement ) { - return null; - } - return ( - - - - ); -} - -function GridVisualizerGrid( { blockElement } ) { - 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(); - } - }; - }, [ blockElement ] ); - return ( -
- { Array.from( { length: gridInfo.numItems }, ( _, i ) => ( -
- ) ) } -
- ); -} - -function getGridInfo( blockElement ) { - const gridTemplateColumns = getComputedCSS( - blockElement, - 'grid-template-columns' - ); - const gridTemplateRows = getComputedCSS( - blockElement, - 'grid-template-rows' - ); - const numColumns = gridTemplateColumns.split( ' ' ).length; - const numRows = gridTemplateRows.split( ' ' ).length; - const numItems = numColumns * numRows; - return { - numItems, - style: { - gridTemplateColumns, - gridTemplateRows, - gap: getComputedCSS( blockElement, 'gap' ), - padding: getComputedCSS( blockElement, 'padding' ), - }, - }; -} diff --git a/packages/block-editor/src/components/grid-visualizer/style.scss b/packages/block-editor/src/components/grid-visualizer/style.scss deleted file mode 100644 index 45140e59c7af9..0000000000000 --- a/packages/block-editor/src/components/grid-visualizer/style.scss +++ /dev/null @@ -1,33 +0,0 @@ -// 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; -} - -.block-editor-grid-visualizer__grid { - display: grid; -} - -.block-editor-grid-visualizer__item { - border: $border-width dashed $gray-300; -} - -.block-editor-grid-item-resizer { - z-index: z-index(".block-editor-grid-visualizer") !important; -} - -.block-editor-grid-item-resizer .components-popover__content * { - pointer-events: none !important; -} - -.block-editor-grid-item-resizer__box { - border: $border-width solid var(--wp-admin-theme-color); - - .components-resizable-box__handle { - pointer-events: all !important; - } -} diff --git a/packages/block-editor/src/components/grid-visualizer/utils.js b/packages/block-editor/src/components/grid-visualizer/utils.js deleted file mode 100644 index a100e596a4e24..0000000000000 --- a/packages/block-editor/src/components/grid-visualizer/utils.js +++ /dev/null @@ -1,5 +0,0 @@ -export function getComputedCSS( element, property ) { - return element.ownerDocument.defaultView - .getComputedStyle( element ) - .getPropertyValue( property ); -} diff --git a/packages/block-editor/src/components/use-on-block-drop/index.js b/packages/block-editor/src/components/use-on-block-drop/index.js index 58ce615436d8e..0e9ad67204199 100644 --- a/packages/block-editor/src/components/use-on-block-drop/index.js +++ b/packages/block-editor/src/components/use-on-block-drop/index.js @@ -336,14 +336,40 @@ export default function useOnBlockDrop( const moveBlocks = useCallback( ( sourceClientIds, sourceRootClientId, insertIndex ) => { + const sourceBlocks = getBlocksByClientId( sourceClientIds ); + + // TODO: Figure out a better place for this. useOnBlockDrop shouldn't care about layout styles. + const unpinBlock = () => { + updateBlockAttributes( + sourceClientIds, + sourceBlocks.reduce( ( accumulator, sourceBlock ) => { + if ( + sourceBlock.attributes.style?.layout?.columnStart || + sourceBlock.attributes.style?.layout?.rowStart + ) { + const { columnStart, rowStart, ...layout } = + sourceBlock.attributes.style.layout; + accumulator[ sourceBlock.clientId ] = { + style: { + ...sourceBlock.attributes.style, + layout, + }, + }; + } + return accumulator; + }, {} ), + /* uniqueByBlock: */ true + ); + }; + if ( operation === 'replace' ) { - const sourceBlocks = getBlocksByClientId( sourceClientIds ); const targetBlockClientIds = getBlockOrder( targetRootClientId ); const targetBlockClientId = targetBlockClientIds[ targetBlockIndex ]; registry.batch( () => { + unpinBlock(); // Remove the source blocks. removeBlocks( sourceClientIds, false ); // Replace the target block with the source blocks. @@ -355,12 +381,15 @@ export default function useOnBlockDrop( ); } ); } else { - moveBlocksToPosition( - sourceClientIds, - sourceRootClientId, - targetRootClientId, - insertIndex - ); + registry.batch( () => { + unpinBlock(); + moveBlocksToPosition( + sourceClientIds, + sourceRootClientId, + targetRootClientId, + insertIndex + ); + } ); } }, [ diff --git a/packages/block-editor/src/hooks/layout-child.js b/packages/block-editor/src/hooks/layout-child.js index 4fa641bff203a..a5e9add7b3a46 100644 --- a/packages/block-editor/src/hooks/layout-child.js +++ b/packages/block-editor/src/hooks/layout-child.js @@ -10,7 +10,11 @@ import { useSelect } from '@wordpress/data'; 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, + GridItemPinToolbarItem, +} from '../components/grid-interactivity'; function useBlockPropsChildLayoutStyles( { style } ) { const shouldRenderChildLayoutStyles = useSelect( ( select ) => { @@ -141,29 +145,34 @@ function ChildLayoutControlsPure( { clientId, style, setAttributes } ) { }, [ clientId ] ); + if ( parentLayout.type !== 'grid' ) { return null; } if ( ! window.__experimentalEnableGridInteractivity ) { return null; } + + function updateLayout( layout ) { + setAttributes( { + style: { + ...style, + layout: { + ...style?.layout, + ...layout, + }, + }, + } ); + } + return ( <> - + { - setAttributes( { - style: { - ...style, - layout: { - ...style?.layout, - columnSpan, - rowSpan, - }, - }, - } ); - } } + layout={ style?.layout } + onChange={ updateLayout } /> ); diff --git a/packages/block-editor/src/layouts/grid.js b/packages/block-editor/src/layouts/grid.js index e71e1739447aa..0282c173a24cc 100644 --- a/packages/block-editor/src/layouts/grid.js +++ b/packages/block-editor/src/layouts/grid.js @@ -23,7 +23,7 @@ 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 } from '../components/grid-interactivity'; const RANGE_CONTROL_MAX_VALUES = { px: 600, @@ -68,7 +68,6 @@ export default { inspectorControls: function GridLayoutInspectorControls( { layout = {}, onChange, - clientId, layoutBlockSupport = {}, } ) { const { allowSizingOnChildren = false } = layoutBlockSupport; @@ -90,14 +89,14 @@ export default { onChange={ onChange } /> ) } - { window.__experimentalEnableGridInteractivity && ( - - ) } ); }, - toolBarControls: function GridLayoutToolbarControls() { - return null; + toolBarControls: function GridLayoutToolbarControls( { clientId } ) { + if ( ! window.__experimentalEnableGridInteractivity ) { + return null; + } + return ; }, getLayoutStyle: function getLayoutStyle( { selector, @@ -136,7 +135,7 @@ export default { } else if ( minimumColumnWidth ) { rules.push( `grid-template-columns: repeat(auto-fill, minmax(min(${ minimumColumnWidth }, 100%), 1fr))`, - `container-type: inline-size` + 'container-type: inline-size' ); } diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 13024d4d2e8fa..5766c096faf53 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -884,7 +884,7 @@ export const blocks = pipe( for ( const clientId of action.clientIds ) { const updatedAttributeEntries = Object.entries( action.uniqueByBlock - ? action.attributes[ clientId ] + ? action.attributes[ clientId ] ?? {} : action.attributes ?? {} ); if ( updatedAttributeEntries.length === 0 ) { diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 535d1005d274d..d2ed7b96dcefe 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -28,7 +28,7 @@ @import "./components/duotone-control/style.scss"; @import "./components/font-appearance-control/style.scss"; @import "./components/global-styles/style.scss"; -@import "./components/grid-visualizer/style.scss"; +@import "./components/grid-interactivity/style.scss"; @import "./components/height-control/style.scss"; @import "./components/image-size-control/style.scss"; @import "./components/inserter-list-item/style.scss"; diff --git a/packages/block-library/src/group/edit.js b/packages/block-library/src/group/edit.js index a763bc95e60d7..2b4db947da068 100644 --- a/packages/block-library/src/group/edit.js +++ b/packages/block-library/src/group/edit.js @@ -109,7 +109,7 @@ function GroupEdit( { attributes, name, setAttributes, clientId } ) { // Default to the regular appender being rendered. let renderAppender; - if ( showPlaceholder ) { + if ( showPlaceholder || type === 'grid' ) { // In the placeholder state, ensure the appender is not rendered. // This is needed because `...innerBlocksProps` is used in the placeholder // state so that blocks can dragged onto the placeholder area