diff --git a/lib/compat.php b/lib/compat.php
new file mode 100644
index 00000000000000..fb09a6e49ef97f
--- /dev/null
+++ b/lib/compat.php
@@ -0,0 +1,26 @@
+ {
- const { verticalAlignment } = attributes;
+/**
+ * Internal dependencies
+ */
+import {
+ toWidthPrecision,
+ getTotalColumnsWidth,
+ getColumnWidths,
+ getAdjacentBlocks,
+ getRedistributedColumnWidths,
+} from '../columns/utils';
+
+function ColumnEdit( {
+ attributes,
+ updateAlignment,
+ updateWidth,
+ hasChildBlocks,
+} ) {
+ const { verticalAlignment, width } = attributes;
const classes = classnames( 'block-core-columns', {
[ `is-vertically-aligned-${ verticalAlignment }` ]: verticalAlignment,
} );
- const onChange = ( alignment ) => updateAlignment( alignment );
-
return (
+
);
diff --git a/packages/block-library/src/columns/edit.js b/packages/block-library/src/columns/edit.js
index 15adfff384dfff..71625625233815 100644
--- a/packages/block-library/src/columns/edit.js
+++ b/packages/block-library/src/columns/edit.js
@@ -2,12 +2,12 @@
* External dependencies
*/
import classnames from 'classnames';
+import { dropRight } from 'lodash';
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { compose } from '@wordpress/compose';
import {
PanelBody,
RangeControl,
@@ -18,12 +18,19 @@ import {
BlockControls,
BlockVerticalAlignmentToolbar,
} from '@wordpress/block-editor';
-import { withSelect, withDispatch } from '@wordpress/data';
+import { withDispatch } from '@wordpress/data';
+import { createBlock } from '@wordpress/blocks';
/**
* Internal dependencies
*/
-import { getColumnsTemplate } from './utils';
+import {
+ getColumnsTemplate,
+ hasExplicitColumnWidths,
+ getMappedColumnWidths,
+ getRedistributedColumnWidths,
+ toWidthPrecision,
+} from './utils';
/**
* Allowed blocks constant is passed to InnerBlocks precisely as specified here.
@@ -36,18 +43,18 @@ import { getColumnsTemplate } from './utils';
*/
const ALLOWED_BLOCKS = [ 'core/column' ];
-export const ColumnsEdit = function( { attributes, setAttributes, className, updateAlignment } ) {
+export function ColumnsEdit( {
+ attributes,
+ className,
+ updateAlignment,
+ updateColumns,
+} ) {
const { columns, verticalAlignment } = attributes;
const classes = classnames( className, `has-${ columns }-columns`, {
[ `are-vertically-aligned-${ verticalAlignment }` ]: verticalAlignment,
} );
- const onChange = ( alignment ) => {
- // Update all the (immediate) child Column Blocks
- updateAlignment( alignment );
- };
-
return (
<>
@@ -55,11 +62,7 @@ export const ColumnsEdit = function( { attributes, setAttributes, className, upd
{
- setAttributes( {
- columns: nextColumns,
- } );
- } }
+ onChange={ updateColumns }
min={ 2 }
max={ 6 }
/>
@@ -67,7 +70,7 @@ export const ColumnsEdit = function( { attributes, setAttributes, className, upd
@@ -79,45 +82,81 @@ export const ColumnsEdit = function( { attributes, setAttributes, className, upd
>
);
-};
+}
+
+export default withDispatch( ( dispatch, ownProps, registry ) => ( {
+ /**
+ * Update all child Column blocks with a new vertical alignment setting
+ * based on whatever alignment is passed in. This allows change to parent
+ * to overide anything set on a individual column basis.
+ *
+ * @param {string} verticalAlignment the vertical alignment setting
+ */
+ updateAlignment( verticalAlignment ) {
+ const { clientId, setAttributes } = ownProps;
+ const { updateBlockAttributes } = dispatch( 'core/block-editor' );
+ const { getBlockOrder } = registry.select( 'core/block-editor' );
+
+ // Update own alignment.
+ setAttributes( { verticalAlignment } );
-const DEFAULT_EMPTY_ARRAY = [];
+ // Update all child Column Blocks to match
+ const innerBlockClientIds = getBlockOrder( clientId );
+ innerBlockClientIds.forEach( ( innerBlockClientId ) => {
+ updateBlockAttributes( innerBlockClientId, {
+ verticalAlignment,
+ } );
+ } );
+ },
-export default compose(
/**
- * Selects the child column Blocks for this parent Column
+ * Updates the column count, including necessary revisions to child Column
+ * blocks to grant required or redistribute available space.
+ *
+ * @param {number} columns New column count.
*/
- withSelect( ( select, { clientId } ) => {
- const { getBlocksByClientId } = select( 'core/editor' );
- const block = getBlocksByClientId( clientId )[ 0 ];
-
- return {
- childColumns: block ? block.innerBlocks : DEFAULT_EMPTY_ARRAY,
- };
- } ),
- withDispatch( ( dispatch, { clientId, childColumns } ) => {
- return {
- /**
- * Update all child column Blocks with a new
- * vertical alignment setting based on whatever
- * alignment is passed in. This allows change to parent
- * to overide anything set on a individual column basis
- *
- * @param {string} alignment the vertical alignment setting
- */
- updateAlignment( alignment ) {
- // Update self...
- dispatch( 'core/editor' ).updateBlockAttributes( clientId, {
- verticalAlignment: alignment,
- } );
-
- // Update all child Column Blocks to match
- childColumns.forEach( ( childColumn ) => {
- dispatch( 'core/editor' ).updateBlockAttributes( childColumn.clientId, {
- verticalAlignment: alignment,
- } );
- } );
- },
- };
- } ),
-)( ColumnsEdit );
+ updateColumns( columns ) {
+ const { clientId, setAttributes, attributes } = ownProps;
+ const { replaceInnerBlocks } = dispatch( 'core/block-editor' );
+ const { getBlocks } = registry.select( 'core/block-editor' );
+
+ // Update columns count.
+ setAttributes( { columns } );
+
+ let innerBlocks = getBlocks( clientId );
+ if ( ! hasExplicitColumnWidths( innerBlocks ) ) {
+ return;
+ }
+
+ // Redistribute available width for existing inner blocks.
+ const { columns: previousColumns } = attributes;
+ const isAddingColumn = columns > previousColumns;
+
+ if ( isAddingColumn ) {
+ // If adding a new column, assign width to the new column equal to
+ // as if it were `1 / columns` of the total available space.
+ const newColumnWidth = toWidthPrecision( 100 / columns );
+
+ // Redistribute in consideration of pending block insertion as
+ // constraining the available working width.
+ const widths = getRedistributedColumnWidths( innerBlocks, 100 - newColumnWidth );
+
+ innerBlocks = [
+ ...getMappedColumnWidths( innerBlocks, widths ),
+ createBlock( 'core/column', {
+ width: newColumnWidth,
+ } ),
+ ];
+ } else {
+ // The removed column will be the last of the inner blocks.
+ innerBlocks = dropRight( innerBlocks );
+
+ // Redistribute as if block is already removed.
+ const widths = getRedistributedColumnWidths( innerBlocks, 100 );
+
+ innerBlocks = getMappedColumnWidths( innerBlocks, widths );
+ }
+
+ replaceInnerBlocks( clientId, innerBlocks, false );
+ },
+} ) )( ColumnsEdit );
diff --git a/packages/block-library/src/columns/editor.scss b/packages/block-library/src/columns/editor.scss
index 7830f7780880a9..c771c79f5f9d3c 100644
--- a/packages/block-library/src/columns/editor.scss
+++ b/packages/block-library/src/columns/editor.scss
@@ -178,14 +178,3 @@ div.block-core-columns.is-vertically-aligned-bottom {
display: none;
}
}
-
-// In absence of making the individual columns resizable, we prevent them from being clickable.
-// This makes them less fiddly. @todo: This should be revisited as the interface is refined.
-.wp-block-columns [data-type="core/column"] {
- pointer-events: none;
-
- // This selector re-enables clicking on any child of a column block.
- .block-core-columns .block-editor-block-list__layout {
- pointer-events: all;
- }
-}
diff --git a/packages/block-library/src/columns/style.scss b/packages/block-library/src/columns/style.scss
index ab2ec375ca1e8e..7a2c0877889332 100644
--- a/packages/block-library/src/columns/style.scss
+++ b/packages/block-library/src/columns/style.scss
@@ -14,8 +14,11 @@
margin-bottom: 1em;
flex-grow: 1;
- // Responsiveness: Show at most one columns on mobile.
- flex-basis: 100%;
+ @media (max-width: #{ ($break-small - 1) }) {
+ // Responsiveness: Show at most one columns on mobile. This must be
+ // important since the Column assigns its own width as an inline style.
+ flex-basis: 100% !important;
+ }
// Prevent the columns from growing wider than their distributed sizes.
min-width: 0;
@@ -30,7 +33,7 @@
flex-basis: calc(50% - #{$grid-size-large});
flex-grow: 0;
- // Add space between the 2 columns. Themes can customize this if they wish to work differently.
+ // Add space between the multiple columns. Themes can customize this if they wish to work differently.
// Only apply this beyond the mobile breakpoint, as there's only a single column on mobile.
&:nth-child(even) {
margin-left: $grid-size-large * 2;
diff --git a/packages/block-library/src/columns/test/utils.js b/packages/block-library/src/columns/test/utils.js
new file mode 100644
index 00000000000000..cb69e1740e1f32
--- /dev/null
+++ b/packages/block-library/src/columns/test/utils.js
@@ -0,0 +1,229 @@
+/**
+ * Internal dependencies
+ */
+import {
+ getColumnsTemplate,
+ toWidthPrecision,
+ getAdjacentBlocks,
+ getEffectiveColumnWidth,
+ getTotalColumnsWidth,
+ getColumnWidths,
+ getRedistributedColumnWidths,
+ hasExplicitColumnWidths,
+ getMappedColumnWidths,
+} from '../utils';
+
+describe( 'getColumnsTemplate', () => {
+ it( 'should return a template corresponding to columns count', () => {
+ const template = getColumnsTemplate( 4 );
+
+ expect( template ).toEqual( [
+ [ 'core/column' ],
+ [ 'core/column' ],
+ [ 'core/column' ],
+ [ 'core/column' ],
+ ] );
+ } );
+} );
+
+describe( 'toWidthPrecision', () => {
+ it( 'should round value to standard precision', () => {
+ const value = toWidthPrecision( 50.108 );
+
+ expect( value ).toBe( 50.11 );
+ } );
+
+ it( 'should return undefined for invalid number', () => {
+ expect( toWidthPrecision( null ) ).toBe( undefined );
+ expect( toWidthPrecision( undefined ) ).toBe( undefined );
+ } );
+} );
+
+describe( 'getAdjacentBlocks', () => {
+ const blockA = { clientId: 'a' };
+ const blockB = { clientId: 'b' };
+ const blockC = { clientId: 'c' };
+ const blocks = [ blockA, blockB, blockC ];
+
+ it( 'should return blocks after clientId', () => {
+ const result = getAdjacentBlocks( blocks, 'b' );
+
+ expect( result ).toEqual( [ blockC ] );
+ } );
+
+ it( 'should return blocks before clientId if clientId is last', () => {
+ const result = getAdjacentBlocks( blocks, 'c' );
+
+ expect( result ).toEqual( [ blockA, blockB ] );
+ } );
+} );
+
+describe( 'getEffectiveColumnWidth', () => {
+ it( 'should return attribute value if set, rounded to precision', () => {
+ const block = { attributes: { width: 50.108 } };
+
+ const width = getEffectiveColumnWidth( block, 3 );
+
+ expect( width ).toBe( 50.11 );
+ } );
+
+ it( 'should return assumed width if attribute value not set, rounded to precision', () => {
+ const block = { attributes: {} };
+
+ const width = getEffectiveColumnWidth( block, 3 );
+
+ expect( width ).toBe( 33.33 );
+ } );
+} );
+
+describe( 'getTotalColumnsWidth', () => {
+ describe( 'explicit width', () => {
+ const blocks = [
+ { clientId: 'a', attributes: { width: 30 } },
+ { clientId: 'b', attributes: { width: 40 } },
+ ];
+
+ it( 'returns the sum total of columns width', () => {
+ const width = getTotalColumnsWidth( blocks );
+
+ expect( width ).toBe( 70 );
+ } );
+ } );
+
+ describe( 'implicit width', () => {
+ const blocks = [
+ { clientId: 'a', attributes: {} },
+ { clientId: 'b', attributes: {} },
+ ];
+
+ it( 'returns the sum total of columns width', () => {
+ const widths = getTotalColumnsWidth( blocks );
+
+ expect( widths ).toBe( 100 );
+ } );
+ } );
+} );
+
+describe( 'getColumnWidths', () => {
+ describe( 'explicit width', () => {
+ const blocks = [
+ { clientId: 'a', attributes: { width: 30.459 } },
+ { clientId: 'b', attributes: { width: 29.543 } },
+ ];
+
+ it( 'returns the column widths', () => {
+ const widths = getColumnWidths( blocks );
+
+ expect( widths ).toEqual( {
+ a: 30.46,
+ b: 29.54,
+ } );
+ } );
+ } );
+
+ describe( 'implicit width', () => {
+ const blocks = [
+ { clientId: 'a', attributes: {} },
+ { clientId: 'b', attributes: {} },
+ ];
+
+ it( 'returns the column widths', () => {
+ const widths = getColumnWidths( blocks );
+
+ expect( widths ).toEqual( {
+ a: 50,
+ b: 50,
+ } );
+ } );
+ } );
+} );
+
+describe( 'getRedistributedColumnWidths', () => {
+ describe( 'explicit width', () => {
+ const blocks = [
+ { clientId: 'a', attributes: { width: 30 } },
+ { clientId: 'b', attributes: { width: 40 } },
+ ];
+
+ it( 'should constrain to fit available width', () => {
+ const widths = getRedistributedColumnWidths( blocks, 60 );
+
+ expect( widths ).toEqual( {
+ a: 25,
+ b: 35,
+ } );
+ } );
+
+ it( 'should expand to fit available width', () => {
+ const widths = getRedistributedColumnWidths( blocks, 80 );
+
+ expect( widths ).toEqual( {
+ a: 35,
+ b: 45,
+ } );
+ } );
+ } );
+
+ describe( 'implicit width', () => {
+ const blocks = [
+ { clientId: 'a', attributes: {} },
+ { clientId: 'b', attributes: {} },
+ ];
+
+ it( 'should equally distribute to available width', () => {
+ const widths = getRedistributedColumnWidths( blocks, 60 );
+
+ expect( widths ).toEqual( {
+ a: 30,
+ b: 30,
+ } );
+ } );
+
+ it( 'should constrain to fit available width', () => {
+ const widths = getRedistributedColumnWidths( blocks, 66.66, 3 );
+
+ expect( widths ).toEqual( {
+ a: 33.33,
+ b: 33.33,
+ } );
+ } );
+ } );
+} );
+
+describe( 'hasExplicitColumnWidths', () => {
+ it( 'returns false if no blocks have explicit width', () => {
+ const blocks = [ { attributes: {} } ];
+
+ const result = hasExplicitColumnWidths( blocks );
+
+ expect( result ).toBe( false );
+ } );
+
+ it( 'returns true if a block has explicit width', () => {
+ const blocks = [ { attributes: { width: 10 } } ];
+
+ const result = hasExplicitColumnWidths( blocks );
+
+ expect( result ).toBe( true );
+ } );
+} );
+
+describe( 'getMappedColumnWidths', () => {
+ it( 'merges to block attributes using provided widths', () => {
+ const blocks = [
+ { clientId: 'a', attributes: { width: 30 } },
+ { clientId: 'b', attributes: { width: 40 } },
+ ];
+ const widths = {
+ a: 25,
+ b: 35,
+ };
+
+ const result = getMappedColumnWidths( blocks, widths );
+
+ expect( result ).toEqual( [
+ { clientId: 'a', attributes: { width: 25 } },
+ { clientId: 'b', attributes: { width: 35 } },
+ ] );
+ } );
+} );
diff --git a/packages/block-library/src/columns/utils.js b/packages/block-library/src/columns/utils.js
index e7e3f90df70fd2..0c4e4c59e9ccd3 100644
--- a/packages/block-library/src/columns/utils.js
+++ b/packages/block-library/src/columns/utils.js
@@ -2,7 +2,7 @@
* External dependencies
*/
import memoize from 'memize';
-import { times } from 'lodash';
+import { times, findIndex, sumBy, merge, mapValues } from 'lodash';
/**
* Returns the layouts configuration for a given number of columns.
@@ -14,3 +14,129 @@ import { times } from 'lodash';
export const getColumnsTemplate = memoize( ( columns ) => {
return times( columns, () => [ 'core/column' ] );
} );
+
+/**
+ * Returns a column width attribute value rounded to standard precision.
+ * Returns `undefined` if the value is not a valid finite number.
+ *
+ * @param {?number} value Raw value.
+ *
+ * @return {number} Value rounded to standard precision.
+ */
+export const toWidthPrecision = ( value ) =>
+ Number.isFinite( value ) ?
+ parseFloat( value.toFixed( 2 ) ) :
+ undefined;
+
+/**
+ * Returns the considered adjacent to that of the specified `clientId` for
+ * resizing consideration. Adjacent blocks are those occurring after, except
+ * when the given block is the last block in the set. For the last block, the
+ * behavior is reversed.
+ *
+ * @param {WPBlock[]} blocks Block objects.
+ * @param {string} clientId Client ID to consider for adjacent blocks.
+ *
+ * @return {WPBlock[]} Adjacent block objects.
+ */
+export function getAdjacentBlocks( blocks, clientId ) {
+ const index = findIndex( blocks, { clientId } );
+ const isLastBlock = index === blocks.length - 1;
+
+ return isLastBlock ? blocks.slice( 0, index ) : blocks.slice( index + 1 );
+}
+
+/**
+ * Returns an effective width for a given block. An effective width is equal to
+ * its attribute value if set, or a computed value assuming equal distribution.
+ *
+ * @param {WPBlock} block Block object.
+ * @param {number} totalBlockCount Total number of blocks in Columns.
+ *
+ * @return {number} Effective column width.
+ */
+export function getEffectiveColumnWidth( block, totalBlockCount ) {
+ const { width = 100 / totalBlockCount } = block.attributes;
+ return toWidthPrecision( width );
+}
+
+/**
+ * Returns the total width occupied by the given set of column blocks.
+ *
+ * @param {WPBlock[]} blocks Block objects.
+ * @param {?number} totalBlockCount Total number of blocks in Columns.
+ * Defaults to number of blocks passed.
+ *
+ * @return {number} Total width occupied by blocks.
+ */
+export function getTotalColumnsWidth( blocks, totalBlockCount = blocks.length ) {
+ return sumBy( blocks, ( block ) => getEffectiveColumnWidth( block, totalBlockCount ) );
+}
+
+/**
+ * Returns an object of `clientId` → `width` of effective column widths.
+ *
+ * @param {WPBlock[]} blocks Block objects.
+ * @param {?number} totalBlockCount Total number of blocks in Columns.
+ * Defaults to number of blocks passed.
+ *
+ * @return {Object