Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove unwrap from transforms and add ungroup to more blocks #50385

Merged
merged 10 commits into from
May 19, 2023
30 changes: 26 additions & 4 deletions docs/reference-guides/block-api/block-transforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ When pasting content it's possible to define a [content model](https://html.spec
When writing `raw` transforms you can control this by supplying a `schema` which describes allowable content and which will be applied to clean up the pasted content before attempting to match with your block. The schemas are passed into [`cleanNodeList` from `@wordpress/dom`](https://github.com/wordpress/gutenberg/blob/trunk/packages/dom/src/dom/clean-node-list.js); check there for a [complete description of the schema](https://github.com/wordpress/gutenberg/blob/trunk/packages/dom/src/phrasing-content.js).

```js
schema = { span: { children: { '#text': {} } } }
schema = { span: { children: { '#text': {} } } };
```

**Example: a custom content model**
Expand All @@ -237,8 +237,8 @@ Suppose we want to match the following HTML snippet and turn it into some kind o

```html
<div data-post-id="13">
<h2>The Post Title</h2>
<p>Some <em>great</em> content.</p>
<h2>The Post Title</h2>
<p>Some <em>great</em> content.</p>
</div>
```

Expand Down Expand Up @@ -270,7 +270,7 @@ A transformation of type `shortcode` is an object that takes the following param

- **type** _(string)_: the value `shortcode`.
- **tag** _(string|array)_: the shortcode tag or list of shortcode aliases this transform can work with.
- **transform** _(function, optional): a callback that receives the shortcode attributes as the first argument and the [WPShortcodeMatch](/packages/shortcode/README.md#next) as the second. It should return a block object or an array of block objects. When this parameter is defined, it will take precedence over the `attributes` parameter.
- **transform** _(function, optional)_: a callback that receives the shortcode attributes as the first argument and the [WPShortcodeMatch](/packages/shortcode/README.md#next) as the second. It should return a block object or an array of block objects. When this parameter is defined, it will take precedence over the `attributes` parameter.
- **attributes** _(object, optional)_: object representing where the block attributes should be sourced from, according to the attributes shape defined by the [block configuration object](./block-registration.md). If a particular attribute contains a `shortcode` key, it should be a function that receives the shortcode attributes as the first arguments and the [WPShortcodeMatch](/packages/shortcode/README.md#next) as second, and returns a value for the attribute that will be sourced in the block's comment.
- **isMatch** _(function, optional)_: a callback that receives the shortcode attributes per the [Shortcode API](https://codex.wordpress.org/Shortcode_API) and should return a boolean. Returning `false` from this function will prevent the shortcode to be transformed into this block.
- **priority** _(number, optional)_: controls the priority with which a transform is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://codex.wordpress.org/Plugin_API#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set.
Expand Down Expand Up @@ -336,3 +336,25 @@ transforms: {
]
},
```

## `ungroup` blocks

Via the optional `transforms` key of the block configuration, blocks can use the `ungroup` subkey to define the blocks that will replace the block being processed. These new blocks will usually be a subset of the existing inner blocks, but could also include new blocks.

If a block has an `ungroup` transform, it is eligible for ungrouping, without the requirement of being the default grouping block. The UI used to ungroup a block with this API is the same as the one used for the default grouping block. In order for the Ungroup button to be displayed, we must have a single grouping block selected, which also contains some inner blocks.

**ungroup** is a callback function that receives the attributes and inner blocks of the block being processed. It should return an array of block objects.

Example:

```js
export const settings = {
title: 'My grouping Block Title',
description: 'My grouping block description',
/* ... */
transforms: {
ungroup: ( attributes, innerBlocks ) =>
innerBlocks.flatMap( ( innerBlock ) => innerBlock.innerBlocks ),
},
};
```
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ import { store as coreStore } from '@wordpress/core-data';
import { getMoversSetup } from '../block-mover/mover-description';
import { store as blockEditorStore } from '../../store';
import BlockTransformationsMenu from '../block-switcher/block-transformations-menu';
import {
useConvertToGroupButtons,
useConvertToGroupButtonProps,
} from '../convert-to-group-buttons';

const BlockActionsMenu = ( {
// Select.
Expand All @@ -55,6 +59,7 @@ const BlockActionsMenu = ( {
rootClientId,
selectedBlockClientId,
selectedBlockPossibleTransformations,
canRemove,
// Dispatch.
createSuccessNotice,
convertToRegularBlocks,
Expand Down Expand Up @@ -93,6 +98,17 @@ const BlockActionsMenu = ( {
},
} = getMoversSetup( isStackedHorizontally, moversOptions );

// Check if selected block is Groupable and/or Ungroupable.
const convertToGroupButtonProps = useConvertToGroupButtonProps( [
selectedBlockClientId,
] );
const { isGroupable, isUngroupable } = convertToGroupButtonProps;
const showConvertToGroupButton =
( isGroupable || isUngroupable ) && canRemove;
const convertToGroupButtons = useConvertToGroupButtons( {
...convertToGroupButtonProps,
} );

const allOptions = {
settings: {
id: 'settingsOption',
Expand Down Expand Up @@ -229,6 +245,10 @@ const BlockActionsMenu = ( {
canDuplicate && allOptions.cutButton,
canDuplicate && isPasteEnabled && allOptions.pasteButton,
canDuplicate && allOptions.duplicateButton,
showConvertToGroupButton && isGroupable && convertToGroupButtons.group,
showConvertToGroupButton &&
isUngroupable &&
convertToGroupButtons.ungroup,
isReusableBlockType &&
innerBlockCount > 0 &&
allOptions.convertToRegularBlocks,
Expand Down Expand Up @@ -327,6 +347,7 @@ export default compose(
getSelectedBlockClientIds,
canInsertBlockType,
getTemplateLock,
canRemoveBlock,
} = select( blockEditorStore );
const block = getBlock( clientId );
const blockName = getBlockName( clientId );
Expand Down Expand Up @@ -363,6 +384,7 @@ export default compose(
const selectedBlockPossibleTransformations = selectedBlock
? getBlockTransformItems( selectedBlock, rootClientId )
: EMPTY_BLOCK_LIST;
const canRemove = canRemoveBlock( selectedBlockClientId );

const isReusableBlockType = block ? isReusableBlock( block ) : false;
const reusableBlock = isReusableBlockType
Expand All @@ -388,6 +410,7 @@ export default compose(
rootClientId,
selectedBlockClientId,
selectedBlockPossibleTransformations,
canRemove,
};
} ),
withDispatch(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function ConvertToGroupButton( {
clientIds,
isGroupable,
isUngroupable,
onUngroup,
blocksSelection,
groupingBlockName,
onClose = () => {},
Expand All @@ -34,10 +35,16 @@ function ConvertToGroupButton( {
};

const onConvertFromGroup = () => {
const innerBlocks = blocksSelection[ 0 ].innerBlocks;
let innerBlocks = blocksSelection[ 0 ].innerBlocks;
if ( ! innerBlocks.length ) {
return;
}
if ( onUngroup ) {
innerBlocks = onUngroup(
blocksSelection[ 0 ].attributes,
blocksSelection[ 0 ].innerBlocks
);
}
replaceBlocks( clientIds, innerBlocks );
};

Expand Down Expand Up @@ -66,7 +73,7 @@ function ConvertToGroupButton( {
>
{ _x(
'Ungroup',
'Ungrouping blocks from within a Group block back into individual blocks within the Editor '
'Ungrouping blocks from within a grouping block back into individual blocks within the Editor '
) }
</MenuItem>
) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,79 @@
export default () => null;
/**
* WordPress dependencies
*/
import { __, _x } from '@wordpress/i18n';
import { switchToBlockType } from '@wordpress/blocks';
import { useDispatch } from '@wordpress/data';
import { store as noticesStore } from '@wordpress/notices';

/**
* Internal dependencies
*/
import { store as blockEditorStore } from '../../store';
import useConvertToGroupButtonProps from './use-convert-to-group-button-props';

function useConvertToGroupButtons( {
clientIds,
onUngroup,
blocksSelection,
groupingBlockName,
} ) {
const { replaceBlocks } = useDispatch( blockEditorStore );
const { createSuccessNotice } = useDispatch( noticesStore );
const onConvertToGroup = () => {
// Activate the `transform` on the Grouping Block which does the conversion.
const newBlocks = switchToBlockType(
blocksSelection,
groupingBlockName
);
if ( newBlocks ) {
replaceBlocks( clientIds, newBlocks );
}
};

const onConvertFromGroup = () => {
let innerBlocks = blocksSelection[ 0 ].innerBlocks;
if ( ! innerBlocks.length ) {
return;
}
if ( onUngroup ) {
innerBlocks = onUngroup(
blocksSelection[ 0 ].attributes,
blocksSelection[ 0 ].innerBlocks
);
}
replaceBlocks( clientIds, innerBlocks );
};

return {
group: {
id: 'groupButtonOption',
label: _x( 'Group', 'verb' ),
value: 'groupButtonOption',
onSelect: () => {
onConvertToGroup();
createSuccessNotice(
// translators: displayed right after the block is grouped
__( 'Block grouped' )
);
},
},
ungroup: {
id: 'ungroupButtonOption',
label: _x(
'Ungroup',
'Ungrouping blocks from within a grouping block back into individual blocks within the Editor'
),
value: 'ungroupButtonOption',
onSelect: () => {
onConvertFromGroup();
createSuccessNotice(
// translators: displayed right after the block is ungrouped.
__( 'Block ungrouped' )
);
},
},
};
}

export { useConvertToGroupButtons, useConvertToGroupButtonProps };
Original file line number Diff line number Diff line change
Expand Up @@ -31,68 +31,62 @@ import { store as blockEditorStore } from '../../store';
* @return {ConvertToGroupButtonProps} Returns the properties needed by `ConvertToGroupButton`.
*/
export default function useConvertToGroupButtonProps( selectedClientIds ) {
const {
clientIds,
isGroupable,
isUngroupable,
blocksSelection,
groupingBlockName,
} = useSelect(
return useSelect(
ntsekouras marked this conversation as resolved.
Show resolved Hide resolved
( select ) => {
const {
getBlockRootClientId,
getBlocksByClientId,
canInsertBlockType,
getSelectedBlockClientIds,
} = select( blockEditorStore );
const { getGroupingBlockName } = select( blocksStore );

const _clientIds = selectedClientIds?.length
const { getGroupingBlockName, getBlockType } =
select( blocksStore );
const clientIds = selectedClientIds?.length
? selectedClientIds
: getSelectedBlockClientIds();
const _groupingBlockName = getGroupingBlockName();
const groupingBlockName = getGroupingBlockName();

const rootClientId = !! _clientIds?.length
? getBlockRootClientId( _clientIds[ 0 ] )
const rootClientId = clientIds?.length
? getBlockRootClientId( clientIds[ 0 ] )
: undefined;

const groupingBlockAvailable = canInsertBlockType(
_groupingBlockName,
groupingBlockName,
rootClientId
);

const _blocksSelection = getBlocksByClientId( _clientIds );

const isSingleGroupingBlock =
_blocksSelection.length === 1 &&
_blocksSelection[ 0 ]?.name === _groupingBlockName;
const blocksSelection = getBlocksByClientId( clientIds );
const isSingleBlockSelected = blocksSelection.length === 1;
const [ firstSelectedBlock ] = blocksSelection;
// A block is ungroupable if it is a single grouping block with inner blocks.
// If a block has an `ungroup` transform, it is also ungroupable, without the
// requirement of being the default grouping block.
// Do we have a single grouping Block selected and does that group have inner blocks?
const isUngroupable =
isSingleBlockSelected &&
( firstSelectedBlock.name === groupingBlockName ||
getBlockType( firstSelectedBlock.name )?.transforms
?.ungroup ) &&
!! firstSelectedBlock.innerBlocks.length;

// Do we have
// 1. Grouping block available to be inserted?
// 2. One or more blocks selected
const _isGroupable =
groupingBlockAvailable && _blocksSelection.length;
const isGroupable =
groupingBlockAvailable && blocksSelection.length;

// Do we have a single Group Block selected and does that group have inner blocks?
const _isUngroupable =
isSingleGroupingBlock &&
!! _blocksSelection[ 0 ].innerBlocks.length;
return {
clientIds: _clientIds,
isGroupable: _isGroupable,
isUngroupable: _isUngroupable,
blocksSelection: _blocksSelection,
groupingBlockName: _groupingBlockName,
clientIds,
isGroupable,
isUngroupable,
blocksSelection,
groupingBlockName,
onUngroup:
isUngroupable &&
getBlockType( firstSelectedBlock.name )?.transforms
?.ungroup,
};
},
[ selectedClientIds ]
);

return {
clientIds,
isGroupable,
isUngroupable,
blocksSelection,
groupingBlockName,
};
}
16 changes: 1 addition & 15 deletions packages/block-editor/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
import { Platform } from '@wordpress/element';
import { applyFilters } from '@wordpress/hooks';
import { symbol } from '@wordpress/icons';
import { __ } from '@wordpress/i18n';
import { create, remove, toHTMLString } from '@wordpress/rich-text';
import deprecated from '@wordpress/deprecated';

Expand Down Expand Up @@ -2101,7 +2100,6 @@ export const getInserterItems = createSelector(
export const getBlockTransformItems = createSelector(
( state, blocks, rootClientId = null ) => {
const normalizedBlocks = Array.isArray( blocks ) ? blocks : [ blocks ];
const [ sourceBlock ] = normalizedBlocks;
const buildBlockTypeTransformItem = buildBlockTypeItem( state, {
buildScope: 'transform',
} );
Expand All @@ -2118,22 +2116,10 @@ export const getBlockTransformItems = createSelector(
] )
);

// Consider unwraping the highest priority.
itemsByName[ '*' ] = {
ntsekouras marked this conversation as resolved.
Show resolved Hide resolved
frecency: +Infinity,
id: '*',
isDisabled: false,
name: '*',
title: __( 'Unwrap' ),
icon: itemsByName[ sourceBlock?.name ]?.icon,
};

const possibleTransforms = getPossibleBlockTransformations(
normalizedBlocks
).reduce( ( accumulator, block ) => {
if ( block === '*' ) {
accumulator.push( itemsByName[ '*' ] );
} else if ( itemsByName[ block?.name ] ) {
if ( itemsByName[ block?.name ] ) {
accumulator.push( itemsByName[ block.name ] );
}
return accumulator;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ exports[`Columns block transforms to Group block 1`] = `
<!-- /wp:group -->"
`;

exports[`Columns block transforms unwraps content 1`] = `
exports[`Columns block transforms ungroups block 1`] = `
"<!-- wp:paragraph {"align":"left"} -->
<p class="has-text-align-left"><strong>Built with modern technology.</strong></p>
<!-- /wp:paragraph -->
Expand Down
Loading