@gechiui/block-editor
Version:
1,276 lines (1,170 loc) • 34.8 kB
JavaScript
/**
* External dependencies
*/
import { castArray, findKey, first, isObject, last, some } from 'lodash';
/**
* GeChiUI dependencies
*/
import {
cloneBlock,
__experimentalCloneSanitizedBlock,
createBlock,
doBlocksMatchTemplate,
getBlockType,
getDefaultBlockName,
hasBlockSupport,
switchToBlockType,
synchronizeBlocksWithTemplate,
} from '@gechiui/blocks';
import { speak } from '@gechiui/a11y';
import { __, _n, sprintf } from '@gechiui/i18n';
import { create, insert, remove, toHTMLString } from '@gechiui/rich-text';
import deprecated from '@gechiui/deprecated';
/**
* Action which will insert a default block insert action if there
* are no other blocks at the root of the editor. This action should be used
* in actions which may result in no blocks remaining in the editor (removal,
* replacement, etc).
*/
const ensureDefaultBlock = () => ( { select, dispatch } ) => {
// To avoid a focus loss when removing the last block, assure there is
// always a default block if the last of the blocks have been removed.
const count = select.getBlockCount();
if ( count > 0 ) {
return;
}
// If there's an custom appender, don't insert default block.
// We have to remember to manually move the focus elsewhere to
// prevent it from being lost though.
const { __unstableHasCustomAppender } = select.getSettings();
if ( __unstableHasCustomAppender ) {
return;
}
dispatch.insertDefaultBlock();
};
/**
* Action that resets blocks state to the specified array of blocks, taking precedence
* over any other content reflected as an edit in state.
*
* @param {Array} blocks Array of blocks.
*/
export const resetBlocks = ( blocks ) => ( { dispatch } ) => {
dispatch( { type: 'RESET_BLOCKS', blocks } );
dispatch( validateBlocksToTemplate( blocks ) );
};
/**
* Block validity is a function of blocks state (at the point of a
* reset) and the template setting. As a compromise to its placement
* across distinct parts of state, it is implemented here as a side-
* effect of the block reset action.
*
* @param {Array} blocks Array of blocks.
*/
export const validateBlocksToTemplate = ( blocks ) => ( {
select,
dispatch,
} ) => {
const template = select.getTemplate();
const templateLock = select.getTemplateLock();
// Unlocked templates are considered always valid because they act
// as default values only.
const isBlocksValidToTemplate =
! template ||
templateLock !== 'all' ||
doBlocksMatchTemplate( blocks, template );
// Update if validity has changed.
const isValidTemplate = select.isValidTemplate();
if ( isBlocksValidToTemplate !== isValidTemplate ) {
dispatch.setTemplateValidity( isBlocksValidToTemplate );
return isBlocksValidToTemplate;
}
};
/**
* A block selection object.
*
* @typedef {Object} GCBlockSelection
*
* @property {string} clientId A block client ID.
* @property {string} attributeKey A block attribute key.
* @property {number} offset An attribute value offset, based on the rich
* text value. See `gc.richText.create`.
*/
/* eslint-disable jsdoc/valid-types */
/**
* Returns an action object used in signalling that selection state should be
* reset to the specified selection.
*
* @param {GCBlockSelection} selectionStart The selection start.
* @param {GCBlockSelection} selectionEnd The selection end.
* @param {0|-1|null} initialPosition Initial block position.
*
* @return {Object} Action object.
*/
export function resetSelection(
selectionStart,
selectionEnd,
initialPosition
) {
/* eslint-enable jsdoc/valid-types */
return {
type: 'RESET_SELECTION',
selectionStart,
selectionEnd,
initialPosition,
};
}
/**
* Returns an action object used in signalling that blocks have been received.
* Unlike resetBlocks, these should be appended to the existing known set, not
* replacing.
*
* @deprecated
*
* @param {Object[]} blocks Array of block objects.
*
* @return {Object} Action object.
*/
export function receiveBlocks( blocks ) {
deprecated( 'gc.data.dispatch( "core/block-editor" ).receiveBlocks', {
since: '5.9',
alternative: 'resetBlocks or insertBlocks',
} );
return {
type: 'RECEIVE_BLOCKS',
blocks,
};
}
/**
* Action that updates attributes of multiple blocks with the specified client IDs.
*
* @param {string|string[]} clientIds Block client IDs.
* @param {Object} attributes Block attributes to be merged. Should be keyed by clientIds if
* uniqueByBlock is true.
* @param {boolean} uniqueByBlock true if each block in clientIds array has a unique set of attributes
* @return {Object} Action object.
*/
export function updateBlockAttributes(
clientIds,
attributes,
uniqueByBlock = false
) {
return {
type: 'UPDATE_BLOCK_ATTRIBUTES',
clientIds: castArray( clientIds ),
attributes,
uniqueByBlock,
};
}
/**
* Action that updates the block with the specified client ID.
*
* @param {string} clientId Block client ID.
* @param {Object} updates Block attributes to be merged.
*
* @return {Object} Action object.
*/
export function updateBlock( clientId, updates ) {
return {
type: 'UPDATE_BLOCK',
clientId,
updates,
};
}
/* eslint-disable jsdoc/valid-types */
/**
* Returns an action object used in signalling that the block with the
* specified client ID has been selected, optionally accepting a position
* value reflecting its selection directionality. An initialPosition of -1
* reflects a reverse selection.
*
* @param {string} clientId Block client ID.
* @param {0|-1|null} initialPosition Optional initial position. Pass as -1 to
* reflect reverse selection.
*
* @return {Object} Action object.
*/
export function selectBlock( clientId, initialPosition = 0 ) {
/* eslint-enable jsdoc/valid-types */
return {
type: 'SELECT_BLOCK',
initialPosition,
clientId,
};
}
/**
* Yields action objects used in signalling that the block preceding the given
* clientId should be selected.
*
* @param {string} clientId Block client ID.
*/
export const selectPreviousBlock = ( clientId ) => ( { select, dispatch } ) => {
const previousBlockClientId = select.getPreviousBlockClientId( clientId );
if ( previousBlockClientId ) {
dispatch.selectBlock( previousBlockClientId, -1 );
}
};
/**
* Yields action objects used in signalling that the block following the given
* clientId should be selected.
*
* @param {string} clientId Block client ID.
*/
export const selectNextBlock = ( clientId ) => ( { select, dispatch } ) => {
const nextBlockClientId = select.getNextBlockClientId( clientId );
if ( nextBlockClientId ) {
dispatch.selectBlock( nextBlockClientId );
}
};
/**
* Action that starts block multi-selection.
*
* @return {Object} Action object.
*/
export function startMultiSelect() {
return {
type: 'START_MULTI_SELECT',
};
}
/**
* Action that stops block multi-selection.
*
* @return {Object} Action object.
*/
export function stopMultiSelect() {
return {
type: 'STOP_MULTI_SELECT',
};
}
/**
* Action that changes block multi-selection.
*
* @param {string} start First block of the multi selection.
* @param {string} end Last block of the multiselection.
*/
export const multiSelect = ( start, end ) => ( { select, dispatch } ) => {
const startBlockRootClientId = select.getBlockRootClientId( start );
const endBlockRootClientId = select.getBlockRootClientId( end );
// Only allow block multi-selections at the same level.
if ( startBlockRootClientId !== endBlockRootClientId ) {
return;
}
dispatch( { type: 'MULTI_SELECT', start, end } );
const blockCount = select.getSelectedBlockCount();
speak(
sprintf(
/* translators: %s: number of selected blocks */
_n( '%s block selected.', '%s blocks selected.', blockCount ),
blockCount
),
'assertive'
);
};
/**
* Action that clears the block selection.
*
* @return {Object} Action object.
*/
export function clearSelectedBlock() {
return {
type: 'CLEAR_SELECTED_BLOCK',
};
}
/**
* Action that enables or disables block selection.
*
* @param {boolean} [isSelectionEnabled=true] Whether block selection should
* be enabled.
*
* @return {Object} Action object.
*/
export function toggleSelection( isSelectionEnabled = true ) {
return {
type: 'TOGGLE_SELECTION',
isSelectionEnabled,
};
}
function getBlocksWithDefaultStylesApplied( blocks, blockEditorSettings ) {
const preferredStyleVariations =
blockEditorSettings?.__experimentalPreferredStyleVariations?.value ??
{};
return blocks.map( ( block ) => {
const blockName = block.name;
if ( ! hasBlockSupport( blockName, 'defaultStylePicker', true ) ) {
return block;
}
if ( ! preferredStyleVariations[ blockName ] ) {
return block;
}
const className = block.attributes?.className;
if ( className?.includes( 'is-style-' ) ) {
return block;
}
const { attributes = {} } = block;
const blockStyle = preferredStyleVariations[ blockName ];
return {
...block,
attributes: {
...attributes,
className: `${
className || ''
} is-style-${ blockStyle }`.trim(),
},
};
} );
}
/* eslint-disable jsdoc/valid-types */
/**
* Action that replaces given blocks with one or more replacement blocks.
*
* @param {(string|string[])} clientIds Block client ID(s) to replace.
* @param {(Object|Object[])} blocks Replacement block(s).
* @param {number} indexToSelect Index of replacement block to select.
* @param {0|-1|null} initialPosition Index of caret after in the selected block after the operation.
* @param {?Object} meta Optional Meta values to be passed to the action object.
*
* @return {Object} Action object.
*/
export const replaceBlocks = (
clientIds,
blocks,
indexToSelect,
initialPosition = 0,
meta
) => ( { select, dispatch } ) => {
/* eslint-enable jsdoc/valid-types */
clientIds = castArray( clientIds );
blocks = getBlocksWithDefaultStylesApplied(
castArray( blocks ),
select.getSettings()
);
const rootClientId = select.getBlockRootClientId( first( clientIds ) );
// Replace is valid if the new blocks can be inserted in the root block.
for ( let index = 0; index < blocks.length; index++ ) {
const block = blocks[ index ];
const canInsertBlock = select.canInsertBlockType(
block.name,
rootClientId
);
if ( ! canInsertBlock ) {
return;
}
}
dispatch( {
type: 'REPLACE_BLOCKS',
clientIds,
blocks,
time: Date.now(),
indexToSelect,
initialPosition,
meta,
} );
dispatch( ensureDefaultBlock() );
};
/**
* Action that replaces a single block with one or more replacement blocks.
*
* @param {(string|string[])} clientId Block client ID to replace.
* @param {(Object|Object[])} block Replacement block(s).
*
* @return {Object} Action object.
*/
export function replaceBlock( clientId, block ) {
return replaceBlocks( clientId, block );
}
/**
* Higher-order action creator which, given the action type to dispatch creates
* an action creator for managing block movement.
*
* @param {string} type Action type to dispatch.
*
* @return {Function} Action creator.
*/
const createOnMove = ( type ) => ( clientIds, rootClientId ) => ( {
select,
dispatch,
} ) => {
// If one of the blocks is locked or the parent is locked, we cannot move any block.
const canMoveBlocks = select.canMoveBlocks( clientIds, rootClientId );
if ( ! canMoveBlocks ) {
return;
}
dispatch( { type, clientIds: castArray( clientIds ), rootClientId } );
};
export const moveBlocksDown = createOnMove( 'MOVE_BLOCKS_DOWN' );
export const moveBlocksUp = createOnMove( 'MOVE_BLOCKS_UP' );
/**
* Action that moves given blocks to a new position.
*
* @param {?string} clientIds The client IDs of the blocks.
* @param {?string} fromRootClientId Root client ID source.
* @param {?string} toRootClientId Root client ID destination.
* @param {number} index The index to move the blocks to.
*/
export const moveBlocksToPosition = (
clientIds,
fromRootClientId = '',
toRootClientId = '',
index
) => ( { select, dispatch } ) => {
const canMoveBlocks = select.canMoveBlocks( clientIds, fromRootClientId );
// If one of the blocks is locked or the parent is locked, we cannot move any block.
if ( ! canMoveBlocks ) {
return;
}
// If moving inside the same root block the move is always possible.
if ( fromRootClientId !== toRootClientId ) {
const canRemoveBlocks = select.canRemoveBlocks(
clientIds,
fromRootClientId
);
// If we're moving to another block, it means we're deleting blocks from
// the original block, so we need to check if removing is possible.
if ( ! canRemoveBlocks ) {
return;
}
const canInsertBlocks = select.canInsertBlocks(
clientIds,
toRootClientId
);
// If moving to other parent block, the move is possible if we can insert a block of the same type inside the new parent block.
if ( ! canInsertBlocks ) {
return;
}
}
dispatch( {
type: 'MOVE_BLOCKS_TO_POSITION',
fromRootClientId,
toRootClientId,
clientIds,
index,
} );
};
/**
* Action that moves given block to a new position.
*
* @param {?string} clientId The client ID of the block.
* @param {?string} fromRootClientId Root client ID source.
* @param {?string} toRootClientId Root client ID destination.
* @param {number} index The index to move the block to.
*/
export function moveBlockToPosition(
clientId,
fromRootClientId = '',
toRootClientId = '',
index
) {
return moveBlocksToPosition(
[ clientId ],
fromRootClientId,
toRootClientId,
index
);
}
/**
* Action that inserts a single block, optionally at a specific index respective a root block list.
*
* @param {Object} block Block object to insert.
* @param {?number} index Index at which block should be inserted.
* @param {?string} rootClientId Optional root client ID of block list on which to insert.
* @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to true.
* @param {?Object} meta Optional Meta values to be passed to the action object.
*
* @return {Object} Action object.
*/
export function insertBlock(
block,
index,
rootClientId,
updateSelection,
meta
) {
return insertBlocks(
[ block ],
index,
rootClientId,
updateSelection,
0,
meta
);
}
/* eslint-disable jsdoc/valid-types */
/**
* Action that inserts an array of blocks, optionally at a specific index respective a root block list.
*
* @param {Object[]} blocks Block objects to insert.
* @param {?number} index Index at which block should be inserted.
* @param {?string} rootClientId Optional root client ID of block list on which to insert.
* @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to true.
* @param {0|-1|null} initialPosition Initial focus position. Setting it to null prevent focusing the inserted block.
* @param {?Object} meta Optional Meta values to be passed to the action object.
* @return {Object} Action object.
*/
export const insertBlocks = (
blocks,
index,
rootClientId,
updateSelection = true,
initialPosition = 0,
meta
) => ( { select, dispatch } ) => {
/* eslint-enable jsdoc/valid-types */
if ( isObject( initialPosition ) ) {
meta = initialPosition;
initialPosition = 0;
deprecated( "meta argument in gc.data.dispatch('core/block-editor')", {
since: '10.1',
plugin: 'Gutenberg',
hint: 'The meta argument is now the 6th argument of the function',
} );
}
blocks = getBlocksWithDefaultStylesApplied(
castArray( blocks ),
select.getSettings()
);
const allowedBlocks = [];
for ( const block of blocks ) {
const isValid = select.canInsertBlockType( block.name, rootClientId );
if ( isValid ) {
allowedBlocks.push( block );
}
}
if ( allowedBlocks.length ) {
dispatch( {
type: 'INSERT_BLOCKS',
blocks: allowedBlocks,
index,
rootClientId,
time: Date.now(),
updateSelection,
initialPosition: updateSelection ? initialPosition : null,
meta,
} );
}
};
/**
* Action that shows the insertion point.
*
* @param {?string} rootClientId Optional root client ID of block list on
* which to insert.
* @param {?number} index Index at which block should be inserted.
* @param {Object} __unstableOptions Wether or not to show an inserter button.
*
* @return {Object} Action object.
*/
export function showInsertionPoint(
rootClientId,
index,
__unstableOptions = {}
) {
const { __unstableWithInserter } = __unstableOptions;
return {
type: 'SHOW_INSERTION_POINT',
rootClientId,
index,
__unstableWithInserter,
};
}
/**
* Action that hides the insertion point.
*
* @return {Object} Action object.
*/
export function hideInsertionPoint() {
return {
type: 'HIDE_INSERTION_POINT',
};
}
/**
* Action that resets the template validity.
*
* @param {boolean} isValid template validity flag.
*
* @return {Object} Action object.
*/
export function setTemplateValidity( isValid ) {
return {
type: 'SET_TEMPLATE_VALIDITY',
isValid,
};
}
/**
* Action that synchronizes the template with the list of blocks.
*
* @return {Object} Action object.
*/
export const synchronizeTemplate = () => ( { select, dispatch } ) => {
dispatch( { type: 'SYNCHRONIZE_TEMPLATE' } );
const blocks = select.getBlocks();
const template = select.getTemplate();
const updatedBlockList = synchronizeBlocksWithTemplate( blocks, template );
dispatch.resetBlocks( updatedBlockList );
};
/**
* Action that merges two blocks.
*
* @param {string} firstBlockClientId Client ID of the first block to merge.
* @param {string} secondBlockClientId Client ID of the second block to merge.
*/
export const mergeBlocks = ( firstBlockClientId, secondBlockClientId ) => ( {
select,
dispatch,
} ) => {
const blocks = [ firstBlockClientId, secondBlockClientId ];
dispatch( { type: 'MERGE_BLOCKS', blocks } );
const [ clientIdA, clientIdB ] = blocks;
const blockA = select.getBlock( clientIdA );
const blockAType = getBlockType( blockA.name );
// Only focus the previous block if it's not mergeable
if ( blockAType && ! blockAType.merge ) {
dispatch.selectBlock( blockA.clientId );
return;
}
const blockB = select.getBlock( clientIdB );
const blockBType = getBlockType( blockB.name );
const { clientId, attributeKey, offset } = select.getSelectionStart();
const selectedBlockType = clientId === clientIdA ? blockAType : blockBType;
const attributeDefinition = selectedBlockType.attributes[ attributeKey ];
const canRestoreTextSelection =
( clientId === clientIdA || clientId === clientIdB ) &&
attributeKey !== undefined &&
offset !== undefined &&
// We cannot restore text selection if the RichText identifier
// is not a defined block attribute key. This can be the case if the
// fallback intance ID is used to store selection (and no RichText
// identifier is set), or when the identifier is wrong.
!! attributeDefinition;
if ( ! attributeDefinition ) {
if ( typeof attributeKey === 'number' ) {
window.console.error(
`RichText needs an identifier prop that is the block attribute key of the attribute it controls. Its type is expected to be a string, but was ${ typeof attributeKey }`
);
} else {
window.console.error(
'The RichText identifier prop does not match any attributes defined by the block.'
);
}
}
// A robust way to retain selection position through various transforms
// is to insert a special character at the position and then recover it.
const START_OF_SELECTED_AREA = '\u0086';
// Clone the blocks so we don't insert the character in a "live" block.
const cloneA = cloneBlock( blockA );
const cloneB = cloneBlock( blockB );
if ( canRestoreTextSelection ) {
const selectedBlock = clientId === clientIdA ? cloneA : cloneB;
const html = selectedBlock.attributes[ attributeKey ];
const {
multiline: multilineTag,
__unstableMultilineWrapperTags: multilineWrapperTags,
__unstablePreserveWhiteSpace: preserveWhiteSpace,
} = attributeDefinition;
const value = insert(
create( {
html,
multilineTag,
multilineWrapperTags,
preserveWhiteSpace,
} ),
START_OF_SELECTED_AREA,
offset,
offset
);
selectedBlock.attributes[ attributeKey ] = toHTMLString( {
value,
multilineTag,
preserveWhiteSpace,
} );
}
// We can only merge blocks with similar types
// thus, we transform the block to merge first
const blocksWithTheSameType =
blockA.name === blockB.name
? [ cloneB ]
: switchToBlockType( cloneB, blockA.name );
// If the block types can not match, do nothing
if ( ! blocksWithTheSameType || ! blocksWithTheSameType.length ) {
return;
}
// Calling the merge to update the attributes and remove the block to be merged
const updatedAttributes = blockAType.merge(
cloneA.attributes,
blocksWithTheSameType[ 0 ].attributes
);
if ( canRestoreTextSelection ) {
const newAttributeKey = findKey(
updatedAttributes,
( v ) =>
typeof v === 'string' &&
v.indexOf( START_OF_SELECTED_AREA ) !== -1
);
const convertedHtml = updatedAttributes[ newAttributeKey ];
const {
multiline: multilineTag,
__unstableMultilineWrapperTags: multilineWrapperTags,
__unstablePreserveWhiteSpace: preserveWhiteSpace,
} = blockAType.attributes[ newAttributeKey ];
const convertedValue = create( {
html: convertedHtml,
multilineTag,
multilineWrapperTags,
preserveWhiteSpace,
} );
const newOffset = convertedValue.text.indexOf( START_OF_SELECTED_AREA );
const newValue = remove( convertedValue, newOffset, newOffset + 1 );
const newHtml = toHTMLString( {
value: newValue,
multilineTag,
preserveWhiteSpace,
} );
updatedAttributes[ newAttributeKey ] = newHtml;
dispatch.selectionChange(
blockA.clientId,
newAttributeKey,
newOffset,
newOffset
);
}
dispatch.replaceBlocks(
[ blockA.clientId, blockB.clientId ],
[
{
...blockA,
attributes: {
...blockA.attributes,
...updatedAttributes,
},
},
...blocksWithTheSameType.slice( 1 ),
],
0 // If we don't pass the `indexToSelect` it will default to the last block.
);
};
/**
* Yields action objects used in signalling that the blocks corresponding to
* the set of specified client IDs are to be removed.
*
* @param {string|string[]} clientIds Client IDs of blocks to remove.
* @param {boolean} selectPrevious True if the previous block should be
* selected when a block is removed.
*/
export const removeBlocks = ( clientIds, selectPrevious = true ) => ( {
select,
dispatch,
} ) => {
if ( ! clientIds || ! clientIds.length ) {
return;
}
clientIds = castArray( clientIds );
const rootClientId = select.getBlockRootClientId( clientIds[ 0 ] );
const canRemoveBlocks = select.canRemoveBlocks( clientIds, rootClientId );
if ( ! canRemoveBlocks ) {
return;
}
if ( selectPrevious ) {
dispatch.selectPreviousBlock( clientIds[ 0 ] );
}
dispatch( { type: 'REMOVE_BLOCKS', clientIds } );
// To avoid a focus loss when removing the last block, assure there is
// always a default block if the last of the blocks have been removed.
dispatch( ensureDefaultBlock() );
};
/**
* Returns an action object used in signalling that the block with the
* specified client ID is to be removed.
*
* @param {string} clientId Client ID of block to remove.
* @param {boolean} selectPrevious True if the previous block should be
* selected when a block is removed.
*
* @return {Object} Action object.
*/
export function removeBlock( clientId, selectPrevious ) {
return removeBlocks( [ clientId ], selectPrevious );
}
/* eslint-disable jsdoc/valid-types */
/**
* Returns an action object used in signalling that the inner blocks with the
* specified client ID should be replaced.
*
* @param {string} rootClientId Client ID of the block whose InnerBlocks will re replaced.
* @param {Object[]} blocks Block objects to insert as new InnerBlocks
* @param {?boolean} updateSelection If true block selection will be updated. If false, block selection will not change. Defaults to false.
* @param {0|-1|null} initialPosition Initial block position.
* @return {Object} Action object.
*/
export function replaceInnerBlocks(
rootClientId,
blocks,
updateSelection = false,
initialPosition = 0
) {
/* eslint-enable jsdoc/valid-types */
return {
type: 'REPLACE_INNER_BLOCKS',
rootClientId,
blocks,
updateSelection,
initialPosition: updateSelection ? initialPosition : null,
time: Date.now(),
};
}
/**
* Returns an action object used to toggle the block editing mode between
* visual and HTML modes.
*
* @param {string} clientId Block client ID.
*
* @return {Object} Action object.
*/
export function toggleBlockMode( clientId ) {
return {
type: 'TOGGLE_BLOCK_MODE',
clientId,
};
}
/**
* Returns an action object used in signalling that the user has begun to type.
*
* @return {Object} Action object.
*/
export function startTyping() {
return {
type: 'START_TYPING',
};
}
/**
* Returns an action object used in signalling that the user has stopped typing.
*
* @return {Object} Action object.
*/
export function stopTyping() {
return {
type: 'STOP_TYPING',
};
}
/**
* Returns an action object used in signalling that the user has begun to drag blocks.
*
* @param {string[]} clientIds An array of client ids being dragged
*
* @return {Object} Action object.
*/
export function startDraggingBlocks( clientIds = [] ) {
return {
type: 'START_DRAGGING_BLOCKS',
clientIds,
};
}
/**
* Returns an action object used in signalling that the user has stopped dragging blocks.
*
* @return {Object} Action object.
*/
export function stopDraggingBlocks() {
return {
type: 'STOP_DRAGGING_BLOCKS',
};
}
/**
* Returns an action object used in signalling that the caret has entered formatted text.
*
* @return {Object} Action object.
*/
export function enterFormattedText() {
return {
type: 'ENTER_FORMATTED_TEXT',
};
}
/**
* Returns an action object used in signalling that the user caret has exited formatted text.
*
* @return {Object} Action object.
*/
export function exitFormattedText() {
return {
type: 'EXIT_FORMATTED_TEXT',
};
}
/**
* Action that changes the position of the user caret.
*
* @param {string} clientId The selected block client ID.
* @param {string} attributeKey The selected block attribute key.
* @param {number} startOffset The start offset.
* @param {number} endOffset The end offset.
*
* @return {Object} Action object.
*/
export function selectionChange(
clientId,
attributeKey,
startOffset,
endOffset
) {
return {
type: 'SELECTION_CHANGE',
clientId,
attributeKey,
startOffset,
endOffset,
};
}
/**
* Action that adds a new block of the default type to the block list.
*
* @param {?Object} attributes Optional attributes of the block to assign.
* @param {?string} rootClientId Optional root client ID of block list on which
* to append.
* @param {?number} index Optional index where to insert the default block
*
* @return {Object} Action object
*/
export function insertDefaultBlock( attributes, rootClientId, index ) {
// Abort if there is no default block type (if it has been unregistered).
const defaultBlockName = getDefaultBlockName();
if ( ! defaultBlockName ) {
return;
}
const block = createBlock( defaultBlockName, attributes );
return insertBlock( block, index, rootClientId );
}
/**
* Action that changes the nested settings of a given block.
*
* @param {string} clientId Client ID of the block whose nested setting are
* being received.
* @param {Object} settings Object with the new settings for the nested block.
*
* @return {Object} Action object
*/
export function updateBlockListSettings( clientId, settings ) {
return {
type: 'UPDATE_BLOCK_LIST_SETTINGS',
clientId,
settings,
};
}
/**
* Action that updates the block editor settings.
*
* @param {Object} settings Updated settings
*
* @return {Object} Action object
*/
export function updateSettings( settings ) {
return {
type: 'UPDATE_SETTINGS',
settings,
};
}
/**
* Action that signals that a temporary reusable block has been saved
* in order to switch its temporary id with the real id.
*
* @param {string} id Reusable block's id.
* @param {string} updatedId Updated block's id.
*
* @return {Object} Action object.
*/
export function __unstableSaveReusableBlock( id, updatedId ) {
return {
type: 'SAVE_REUSABLE_BLOCK_SUCCESS',
id,
updatedId,
};
}
/**
* Action that marks the last block change explicitly as persistent.
*
* @return {Object} Action object.
*/
export function __unstableMarkLastChangeAsPersistent() {
return { type: 'MARK_LAST_CHANGE_AS_PERSISTENT' };
}
/**
* Action that signals that the next block change should be marked explicitly as not persistent.
*
* @return {Object} Action object.
*/
export function __unstableMarkNextChangeAsNotPersistent() {
return { type: 'MARK_NEXT_CHANGE_AS_NOT_PERSISTENT' };
}
/**
* Action that marks the last block change as an automatic change, meaning it was not
* performed by the user, and can be undone using the `Escape` and `Backspace` keys.
* This action must be called after the change was made, and any actions that are a
* consequence of it, so it is recommended to be called at the next idle period to ensure all
* selection changes have been recorded.
*/
export const __unstableMarkAutomaticChange = () => ( { dispatch } ) => {
dispatch( { type: 'MARK_AUTOMATIC_CHANGE' } );
const { requestIdleCallback = ( cb ) => setTimeout( cb, 100 ) } = window;
requestIdleCallback( () => {
dispatch( { type: 'MARK_AUTOMATIC_CHANGE_FINAL' } );
} );
};
/**
* Action that enables or disables the navigation mode.
*
* @param {string} isNavigationMode Enable/Disable navigation mode.
*/
export const setNavigationMode = ( isNavigationMode = true ) => ( {
dispatch,
} ) => {
dispatch( { type: 'SET_NAVIGATION_MODE', isNavigationMode } );
if ( isNavigationMode ) {
speak(
__(
'您当前处于导航模式。 使用Tab键和方向键浏览区块。 使用向左和向右箭头键在嵌套级别之间移动。 要退出导航模式并编辑选定的块,请按Enter。'
)
);
} else {
speak(
__(
'您正位于编辑模式。要返回导航模式,按Escape。'
)
);
}
};
/**
* Action that enables or disables the block moving mode.
*
* @param {string|null} hasBlockMovingClientId Enable/Disable block moving mode.
*/
export const setBlockMovingClientId = ( hasBlockMovingClientId = null ) => ( {
dispatch,
} ) => {
dispatch( { type: 'SET_BLOCK_MOVING_MODE', hasBlockMovingClientId } );
if ( hasBlockMovingClientId ) {
speak(
__(
'使用Tab键和箭头键选择新的块位置。使用左右箭头键在嵌套级别之间移动。选定位置后按Enter或空格键移动块。'
)
);
}
};
/**
* Action that duplicates a list of blocks.
*
* @param {string[]} clientIds
* @param {boolean} updateSelection
*/
export const duplicateBlocks = ( clientIds, updateSelection = true ) => ( {
select,
dispatch,
} ) => {
if ( ! clientIds || ! clientIds.length ) {
return;
}
// Return early if blocks don't exist.
const blocks = select.getBlocksByClientId( clientIds );
if ( some( blocks, ( block ) => ! block ) ) {
return;
}
// Return early if blocks don't support multiple usage.
const blockNames = blocks.map( ( block ) => block.name );
if (
blockNames.some(
( blockName ) => ! hasBlockSupport( blockName, 'multiple', true )
)
) {
return;
}
const rootClientId = select.getBlockRootClientId( clientIds[ 0 ] );
const lastSelectedIndex = select.getBlockIndex(
last( castArray( clientIds ) )
);
const clonedBlocks = blocks.map( ( block ) =>
__experimentalCloneSanitizedBlock( block )
);
dispatch.insertBlocks(
clonedBlocks,
lastSelectedIndex + 1,
rootClientId,
updateSelection
);
if ( clonedBlocks.length > 1 && updateSelection ) {
dispatch.multiSelect(
first( clonedBlocks ).clientId,
last( clonedBlocks ).clientId
);
}
return clonedBlocks.map( ( block ) => block.clientId );
};
/**
* Action that inserts an empty block before a given block.
*
* @param {string} clientId
*/
export const insertBeforeBlock = ( clientId ) => ( { select, dispatch } ) => {
if ( ! clientId ) {
return;
}
const rootClientId = select.getBlockRootClientId( clientId );
const isLocked = select.getTemplateLock( rootClientId );
if ( isLocked ) {
return;
}
const firstSelectedIndex = select.getBlockIndex( clientId );
return dispatch.insertDefaultBlock( {}, rootClientId, firstSelectedIndex );
};
/**
* Action that inserts an empty block after a given block.
*
* @param {string} clientId
*/
export const insertAfterBlock = ( clientId ) => ( { select, dispatch } ) => {
if ( ! clientId ) {
return;
}
const rootClientId = select.getBlockRootClientId( clientId );
const isLocked = select.getTemplateLock( rootClientId );
if ( isLocked ) {
return;
}
const firstSelectedIndex = select.getBlockIndex( clientId );
return dispatch.insertDefaultBlock(
{},
rootClientId,
firstSelectedIndex + 1
);
};
/**
* Action that toggles the highlighted block state.
*
* @param {string} clientId The block's clientId.
* @param {boolean} isHighlighted The highlight state.
*/
export function toggleBlockHighlight( clientId, isHighlighted ) {
return {
type: 'TOGGLE_BLOCK_HIGHLIGHT',
clientId,
isHighlighted,
};
}
/**
* Action that "flashes" the block with a given `clientId` by rhythmically highlighting it.
*
* @param {string} clientId Target block client ID.
*/
export const flashBlock = ( clientId ) => async ( { dispatch } ) => {
dispatch( toggleBlockHighlight( clientId, true ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
dispatch( toggleBlockHighlight( clientId, false ) );
};
/**
* Action that sets whether a block has controlled inner blocks.
*
* @param {string} clientId The block's clientId.
* @param {boolean} hasControlledInnerBlocks True if the block's inner blocks are controlled.
*/
export function setHasControlledInnerBlocks(
clientId,
hasControlledInnerBlocks
) {
return {
type: 'SET_HAS_CONTROLLED_INNER_BLOCKS',
hasControlledInnerBlocks,
clientId,
};
}