UNPKG

@wordpress/block-editor

Version:
712 lines (665 loc) 19.6 kB
/** * External dependencies */ import { Pressable, View } from 'react-native'; import clsx from 'clsx'; /** * WordPress dependencies */ import { useCallback, useMemo, useState, useRef, memo, } from '@wordpress/element'; import { withFilters } from '@wordpress/components'; import { __experimentalGetAccessibleBlockLabel as getAccessibleBlockLabel, getBlockType, getDefaultBlockName, isUnmodifiedBlock, isUnmodifiedDefaultBlock, switchToBlockType, } from '@wordpress/blocks'; import { useDispatch, useSelect, withDispatch, withSelect, } from '@wordpress/data'; import { compose, ifCondition } from '@wordpress/compose'; /** * Internal dependencies */ import BlockEdit from '../block-edit'; import BlockDraggable from '../block-draggable'; import BlockInvalidWarning from './block-invalid-warning'; import BlockOutline from './block-outline'; import { store as blockEditorStore } from '../../store'; import { useLayout } from './layout'; import useScrollUponInsertion from './use-scroll-upon-insertion'; import { useSettings } from '../use-settings'; import { unlock } from '../../lock-unlock'; import BlockCrashBoundary from './block-crash-boundary'; import BlockCrashWarning from './block-crash-warning'; import { getMergedGlobalStyles, GlobalStylesContext, useGlobalStyles, useMobileGlobalStylesColors, } from '../global-styles/use-global-styles-context'; const EMPTY_ARRAY = []; /** * 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, }; // May be set to undefined, so check if the property is set! if ( propsA?.hasOwnProperty( 'className' ) && propsB?.hasOwnProperty( 'className' ) ) { newProps.className = clsx( propsA.className, propsB.className ); } if ( propsA?.hasOwnProperty( 'style' ) && propsB?.hasOwnProperty( 'style' ) ) { newProps.style = { ...propsA.style, ...propsB.style }; } return newProps; } function BlockWrapper( { accessibilityLabel, blockCategory, children, clientId, draggingClientId, draggingEnabled, hasInnerBlocks, isDescendentBlockSelected, isSelected, isTouchable, marginHorizontal, marginVertical, name, onFocus, } ) { const blockWrapperStyles = { flex: 1 }; const blockWrapperStyle = [ blockWrapperStyles, { marginVertical, marginHorizontal, }, ]; const accessible = ! ( isSelected || isDescendentBlockSelected ); const ref = useRef(); const [ isLayoutCalculated, setIsLayoutCalculated ] = useState(); useScrollUponInsertion( { clientId, isSelected, isLayoutCalculated, elementRef: ref, } ); const onLayout = useCallback( () => { setIsLayoutCalculated( true ); }, [] ); return ( <Pressable accessibilityLabel={ accessibilityLabel } accessibilityRole="button" accessible={ accessible } disabled={ ! isTouchable } onPress={ onFocus } style={ blockWrapperStyle } ref={ ref } onLayout={ onLayout } > <BlockOutline blockCategory={ blockCategory } hasInnerBlocks={ hasInnerBlocks } isSelected={ isSelected } name={ name } /> <BlockCrashBoundary blockName={ name } fallback={ <BlockCrashWarning /> } > <BlockDraggable clientId={ clientId } draggingClientId={ draggingClientId } enabled={ draggingEnabled } testID="draggable-trigger-content" > { children } </BlockDraggable> </BlockCrashBoundary> </Pressable> ); } function BlockListBlock( { attributes, blockWidth: blockWrapperWidth, canRemove, clientId, contentStyle, isLocked, isSelected, isSelectionEnabled, isStackedHorizontally, isValid, marginHorizontal, marginVertical, name, onDeleteBlock, onInsertBlocksAfter, onMerge, onReplace, parentBlockAlignment, parentWidth, rootClientId, setAttributes, toggleSelection, wrapperProps, } ) { const { baseGlobalStyles, blockCategory, blockType, draggingClientId, draggingEnabled, hasInnerBlocks, isDescendantOfParentSelected, isDescendentBlockSelected, isParentSelected, order, mayDisplayControls, blockEditingMode, } = useSelect( ( select ) => { const { getBlockCount, getBlockHierarchyRootClientId, getBlockIndex, getBlockParents, getSelectedBlockClientId, getSettings, hasSelectedInnerBlock, getBlockName, isFirstMultiSelectedBlock, getMultiSelectedBlockClientIds, getBlockEditingMode, } = select( blockEditorStore ); const currentBlockType = getBlockType( name || 'core/missing' ); const currentBlockCategory = currentBlockType?.category; const blockOrder = getBlockIndex( clientId ); const descendentBlockSelected = hasSelectedInnerBlock( clientId, true ); const selectedBlockClientId = getSelectedBlockClientId(); const parents = getBlockParents( clientId, true ); const parentSelected = // Set false as a default value to prevent re-render when it's changed from null to false. ( selectedBlockClientId || false ) && selectedBlockClientId === rootClientId; const selectedParents = clientId ? parents : []; const descendantOfParentSelected = selectedParents.includes( rootClientId ); const blockHasInnerBlocks = getBlockCount( clientId ) > 0; // For blocks with inner blocks, we only enable the dragging in the nested // blocks if any of them are selected. This way we prevent the long-press // gesture from being disabled for elements within the block UI. const isDraggingEnabled = ! blockHasInnerBlocks || isSelected || ! descendentBlockSelected; // Dragging nested blocks is not supported yet. For this reason, the block to be dragged // will be the top in the hierarchy. const currentDraggingClientId = getBlockHierarchyRootClientId( clientId ); const globalStylesBaseStyles = getSettings()?.__experimentalGlobalStylesBaseStyles; return { baseGlobalStyles: globalStylesBaseStyles, blockCategory: currentBlockCategory, blockType: currentBlockType, draggingClientId: currentDraggingClientId, draggingEnabled: isDraggingEnabled, hasInnerBlocks: blockHasInnerBlocks, isDescendantOfParentSelected: descendantOfParentSelected, isDescendentBlockSelected: descendentBlockSelected, isParentSelected: parentSelected, order: blockOrder, mayDisplayControls: isSelected || ( isFirstMultiSelectedBlock( clientId ) && getMultiSelectedBlockClientIds().every( ( id ) => getBlockName( id ) === name ) ), blockEditingMode: getBlockEditingMode( clientId ), }; }, [ clientId, isSelected, name, rootClientId ] ); const { removeBlock, selectBlock } = useDispatch( blockEditorStore ); const initialBlockWidth = blockWrapperWidth - 2 * marginHorizontal; const [ blockWidth, setBlockWidth ] = useState( initialBlockWidth ); const parentLayout = useLayout() || {}; const defaultColors = useMobileGlobalStylesColors(); const globalStyle = useGlobalStyles(); const [ fontSizes ] = useSettings( 'typography.fontSizes' ); const onRemove = useCallback( () => removeBlock( clientId ), [ clientId, removeBlock ] ); const onFocus = useCallback( () => { if ( ! isSelected ) { selectBlock( clientId ); } }, [ selectBlock, clientId, isSelected ] ); const onLayout = useCallback( ( { nativeEvent } ) => { const layoutWidth = Math.floor( nativeEvent.layout.width ); if ( ! blockWidth || ! layoutWidth ) { return; } if ( blockWidth !== layoutWidth ) { setBlockWidth( layoutWidth ); } }, [ blockWidth, setBlockWidth ] ); // Determine whether the block has props to apply to the wrapper. if ( blockType?.getEditWrapperProps ) { wrapperProps = mergeWrapperProps( wrapperProps, blockType.getEditWrapperProps( attributes ) ); } // Inherited styles merged with block level styles. const mergedStyle = useMemo( () => { return getMergedGlobalStyles( baseGlobalStyles, globalStyle, wrapperProps?.style, attributes, defaultColors, name, fontSizes || EMPTY_ARRAY ); }, [ // It is crucial to keep the dependencies array minimal to prevent unnecessary calls that could negatively impact performance. // JSON.stringify is used for the following purposes: // 1. To create a single, comparable value from the globalStyle, wrapperProps.style, and attributes objects. This allows useMemo to // efficiently determine if a change has occurred in any of these objects. // 2. To filter the attributes object, ensuring that only the relevant attributes (included in // GlobalStylesContext.BLOCK_STYLE_ATTRIBUTES) are considered as dependencies. This reduces the likelihood of // unnecessary useMemo calls when other, unrelated attributes change. JSON.stringify( globalStyle ), JSON.stringify( wrapperProps?.style ), JSON.stringify( Object.fromEntries( Object.entries( attributes ?? {} ).filter( ( [ key ] ) => GlobalStylesContext.BLOCK_STYLE_ATTRIBUTES.includes( key ) ) ) ), ] ); const isFocused = isSelected || isDescendentBlockSelected; const isTouchable = isSelected || isDescendantOfParentSelected || isParentSelected || ! rootClientId; const accessibilityLabel = getAccessibleBlockLabel( blockType, attributes, order + 1 ); return ( <BlockWrapper accessibilityLabel={ accessibilityLabel } blockCategory={ blockCategory } clientId={ clientId } draggingClientId={ draggingClientId } draggingEnabled={ draggingEnabled } hasInnerBlocks={ hasInnerBlocks } isDescendentBlockSelected={ isDescendentBlockSelected } isFocused={ isFocused } isSelected={ isSelected } isStackedHorizontally={ isStackedHorizontally } isTouchable={ isTouchable } marginHorizontal={ marginHorizontal } marginVertical={ marginVertical } name={ name } onFocus={ onFocus } > { () => ! isValid ? ( <BlockInvalidWarning clientId={ clientId } /> ) : ( <GlobalStylesContext.Provider value={ mergedStyle }> <BlockEdit attributes={ attributes } blockWidth={ blockWidth } clientId={ clientId } contentStyle={ contentStyle } insertBlocksAfter={ isLocked ? undefined : onInsertBlocksAfter } isSelected={ isSelected } isSelectionEnabled={ isSelectionEnabled } mergeBlocks={ canRemove ? onMerge : undefined } name={ name } onDeleteBlock={ onDeleteBlock } onFocus={ onFocus } onRemove={ canRemove ? onRemove : undefined } onReplace={ canRemove ? onReplace : undefined } parentBlockAlignment={ parentBlockAlignment } parentWidth={ parentWidth } setAttributes={ setAttributes } style={ mergedStyle } toggleSelection={ toggleSelection } __unstableParentLayout={ Object.keys( parentLayout ).length ? parentLayout : undefined } wrapperProps={ wrapperProps } mayDisplayControls={ mayDisplayControls } blockEditingMode={ blockEditingMode } /> <View onLayout={ onLayout } /> </GlobalStylesContext.Provider> ) } </BlockWrapper> ); } const applyWithSelect = withSelect( ( select, { clientId, rootClientId } ) => { const { isBlockSelected, getBlockMode, isSelectionEnabled, getTemplateLock, getBlockWithoutAttributes, getBlockAttributes, canRemoveBlock, canMoveBlock, } = unlock( select( blockEditorStore ) ); const block = getBlockWithoutAttributes( clientId ); const attributes = getBlockAttributes( clientId ); const isSelected = isBlockSelected( clientId ); const templateLock = getTemplateLock( rootClientId ); const canRemove = canRemoveBlock( clientId ); const canMove = canMoveBlock( clientId ); // 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, 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 ); } else if ( getBlockName( clientId ) !== getDefaultBlockName() ) { const replacement = switchToBlockType( getBlock( clientId ), getDefaultBlockName() ); if ( replacement && replacement.length ) { replaceBlocks( clientId, replacement ); } } } }, onReplace( blocks, indexToSelect, initialPosition, meta ) { if ( blocks.length && ! isUnmodifiedDefaultBlock( blocks[ blocks.length - 1 ] ) ) { __unstableMarkLastChangeAsPersistent(); } replaceBlocks( [ ownProps.clientId ], blocks, indexToSelect, initialPosition, meta ); }, toggleSelection( selectionEnabled ) { toggleSelection( selectionEnabled ); }, }; } ); export default compose( memo, 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 );