Skip to content

Commit

Permalink
Remove unwrap from transforms and add ungroup to more blocks (#50385
Browse files Browse the repository at this point in the history
)

* Remove `unwrap` from transforms and add `ungroup` to more blocks

* add docs

* update e2e tests

* add e2e test

* Refactor useConvertToGroupButtonProps for readability

* Apply suggestions from code review

Co-authored-by: Miguel Fonseca <miguelcsf@gmail.com>

* update docs

* reword ungrouping in docs

* update e2e test

* [RNMobile] Add Group and Ungroup block actions (#50693)

* Add `useConvertToGroupButtons` hook

Most of the logic of this hook has been extracted from `ConvertToGroupButton` component. The main difference is that we return the configuration for the block actions instead of a component.

* Add Group and Ungroup options to block actions menu

* Remove `canUnwrap` option from `getBlockTransformOptions` test helper

* Update tests for Group, Quote and Columns blocks

---------

Co-authored-by: Miguel Fonseca <miguelcsf@gmail.com>
Co-authored-by: Carlos Garcia <fluiddot@gmail.com>
  • Loading branch information
3 people authored May 19, 2023
1 parent fc3c4f5 commit 7a93783
Show file tree
Hide file tree
Showing 19 changed files with 242 additions and 148 deletions.
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(
( 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[ '*' ] = {
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

0 comments on commit 7a93783

Please sign in to comment.