diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index b97cb649bb0bc..f5d40ae8a2110 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -22,6 +22,9 @@ function gutenberg_enable_experiments() { if ( $gutenberg_experiments && array_key_exists( 'gutenberg-color-randomizer', $gutenberg_experiments ) ) { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableColorRandomizer = true', 'before' ); } + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-grid-interactivity', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableGridInteractivity = true', 'before' ); + } if ( gutenberg_is_experiment_enabled( 'gutenberg-no-tinymce' ) ) { wp_add_inline_script( 'wp-block-library', 'window.__experimentalDisableTinymce = true', 'before' ); } diff --git a/lib/experiments-page.php b/lib/experiments-page.php index 6608fbb138c58..f66e093219263 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -90,6 +90,7 @@ function gutenberg_initialize_experiments_settings() { 'id' => 'gutenberg-color-randomizer', ) ); + add_settings_field( 'gutenberg-form-blocks', __( 'Form and input blocks ', 'gutenberg' ), @@ -101,6 +102,19 @@ function gutenberg_initialize_experiments_settings() { 'id' => 'gutenberg-form-blocks', ) ); + + add_settings_field( + 'gutenberg-grid-interactivity', + __( 'Grid interactivty ', 'gutenberg' ), + 'gutenberg_display_experiment_field', + 'gutenberg-experiments', + 'gutenberg_experiments_section', + array( + 'label' => __( 'Test enhancements to the Grid block that let you move and resize items in the editor canvas.', 'gutenberg' ), + 'id' => 'gutenberg-grid-interactivity', + ) + ); + add_settings_field( 'gutenberg-no-tinymce', __( 'Disable TinyMCE and Classic block', 'gutenberg' ), diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index ff21d1d8df8f3..762508f921a00 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -108,6 +108,9 @@ $z-layers: ( // Above the block list and the header. ".block-editor-block-popover": 31, + // Below the block toolbar. + ".block-editor-grid-visualizer": 30, + // Show snackbars above everything (similar to popovers) ".components-snackbar-list": 100000, 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 new file mode 100644 index 0000000000000..54683e48beeea --- /dev/null +++ b/packages/block-editor/src/components/grid-visualizer/grid-item-resizer.js @@ -0,0 +1,100 @@ +/** + * 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 new file mode 100644 index 0000000000000..2ca65eb6722e4 --- /dev/null +++ b/packages/block-editor/src/components/grid-visualizer/grid-visualizer.js @@ -0,0 +1,81 @@ +/** + * 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/index.js b/packages/block-editor/src/components/grid-visualizer/index.js new file mode 100644 index 0000000000000..add845d702203 --- /dev/null +++ b/packages/block-editor/src/components/grid-visualizer/index.js @@ -0,0 +1,2 @@ +export { GridVisualizer } from './grid-visualizer'; +export { GridItemResizer } from './grid-item-resizer'; diff --git a/packages/block-editor/src/components/grid-visualizer/style.scss b/packages/block-editor/src/components/grid-visualizer/style.scss new file mode 100644 index 0000000000000..45140e59c7af9 --- /dev/null +++ b/packages/block-editor/src/components/grid-visualizer/style.scss @@ -0,0 +1,33 @@ +// 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 new file mode 100644 index 0000000000000..a100e596a4e24 --- /dev/null +++ b/packages/block-editor/src/components/grid-visualizer/utils.js @@ -0,0 +1,5 @@ +export function getComputedCSS( element, property ) { + return element.ownerDocument.defaultView + .getComputedStyle( element ) + .getPropertyValue( property ); +} diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index e6227ea2b03e2..36efe3dcf409b 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -42,6 +42,7 @@ createBlockEditFilter( contentLockUI, blockHooks, blockRenaming, + childLayout, ].filter( Boolean ) ); createBlockListBlockFilter( [ diff --git a/packages/block-editor/src/hooks/layout-child.js b/packages/block-editor/src/hooks/layout-child.js index a9a6ff4db0f4a..d8333e8e0e830 100644 --- a/packages/block-editor/src/hooks/layout-child.js +++ b/packages/block-editor/src/hooks/layout-child.js @@ -10,6 +10,7 @@ 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'; function useBlockPropsChildLayoutStyles( { style } ) { const shouldRenderChildLayoutStyles = useSelect( ( select ) => { @@ -96,8 +97,45 @@ function useBlockPropsChildLayoutStyles( { style } ) { return { className: `wp-container-content-${ id }` }; } +function ChildLayoutControlsPure( { clientId, style, setAttributes } ) { + const parentLayout = useLayout() || {}; + const rootClientId = useSelect( + ( select ) => { + return select( blockEditorStore ).getBlockRootClientId( clientId ); + }, + [ clientId ] + ); + if ( parentLayout.type !== 'grid' ) { + return null; + } + if ( ! window.__experimentalEnableGridInteractivity ) { + return null; + } + return ( + <> + + { + setAttributes( { + style: { + ...style, + layout: { + ...style?.layout, + columnSpan, + rowSpan, + }, + }, + } ); + } } + /> + + ); +} + export default { useBlockProps: useBlockPropsChildLayoutStyles, + edit: ChildLayoutControlsPure, attributeKeys: [ 'style' ], hasSupport() { return true; diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index a83d07398d54a..76a5557850a60 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -135,7 +135,12 @@ export function useLayoutStyles( blockAttributes = {}, blockName, selector ) { return css; } -function LayoutPanelPure( { layout, setAttributes, name: blockName } ) { +function LayoutPanelPure( { + layout, + setAttributes, + name: blockName, + clientId, +} ) { const settings = useBlockSettings( blockName ); // Block settings come from theme.json under settings.[blockName]. const { layout: layoutSettings } = settings; @@ -266,6 +271,8 @@ function LayoutPanelPure( { layout, setAttributes, name: blockName } ) { layout={ usedLayout } onChange={ onChangeLayout } layoutBlockSupport={ blockSupportAndThemeSettings } + name={ blockName } + clientId={ clientId } /> ) } { constrainedType && displayControlsForLegacyLayouts && ( @@ -273,6 +280,8 @@ function LayoutPanelPure( { layout, setAttributes, name: blockName } ) { layout={ usedLayout } onChange={ onChangeLayout } layoutBlockSupport={ blockSupportAndThemeSettings } + name={ blockName } + clientId={ clientId } /> ) } @@ -282,6 +291,8 @@ function LayoutPanelPure( { layout, setAttributes, name: blockName } ) { layout={ usedLayout } onChange={ onChangeLayout } layoutBlockSupport={ layoutBlockSupport } + name={ blockName } + clientId={ clientId } /> ) } diff --git a/packages/block-editor/src/layouts/grid.js b/packages/block-editor/src/layouts/grid.js index a27d07b3854a2..0dc72694bd568 100644 --- a/packages/block-editor/src/layouts/grid.js +++ b/packages/block-editor/src/layouts/grid.js @@ -23,6 +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'; const RANGE_CONTROL_MAX_VALUES = { px: 600, @@ -67,6 +68,7 @@ export default { inspectorControls: function GridLayoutInspectorControls( { layout = {}, onChange, + clientId, } ) { return ( <> @@ -85,10 +87,13 @@ export default { onChange={ onChange } /> ) } + { window.__experimentalEnableGridInteractivity && ( + + ) } ); }, - toolBarControls: function DefaultLayoutToolbarControls() { + toolBarControls: function GridLayoutToolbarControls() { return null; }, getLayoutStyle: function getLayoutStyle( { diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 01931adace3f1..d7aa3ebcc12d0 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -27,6 +27,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/height-control/style.scss"; @import "./components/image-size-control/style.scss"; @import "./components/inserter-list-item/style.scss";