Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 38 additions & 12 deletions docs/reference-guides/block-api/block-transforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,21 @@ A transformation of type `block` is an object that takes the following parameter
- **isMatch** _(function, optional)_: a callback that receives the block attributes as the first argument and the block object as the second argument and should return a boolean. Returning `false` from this function will prevent the transform from being available and displayed as an option to the user.
- **isMultiBlock** _(boolean, optional)_: whether the transformation can be applied when multiple blocks are selected. If true, the `transform` function's first parameter will be an array containing each selected block's attributes, and the second an array of each selected block's inner blocks. False by default.
- **priority** _(number, optional)_: controls the priority with which a transformation is applied, where a lower value will take precedence over higher values. This behaves much like a [WordPress hook](https://developer.wordpress.org/reference/#Hook_to_WordPress). Like hooks, the default priority is `10` when not otherwise set.
- **shortcut** _(object, optional)_: a keyboard shortcut that applies the transform to the selected block. See [Keyboard shortcuts](#keyboard-shortcuts) below.
- **shortcuts** _(array, optional)_: keyboard shortcuts that apply the transform to the selected block. See [Keyboard shortcuts](#keyboard-shortcuts) below.

#### Keyboard shortcuts

A transform of type `block` can declare a keyboard shortcut, which is registered with the editor's keyboard shortcuts store and listed in the keyboard shortcuts help modal. Pressing the key combination transforms the selected block, as long as the block is fully editable.
A transform of type `block` can declare keyboard shortcuts, which are registered with the editor's keyboard shortcuts store and listed in the keyboard shortcuts help modal. Pressing the key combination transforms the selected block, as long as the block is fully editable.

The `shortcut` object takes the following parameters:
Each entry of `shortcuts` takes the following parameters:

- **name** _(string)_: a unique and machine-readable shortcut name, e.g. `core/block-editor/transform-heading-to-paragraph`.
- **description** _(string)_: a translated description, displayed in the keyboard shortcuts help modal.
- **keyCombination** _(object)_: the key combination that triggers the shortcut, as a `character` and an optional `modifier` (one of the modifiers supported by the [`wp-keycodes` package](/packages/keycodes/README.md), e.g. `access` or `primary`).
- **aliases** _(array, optional)_: alternative key combinations that trigger the same shortcut.
- **variationName** _(string, optional)_: the variation of the produced block the shortcut targets, defaulting to the transform's own `variationName`. Declaring it per shortcut is what lets a single transform offer one shortcut per variation without appearing more than once in the block switcher.

On a `to` transform the shortcut applies to the block declaring it, and produces the first entry of `blocks`. On a `from` transform it applies to any block listed in `blocks`, and produces the block declaring it.
On a `to` transform the shortcuts apply to the block declaring them, and produce the first entry of `blocks`. On a `from` transform they apply to any block listed in `blocks`, and produce the block declaring them.

**Example: transform a Heading block into a Paragraph block with <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>0</kbd>**

Expand All @@ -69,13 +70,15 @@ transforms: {
{
type: 'block',
blocks: [ 'core/paragraph' ],
shortcut: {
name: 'core/block-editor/transform-heading-to-paragraph',
description: __(
'Transform the selected heading into a paragraph.'
),
keyCombination: { modifier: 'access', character: '0' },
},
shortcuts: [
{
name: 'core/block-editor/transform-heading-to-paragraph',
description: __(
'Transform the selected heading into a paragraph.'
),
keyCombination: { modifier: 'access', character: '0' },
},
],
transform: ( { content } ) => {
return createBlock( 'core/paragraph', { content } );
},
Expand All @@ -84,9 +87,32 @@ transforms: {
},
```

**Example: transform any block into a specific heading level**

The Heading block hangs one shortcut per level on its single transform from Paragraph, so that <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>2</kbd> on a paragraph produces an H2 while the block switcher still offers "Heading" once:

```js
transforms: {
from: [
{
type: 'block',
blocks: [ 'core/paragraph' ],
shortcuts: [ 1, 2, 3, 4, 5, 6 ].map( ( level ) => ( {
name: `core/block-editor/transform-to-heading-${ level }`,
description: /* … */,
keyCombination: { modifier: 'access', character: `${ level }` },
variationName: `h${ level }`,
} ) ),
transform: ( { content } ) =>
createBlock( 'core/heading', { content } ),
},
],
},
```

Key combinations are matched globally, so a combination already claimed by another block or by the editor itself will not reliably reach your transform. In development builds a warning is logged when two blocks declare the same combination.

To apply a variation of a block type rather than the block type itself, declare the shortcut on the [block variation](/docs/reference-guides/block-api/block-variations.md#using-shortcut) instead.
To switch between variations of a block that is *already* of the target type, declare the shortcut on the [block variation](/docs/reference-guides/block-api/block-variations.md#using-shortcut) instead. A shortcut that should do both — reach the variation from another block type and switch to it within the type — is declared in both places under the same `name`.

**Example: from Paragraph block to Heading block**

Expand Down
4 changes: 3 additions & 1 deletion docs/reference-guides/block-api/block-variations.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,9 @@ wp.blocks.registerBlockVariation( 'core/heading', {
} );
```

The shortcut is not limited to blocks of the variation's own block type. When the selected block is already of that type, the variation's `attributes` are applied to it — unless `isActive` reports the variation as already active, in which case nothing happens. Otherwise the selected block is transformed to the variation's block type first, which requires a [block transform](/docs/reference-guides/block-api/block-transforms.md#block) between the two block types to exist. In the example above, that is what lets the shortcut turn a Paragraph block into a Heading block as well as change the level of an existing one.
The shortcut only applies to blocks that are already of the variation's own block type: it sets the variation's `attributes` on the selected block, and does nothing when `isActive` reports the variation as already active. In the example above, that is what lets the shortcut change the level of an existing Heading block.

To reach the variation from a *different* block type, declare the same shortcut on a [block transform](/docs/reference-guides/block-api/block-transforms.md#keyboard-shortcuts) that produces this block, giving it the same `name` and a `variationName` pointing back at the variation. The Heading block does both, which is why <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>2</kbd> turns a Paragraph into an H2 as well as changing an existing heading's level. Declaring one shortcut in several places is expected, as long as every declaration agrees on the key combination and description; the two can never both apply to the same selection.

Shortcuts only apply to blocks that are fully editable, and are ignored for blocks in a restricted editing mode such as content-only editing.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function useApplyBlockShortcut() {
}

const blockName = getBlockName( clientId );
if ( blockNames && ! blockNames.includes( blockName ) ) {
if ( ! blockNames.includes( blockName ) ) {
return false;
}

Expand Down Expand Up @@ -159,30 +159,47 @@ function BlockShortcutsRegister( { shortcuts } ) {
const { registerShortcut, unregisterShortcut } = useDispatch(
keyboardShortcutsStore
);
// Shortcuts registered by a previous run, so that those belonging to a
// block type that has since been unregistered can be removed. Registration
// deliberately outlives this component: providers can be nested, and
// unmounting one of them must not take the shortcuts away from the others.
const registeredRef = useRef( new Set() );
// Shortcuts registered by a previous run, keyed by name, so that those
// belonging to a block type that has since been unregistered can be
// removed. Registration deliberately outlives this component: providers can
// be nested, and unmounting one of them must not take the shortcuts away
// from the others.
const registeredRef = useRef( new Map() );

useEffect( () => {
const registered = new Set();
const registered = new Map();
const combinations = new Map();

for ( const shortcut of shortcuts ) {
// The store keys shortcuts by name, so the second registration
// would overwrite the first and leave one of the two blocks
// answering to the other's key combination.
if ( registered.has( shortcut.name ) ) {
warning(
`Block keyboard shortcut "${ shortcut.name }" is declared more than once.`
);
const config = {
name: shortcut.name,
category: 'block',
description: shortcut.description,
keyCombination: shortcut.keyCombination,
aliases: shortcut.aliases,
};
const serialized = JSON.stringify( config );

// One shortcut is declared once per block change it can make, so
// the same name legitimately appears more than once: the heading
// level shortcuts are declared both on the variation, for a
// heading, and on the transform that produces one from a
// paragraph. Only declarations that disagree are a mistake, since
// the store keys shortcuts by name and the last one registered
// would decide what the others are matched against.
const previous = registered.get( shortcut.name );
if ( previous !== undefined ) {
if ( previous !== serialized ) {
warning(
`Block keyboard shortcut "${ shortcut.name }" is declared more than once with different settings.`
);
}
continue;
}

// Handlers are matched by key combination rather than by name, so
// two blocks claiming the same one both run on the same event, in
// registration order, and the first that applies takes it.
// two shortcuts claiming the same one both run on the same event,
// in registration order, and the first that applies takes it.
const { modifier, character } = shortcut.keyCombination;
const combination = `${ modifier ?? '' }+${ character }`;
if ( combinations.has( combination ) ) {
Expand All @@ -196,17 +213,11 @@ function BlockShortcutsRegister( { shortcuts } ) {
}
combinations.set( combination, shortcut.name );

registerShortcut( {
name: shortcut.name,
category: 'block',
description: shortcut.description,
keyCombination: shortcut.keyCombination,
aliases: shortcut.aliases,
} );
registered.add( shortcut.name );
registerShortcut( config );
registered.set( shortcut.name, serialized );
}

for ( const name of registeredRef.current ) {
for ( const name of registeredRef.current.keys() ) {
if ( ! registered.has( name ) ) {
unregisterShortcut( name );
}
Expand Down
49 changes: 34 additions & 15 deletions packages/block-library/src/heading/transforms.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createBlock, getBlockAttributes } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { getLevelFromHeadingNodeName } from './shared';
import { getTransformedAttributes } from '../utils/get-transformed-attributes';

Expand All @@ -9,6 +9,23 @@ const transforms = {
type: 'block',
isMultiBlock: true,
blocks: [ 'core/paragraph' ],
// The level shortcuts are declared here as well as on the
// variations, so that they reach a paragraph and not only a
// heading. Both declarations describe the same shortcut, and only
// one of them can apply to any given selection.
shortcuts: [ 1, 2, 3, 4, 5, 6 ].map( ( level ) => ( {
name: `core/block-editor/transform-to-heading-${ level }`,
description: sprintf(
/* translators: %d: heading level e.g: "1", "2", "3" */
__( 'Transform the selected block into a heading %d.' ),
level
),
keyCombination: {
modifier: 'access',
character: `${ level }`,
},
variationName: `h${ level }`,
} ) ),
transform: ( attributes ) =>
attributes.map( ( _attributes ) => {
const { content, anchor, style } = _attributes;
Expand Down Expand Up @@ -97,22 +114,24 @@ const transforms = {
type: 'block',
isMultiBlock: true,
blocks: [ 'core/paragraph' ],
shortcut: {
name: 'core/block-editor/transform-heading-to-paragraph',
description: __(
'Transform the selected heading into a paragraph.'
),
keyCombination: {
modifier: 'access',
character: '0',
},
aliases: [
{
shortcuts: [
{
name: 'core/block-editor/transform-heading-to-paragraph',
description: __(
'Transform the selected heading into a paragraph.'
),
keyCombination: {
modifier: 'access',
character: '7',
character: '0',
},
],
},
aliases: [
{
modifier: 'access',
character: '7',
},
],
},
],
transform: ( attributes ) =>
attributes.map( ( _attributes ) => {
const { content, style } = _attributes;
Expand Down
50 changes: 25 additions & 25 deletions packages/blocks/src/store/private-selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ type ResolvedBlockShortcut = BlockShortcut & {
*/
targetBlockName: string;
/**
* The block types the shortcut applies to. When undefined, the shortcut
* applies to any block that can be transformed to `targetBlockName`.
* The block types the shortcut applies to.
*/
blockNames?: string[];
blockNames: string[];
/**
* The variation of `targetBlockName` the shortcut produces, if any.
*/
Expand Down Expand Up @@ -324,10 +323,9 @@ export const getBlockKeyboardShortcuts = createSelector(
const shortcuts: ResolvedBlockShortcut[] = [];

for ( const blockName of Object.keys( state.blockTypes ) ) {
// A variation shortcut produces the variation of the block type it
// is declared on. It is not restricted to a source block type: any
// block with a transform to this block type can be its subject,
// which is what makes `access+2` on a paragraph produce a heading.
// A variation shortcut only applies to blocks that are already of
// the block type declaring it. Reaching the variation from another
// block type is a transform, and is declared as one.
for ( const variation of state.blockVariations[ blockName ] ??
[] ) {
if ( ! variation.shortcut ) {
Expand All @@ -336,6 +334,7 @@ export const getBlockKeyboardShortcuts = createSelector(
shortcuts.push( {
...variation.shortcut,
targetBlockName: blockName,
blockNames: [ blockName ],
variationName: variation.name,
} );
}
Expand All @@ -346,35 +345,36 @@ export const getBlockKeyboardShortcuts = createSelector(
// A `to` transform can list several target blocks, but a shortcut
// can only produce one of them. The first is used.
const targetBlockName = transform.blocks?.[ 0 ];
if (
! transform.shortcut ||
transform.type !== 'block' ||
! targetBlockName
) {
if ( transform.type !== 'block' || ! targetBlockName ) {
continue;
}
shortcuts.push( {
...transform.shortcut,
targetBlockName,
blockNames: [ blockName ],
variationName: transform.variationName,
} );
for ( const shortcut of transform.shortcuts ?? [] ) {
shortcuts.push( {
...shortcut,
targetBlockName,
blockNames: [ blockName ],
variationName:
shortcut.variationName ?? transform.variationName,
} );
}
}

for ( const transform of transforms?.from ?? [] ) {
if (
! transform.shortcut ||
transform.type !== 'block' ||
! transform.blocks?.length
) {
continue;
}
shortcuts.push( {
...transform.shortcut,
targetBlockName: blockName,
blockNames: transform.blocks,
variationName: transform.variationName,
} );
for ( const shortcut of transform.shortcuts ?? [] ) {
shortcuts.push( {
...shortcut,
targetBlockName: blockName,
blockNames: transform.blocks,
variationName:
shortcut.variationName ?? transform.variationName,
} );
}
}
}

Expand Down
Loading
Loading