@wordpress/block-editor
Version:
410 lines (361 loc) • 10.2 kB
JavaScript
/**
* External dependencies
*/
import clsx from 'clsx';
/**
* WordPress dependencies
*/
import { speak } from '@wordpress/a11y';
import { __, _x, sprintf } from '@wordpress/i18n';
import { Dropdown, Button } from '@wordpress/components';
import { Component } from '@wordpress/element';
import { withDispatch, withSelect } from '@wordpress/data';
import { compose, ifCondition } from '@wordpress/compose';
import { createBlock, store as blocksStore } from '@wordpress/blocks';
import { plus } from '@wordpress/icons';
/**
* Internal dependencies
*/
import InserterMenu from './menu';
import QuickInserter from './quick-inserter';
import { store as blockEditorStore } from '../../store';
const defaultRenderToggle = ( {
onToggle,
disabled,
isOpen,
blockTitle,
hasSingleBlockType,
toggleProps = {},
} ) => {
const {
as: Wrapper = Button,
label: labelProp,
onClick,
...rest
} = toggleProps;
let label = labelProp;
if ( ! label && hasSingleBlockType ) {
label = sprintf(
// translators: %s: the name of the block when there is only one
_x( 'Add %s', 'directly add the only allowed block' ),
blockTitle
);
} else if ( ! label ) {
label = _x( 'Add block', 'Generic label for block inserter button' );
}
// Handle both onClick functions from the toggle and the parent component.
function handleClick( event ) {
if ( onToggle ) {
onToggle( event );
}
if ( onClick ) {
onClick( event );
}
}
return (
<Wrapper
__next40pxDefaultSize={ toggleProps.as ? undefined : true }
icon={ plus }
label={ label }
tooltipPosition="bottom"
onClick={ handleClick }
className="block-editor-inserter__toggle"
aria-haspopup={ ! hasSingleBlockType ? 'true' : false }
aria-expanded={ ! hasSingleBlockType ? isOpen : false }
disabled={ disabled }
{ ...rest }
/>
);
};
class Inserter extends Component {
constructor() {
super( ...arguments );
this.onToggle = this.onToggle.bind( this );
this.renderToggle = this.renderToggle.bind( this );
this.renderContent = this.renderContent.bind( this );
}
onToggle( isOpen ) {
const { onToggle } = this.props;
// Surface toggle callback to parent component.
if ( onToggle ) {
onToggle( isOpen );
}
}
/**
* Render callback to display Dropdown toggle element.
*
* @param {Object} options
* @param {Function} options.onToggle Callback to invoke when toggle is
* pressed.
* @param {boolean} options.isOpen Whether dropdown is currently open.
*
* @return {Element} Dropdown toggle element.
*/
renderToggle( { onToggle, isOpen } ) {
const {
disabled,
blockTitle,
hasSingleBlockType,
directInsertBlock,
toggleProps,
hasItems,
renderToggle = defaultRenderToggle,
} = this.props;
return renderToggle( {
onToggle,
isOpen,
disabled: disabled || ! hasItems,
blockTitle,
hasSingleBlockType,
directInsertBlock,
toggleProps,
} );
}
/**
* Render callback to display Dropdown content element.
*
* @param {Object} options
* @param {Function} options.onClose Callback to invoke when dropdown is
* closed.
*
* @return {Element} Dropdown content element.
*/
renderContent( { onClose } ) {
const {
rootClientId,
clientId,
isAppender,
showInserterHelpPanel,
// This prop is experimental to give some time for the quick inserter to mature
// Feel free to make them stable after a few releases.
__experimentalIsQuick: isQuick,
onSelectOrClose,
selectBlockOnInsert,
} = this.props;
if ( isQuick ) {
return (
<QuickInserter
onSelect={ ( blocks ) => {
const firstBlock =
Array.isArray( blocks ) && blocks?.length
? blocks[ 0 ]
: blocks;
if (
onSelectOrClose &&
typeof onSelectOrClose === 'function'
) {
onSelectOrClose( firstBlock );
}
onClose();
} }
rootClientId={ rootClientId }
clientId={ clientId }
isAppender={ isAppender }
selectBlockOnInsert={ selectBlockOnInsert }
/>
);
}
return (
<InserterMenu
onSelect={ () => {
onClose();
} }
rootClientId={ rootClientId }
clientId={ clientId }
isAppender={ isAppender }
showInserterHelpPanel={ showInserterHelpPanel }
/>
);
}
render() {
const {
position,
hasSingleBlockType,
directInsertBlock,
insertOnlyAllowedBlock,
__experimentalIsQuick: isQuick,
onSelectOrClose,
} = this.props;
if ( hasSingleBlockType || directInsertBlock ) {
return this.renderToggle( { onToggle: insertOnlyAllowedBlock } );
}
return (
<Dropdown
className="block-editor-inserter"
contentClassName={ clsx( 'block-editor-inserter__popover', {
'is-quick': isQuick,
} ) }
popoverProps={ { position, shift: true } }
onToggle={ this.onToggle }
expandOnMobile
headerTitle={ __( 'Add a block' ) }
renderToggle={ this.renderToggle }
renderContent={ this.renderContent }
onClose={ onSelectOrClose }
/>
);
}
}
export default compose( [
withSelect(
( select, { clientId, rootClientId, shouldDirectInsert = true } ) => {
const {
getBlockRootClientId,
hasInserterItems,
getAllowedBlocks,
getDirectInsertBlock,
} = select( blockEditorStore );
const { getBlockVariations } = select( blocksStore );
rootClientId =
rootClientId || getBlockRootClientId( clientId ) || undefined;
const allowedBlocks = getAllowedBlocks( rootClientId );
const directInsertBlock =
shouldDirectInsert && getDirectInsertBlock( rootClientId );
const hasSingleBlockType =
allowedBlocks?.length === 1 &&
getBlockVariations( allowedBlocks[ 0 ].name, 'inserter' )
?.length === 0;
let allowedBlockType = false;
if ( hasSingleBlockType ) {
allowedBlockType = allowedBlocks[ 0 ];
}
return {
hasItems: hasInserterItems( rootClientId ),
hasSingleBlockType,
blockTitle: allowedBlockType ? allowedBlockType.title : '',
allowedBlockType,
directInsertBlock,
rootClientId,
};
}
),
withDispatch( ( dispatch, ownProps, { select } ) => {
return {
insertOnlyAllowedBlock() {
const {
rootClientId,
clientId,
isAppender,
hasSingleBlockType,
allowedBlockType,
directInsertBlock,
onSelectOrClose,
selectBlockOnInsert,
} = ownProps;
if ( ! hasSingleBlockType && ! directInsertBlock ) {
return;
}
function getAdjacentBlockAttributes( attributesToCopy ) {
const { getBlock, getPreviousBlockClientId } =
select( blockEditorStore );
if (
! attributesToCopy ||
( ! clientId && ! rootClientId )
) {
return {};
}
const result = {};
let adjacentAttributes = {};
// If there is no clientId, then attempt to get attributes
// from the last block within innerBlocks of the root block.
if ( ! clientId ) {
const parentBlock = getBlock( rootClientId );
if ( parentBlock?.innerBlocks?.length ) {
const lastInnerBlock =
parentBlock.innerBlocks[
parentBlock.innerBlocks.length - 1
];
if (
directInsertBlock &&
directInsertBlock?.name === lastInnerBlock.name
) {
adjacentAttributes = lastInnerBlock.attributes;
}
}
} else {
// Otherwise, attempt to get attributes from the
// previous block relative to the current clientId.
const currentBlock = getBlock( clientId );
const previousBlock = getBlock(
getPreviousBlockClientId( clientId )
);
if ( currentBlock?.name === previousBlock?.name ) {
adjacentAttributes =
previousBlock?.attributes || {};
}
}
// Copy over only those attributes flagged to be copied.
attributesToCopy.forEach( ( attribute ) => {
if ( adjacentAttributes.hasOwnProperty( attribute ) ) {
result[ attribute ] =
adjacentAttributes[ attribute ];
}
} );
return result;
}
function getInsertionIndex() {
const {
getBlockIndex,
getBlockSelectionEnd,
getBlockOrder,
getBlockRootClientId,
} = select( blockEditorStore );
// If the clientId is defined, we insert at the position of the block.
if ( clientId ) {
return getBlockIndex( clientId );
}
// If there a selected block, we insert after the selected block.
const end = getBlockSelectionEnd();
if (
! isAppender &&
end &&
getBlockRootClientId( end ) === rootClientId
) {
return getBlockIndex( end ) + 1;
}
// Otherwise, we insert at the end of the current rootClientId.
return getBlockOrder( rootClientId ).length;
}
const { insertBlock } = dispatch( blockEditorStore );
let blockToInsert;
// Attempt to augment the directInsertBlock with attributes from an adjacent block.
// This ensures styling from nearby blocks is preserved in the newly inserted block.
// See: https://github.com/WordPress/gutenberg/issues/37904
if ( directInsertBlock ) {
const newAttributes = getAdjacentBlockAttributes(
directInsertBlock.attributesToCopy
);
blockToInsert = createBlock( directInsertBlock.name, {
...( directInsertBlock.attributes || {} ),
...newAttributes,
} );
} else {
blockToInsert = createBlock( allowedBlockType.name );
}
insertBlock(
blockToInsert,
getInsertionIndex(),
rootClientId,
selectBlockOnInsert
);
if ( onSelectOrClose ) {
onSelectOrClose( {
clientId: blockToInsert?.clientId,
} );
}
const message = sprintf(
// translators: %s: the name of the block that has been added
__( '%s block added' ),
allowedBlockType.title
);
speak( message );
},
};
} ),
// The global inserter should always be visible, we are using ( ! isAppender && ! rootClientId && ! clientId ) as
// a way to detect the global Inserter.
ifCondition(
( { hasItems, isAppender, rootClientId, clientId } ) =>
hasItems || ( ! isAppender && ! rootClientId && ! clientId )
),
] )( Inserter );