@wordpress/block-editor
Version:
531 lines (489 loc) • 14.3 kB
JavaScript
/**
* External dependencies
*/
import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { useMemo, useCallback, RawHTML } from '@wordpress/element';
import {
getBlockType,
getSaveContent,
isUnmodifiedDefaultBlock,
serializeRawBlock,
switchToBlockType,
getDefaultBlockName,
isUnmodifiedBlock,
} from '@wordpress/blocks';
import { withFilters } from '@wordpress/components';
import {
withDispatch,
withSelect,
useDispatch,
useSelect,
} from '@wordpress/data';
import { compose, pure, ifCondition } from '@wordpress/compose';
import { safeHTML } from '@wordpress/dom';
/**
* Internal dependencies
*/
import BlockEdit from '../block-edit';
import BlockInvalidWarning from './block-invalid-warning';
import BlockCrashWarning from './block-crash-warning';
import BlockCrashBoundary from './block-crash-boundary';
import BlockHtml from './block-html';
import { useBlockProps } from './use-block-props';
import { store as blockEditorStore } from '../../store';
import { useLayout } from './layout';
import { unlock } from '../../lock-unlock';
import { BlockListBlockContext } from './block-list-block-context';
/**
* Merges wrapper props with special handling for classNames and styles.
*
* @param {Object} propsA
* @param {Object} propsB
*
* @return {Object} Merged props.
*/
function mergeWrapperProps( propsA, propsB ) {
const newProps = {
...propsA,
...propsB,
};
if ( propsA?.className && propsB?.className ) {
newProps.className = classnames( propsA.className, propsB.className );
}
if ( propsA?.style && propsB?.style ) {
newProps.style = { ...propsA.style, ...propsB.style };
}
return newProps;
}
function Block( { children, isHtml, ...props } ) {
return (
<div { ...useBlockProps( props, { __unstableIsHtml: isHtml } ) }>
{ children }
</div>
);
}
function BlockListBlock( {
block: { __unstableBlockSource },
mode,
isLocked,
canRemove,
clientId,
isSelected,
isSelectionEnabled,
className,
__unstableLayoutClassNames: layoutClassNames,
name,
isValid,
attributes,
wrapperProps,
setAttributes,
onReplace,
onInsertBlocksAfter,
onMerge,
toggleSelection,
} ) {
const {
themeSupportsLayout,
isTemporarilyEditingAsBlocks,
blockEditingMode,
} = useSelect(
( select ) => {
const {
getSettings,
__unstableGetTemporarilyEditingAsBlocks,
getBlockEditingMode,
} = unlock( select( blockEditorStore ) );
return {
themeSupportsLayout: getSettings().supportsLayout,
isTemporarilyEditingAsBlocks:
__unstableGetTemporarilyEditingAsBlocks() === clientId,
blockEditingMode: getBlockEditingMode( clientId ),
};
},
[ clientId ]
);
const { removeBlock } = useDispatch( blockEditorStore );
const onRemove = useCallback( () => removeBlock( clientId ), [ clientId ] );
const parentLayout = useLayout() || {};
// We wrap the BlockEdit component in a div that hides it when editing in
// HTML mode. This allows us to render all of the ancillary pieces
// (InspectorControls, etc.) which are inside `BlockEdit` but not
// `BlockHTML`, even in HTML mode.
let blockEdit = (
<BlockEdit
name={ name }
isSelected={ isSelected }
attributes={ attributes }
setAttributes={ setAttributes }
insertBlocksAfter={ isLocked ? undefined : onInsertBlocksAfter }
onReplace={ canRemove ? onReplace : undefined }
onRemove={ canRemove ? onRemove : undefined }
mergeBlocks={ canRemove ? onMerge : undefined }
clientId={ clientId }
isSelectionEnabled={ isSelectionEnabled }
toggleSelection={ toggleSelection }
__unstableLayoutClassNames={ layoutClassNames }
__unstableParentLayout={
Object.keys( parentLayout ).length ? parentLayout : undefined
}
/>
);
const blockType = getBlockType( name );
if ( blockEditingMode === 'disabled' ) {
wrapperProps = {
...wrapperProps,
tabIndex: -1,
};
}
// Determine whether the block has props to apply to the wrapper.
if ( blockType?.getEditWrapperProps ) {
wrapperProps = mergeWrapperProps(
wrapperProps,
blockType.getEditWrapperProps( attributes )
);
}
const isAligned =
wrapperProps &&
!! wrapperProps[ 'data-align' ] &&
! themeSupportsLayout;
// For aligned blocks, provide a wrapper element so the block can be
// positioned relative to the block column.
// This is only kept for classic themes that don't support layout
// Historically we used to rely on extra divs and data-align to
// provide the alignments styles in the editor.
// Due to the differences between frontend and backend, we migrated
// to the layout feature, and we're now aligning the markup of frontend
// and backend.
if ( isAligned ) {
blockEdit = (
<div
className="wp-block"
data-align={ wrapperProps[ 'data-align' ] }
>
{ blockEdit }
</div>
);
}
let block;
if ( ! isValid ) {
const saveContent = __unstableBlockSource
? serializeRawBlock( __unstableBlockSource )
: getSaveContent( blockType, attributes );
block = (
<Block className="has-warning">
<BlockInvalidWarning clientId={ clientId } />
<RawHTML>{ safeHTML( saveContent ) }</RawHTML>
</Block>
);
} else if ( mode === 'html' ) {
// Render blockEdit so the inspector controls don't disappear.
// See #8969.
block = (
<>
<div style={ { display: 'none' } }>{ blockEdit }</div>
<Block isHtml>
<BlockHtml clientId={ clientId } />
</Block>
</>
);
} else if ( blockType?.apiVersion > 1 ) {
block = blockEdit;
} else {
block = <Block { ...wrapperProps }>{ blockEdit }</Block>;
}
const { 'data-align': dataAlign, ...restWrapperProps } = wrapperProps ?? {};
const value = {
clientId,
className: classnames(
{
'is-editing-disabled': blockEditingMode === 'disabled',
'is-content-locked-temporarily-editing-as-blocks':
isTemporarilyEditingAsBlocks,
},
dataAlign && themeSupportsLayout && `align${ dataAlign }`,
className
),
wrapperProps: restWrapperProps,
isAligned,
};
const memoizedValue = useMemo( () => value, Object.values( value ) );
return (
<BlockListBlockContext.Provider value={ memoizedValue }>
<BlockCrashBoundary
fallback={
<Block className="has-warning">
<BlockCrashWarning />
</Block>
}
>
{ block }
</BlockCrashBoundary>
</BlockListBlockContext.Provider>
);
}
const applyWithSelect = withSelect( ( select, { clientId, rootClientId } ) => {
const {
isBlockSelected,
getBlockMode,
isSelectionEnabled,
getTemplateLock,
__unstableGetBlockWithoutInnerBlocks,
canRemoveBlock,
canMoveBlock,
} = select( blockEditorStore );
const block = __unstableGetBlockWithoutInnerBlocks( clientId );
const isSelected = isBlockSelected( clientId );
const templateLock = getTemplateLock( rootClientId );
const canRemove = canRemoveBlock( clientId, rootClientId );
const canMove = canMoveBlock( clientId, rootClientId );
// The fallback to `{}` is a temporary fix.
// This function should never be called when a block is not present in
// the state. It happens now because the order in withSelect rendering
// is not correct.
const { name, attributes, isValid } = block || {};
// Do not add new properties here, use `useSelect` instead to avoid
// leaking new props to the public API (editor.BlockListBlock filter).
return {
mode: getBlockMode( clientId ),
isSelectionEnabled: isSelectionEnabled(),
isLocked: !! templateLock,
canRemove,
canMove,
// Users of the editor.BlockListBlock filter used to be able to
// access the block prop.
// Ideally these blocks would rely on the clientId prop only.
// This is kept for backward compatibility reasons.
block,
name,
attributes,
isValid,
isSelected,
};
} );
const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => {
const {
updateBlockAttributes,
insertBlocks,
mergeBlocks,
replaceBlocks,
toggleSelection,
__unstableMarkLastChangeAsPersistent,
moveBlocksToPosition,
removeBlock,
} = dispatch( blockEditorStore );
// Do not add new properties here, use `useDispatch` instead to avoid
// leaking new props to the public API (editor.BlockListBlock filter).
return {
setAttributes( newAttributes ) {
const { getMultiSelectedBlockClientIds } =
registry.select( blockEditorStore );
const multiSelectedBlockClientIds =
getMultiSelectedBlockClientIds();
const { clientId } = ownProps;
const clientIds = multiSelectedBlockClientIds.length
? multiSelectedBlockClientIds
: [ clientId ];
updateBlockAttributes( clientIds, newAttributes );
},
onInsertBlocks( blocks, index ) {
const { rootClientId } = ownProps;
insertBlocks( blocks, index, rootClientId );
},
onInsertBlocksAfter( blocks ) {
const { clientId, rootClientId } = ownProps;
const { getBlockIndex } = registry.select( blockEditorStore );
const index = getBlockIndex( clientId );
insertBlocks( blocks, index + 1, rootClientId );
},
onMerge( forward ) {
const { clientId, rootClientId } = ownProps;
const {
getPreviousBlockClientId,
getNextBlockClientId,
getBlock,
getBlockAttributes,
getBlockName,
getBlockOrder,
getBlockIndex,
getBlockRootClientId,
canInsertBlockType,
} = registry.select( blockEditorStore );
/**
* Moves the block with clientId up one level. If the block type
* cannot be inserted at the new location, it will be attempted to
* convert to the default block type.
*
* @param {string} _clientId The block to move.
* @param {boolean} changeSelection Whether to change the selection
* to the moved block.
*/
function moveFirstItemUp( _clientId, changeSelection = true ) {
const targetRootClientId = getBlockRootClientId( _clientId );
const blockOrder = getBlockOrder( _clientId );
const [ firstClientId ] = blockOrder;
if (
blockOrder.length === 1 &&
isUnmodifiedBlock( getBlock( firstClientId ) )
) {
removeBlock( _clientId );
} else {
registry.batch( () => {
if (
canInsertBlockType(
getBlockName( firstClientId ),
targetRootClientId
)
) {
moveBlocksToPosition(
[ firstClientId ],
_clientId,
targetRootClientId,
getBlockIndex( _clientId )
);
} else {
const replacement = switchToBlockType(
getBlock( firstClientId ),
getDefaultBlockName()
);
if ( replacement && replacement.length ) {
insertBlocks(
replacement,
getBlockIndex( _clientId ),
targetRootClientId,
changeSelection
);
removeBlock( firstClientId, false );
}
}
if (
! getBlockOrder( _clientId ).length &&
isUnmodifiedBlock( getBlock( _clientId ) )
) {
removeBlock( _clientId, false );
}
} );
}
}
// For `Delete` or forward merge, we should do the exact same thing
// as `Backspace`, but from the other block.
if ( forward ) {
if ( rootClientId ) {
const nextRootClientId =
getNextBlockClientId( rootClientId );
if ( nextRootClientId ) {
// If there is a block that follows with the same parent
// block name and the same attributes, merge the inner
// blocks.
if (
getBlockName( rootClientId ) ===
getBlockName( nextRootClientId )
) {
const rootAttributes =
getBlockAttributes( rootClientId );
const previousRootAttributes =
getBlockAttributes( nextRootClientId );
if (
Object.keys( rootAttributes ).every(
( key ) =>
rootAttributes[ key ] ===
previousRootAttributes[ key ]
)
) {
registry.batch( () => {
moveBlocksToPosition(
getBlockOrder( nextRootClientId ),
nextRootClientId,
rootClientId
);
removeBlock( nextRootClientId, false );
} );
return;
}
} else {
mergeBlocks( rootClientId, nextRootClientId );
return;
}
}
}
const nextBlockClientId = getNextBlockClientId( clientId );
if ( ! nextBlockClientId ) {
return;
}
if ( getBlockOrder( nextBlockClientId ).length ) {
moveFirstItemUp( nextBlockClientId, false );
} else {
mergeBlocks( clientId, nextBlockClientId );
}
} else {
const previousBlockClientId =
getPreviousBlockClientId( clientId );
if ( previousBlockClientId ) {
mergeBlocks( previousBlockClientId, clientId );
} else if ( rootClientId ) {
const previousRootClientId =
getPreviousBlockClientId( rootClientId );
// If there is a preceding block with the same parent block
// name and the same attributes, merge the inner blocks.
if (
previousRootClientId &&
getBlockName( rootClientId ) ===
getBlockName( previousRootClientId )
) {
const rootAttributes =
getBlockAttributes( rootClientId );
const previousRootAttributes =
getBlockAttributes( previousRootClientId );
if (
Object.keys( rootAttributes ).every(
( key ) =>
rootAttributes[ key ] ===
previousRootAttributes[ key ]
)
) {
registry.batch( () => {
moveBlocksToPosition(
getBlockOrder( rootClientId ),
rootClientId,
previousRootClientId
);
removeBlock( rootClientId, false );
} );
return;
}
}
moveFirstItemUp( rootClientId );
}
}
},
onReplace( blocks, indexToSelect, initialPosition ) {
if (
blocks.length &&
! isUnmodifiedDefaultBlock( blocks[ blocks.length - 1 ] )
) {
__unstableMarkLastChangeAsPersistent();
}
replaceBlocks(
[ ownProps.clientId ],
blocks,
indexToSelect,
initialPosition
);
},
toggleSelection( selectionEnabled ) {
toggleSelection( selectionEnabled );
},
};
} );
export default compose(
pure,
applyWithSelect,
applyWithDispatch,
// Block is sometimes not mounted at the right time, causing it be undefined
// see issue for more info
// https://github.com/WordPress/gutenberg/issues/17013
ifCondition( ( { block } ) => !! block ),
withFilters( 'editor.BlockListBlock' )
)( BlockListBlock );