@wordpress/block-editor
Version:
422 lines (391 loc) • 10.9 kB
JavaScript
/**
* External dependencies
*/
import { View, Text, TouchableWithoutFeedback, Dimensions } from 'react-native';
import { pick } from 'lodash';
/**
* WordPress dependencies
*/
import { Component, createRef, useMemo } from '@wordpress/element';
import {
GlobalStylesContext,
getMergedGlobalStyles,
alignmentHelpers,
useGlobalStyles,
} from '@wordpress/components';
import { withDispatch, withSelect } from '@wordpress/data';
import { compose, withPreferredColorScheme } from '@wordpress/compose';
import {
getBlockType,
__experimentalGetAccessibleBlockLabel as getAccessibleBlockLabel,
} from '@wordpress/blocks';
import { __experimentalUseEditorFeature as useEditorFeature } from '@wordpress/block-editor';
/**
* Internal dependencies
*/
import styles from './block.scss';
import BlockEdit from '../block-edit';
import BlockInvalidWarning from './block-invalid-warning';
import BlockMobileToolbar from '../block-mobile-toolbar';
import { store as blockEditorStore } from '../../store';
const emptyArray = [];
function BlockForType( {
attributes,
clientId,
contentStyle,
getBlockWidth,
insertBlocksAfter,
isSelected,
mergeBlocks,
name,
onBlockFocus,
onChange,
onDeleteBlock,
onReplace,
parentWidth,
wrapperProps,
blockWidth,
} ) {
const defaultColors = useEditorFeature( 'color.palette' ) || emptyArray;
const globalStyle = useGlobalStyles();
const mergedStyle = useMemo( () => {
return getMergedGlobalStyles(
globalStyle,
wrapperProps.style,
attributes,
defaultColors
);
}, [
defaultColors,
globalStyle,
// I couldn't simply use attributes and wrapperProps.styles as a dependency because they are almost always a new reference.
// Thanks to the JSON.stringify we check if the value is the same instead of reference.
JSON.stringify( wrapperProps.style ),
JSON.stringify(
pick( attributes, GlobalStylesContext.BLOCK_STYLE_ATTRIBUTES )
),
] );
return (
<GlobalStylesContext.Provider value={ mergedStyle }>
<BlockEdit
name={ name }
isSelected={ isSelected }
attributes={ attributes }
setAttributes={ onChange }
onFocus={ onBlockFocus }
onReplace={ onReplace }
insertBlocksAfter={ insertBlocksAfter }
mergeBlocks={ mergeBlocks }
// Block level styles
wrapperProps={ wrapperProps }
// inherited styles merged with block level styles
mergedStyle={ mergedStyle }
clientId={ clientId }
parentWidth={ parentWidth }
contentStyle={ contentStyle }
onDeleteBlock={ onDeleteBlock }
blockWidth={ blockWidth }
/>
<View onLayout={ getBlockWidth } />
</GlobalStylesContext.Provider>
);
}
class BlockListBlock extends Component {
constructor() {
super( ...arguments );
this.insertBlocksAfter = this.insertBlocksAfter.bind( this );
this.onFocus = this.onFocus.bind( this );
this.getBlockWidth = this.getBlockWidth.bind( this );
this.state = {
blockWidth: this.props.blockWidth - 2 * this.props.marginHorizontal,
};
this.anchorNodeRef = createRef();
}
onFocus() {
const { firstToSelectId, isSelected, onSelect } = this.props;
if ( ! isSelected ) {
onSelect( firstToSelectId );
}
}
insertBlocksAfter( blocks ) {
this.props.onInsertBlocks( blocks, this.props.order + 1 );
if ( blocks[ 0 ] ) {
// focus on the first block inserted
this.props.onSelect( blocks[ 0 ].clientId );
}
}
getBlockWidth( { nativeEvent } ) {
const { layout } = nativeEvent;
const { blockWidth } = this.state;
const layoutWidth = Math.floor( layout.width );
if ( ! blockWidth || ! layoutWidth ) {
return;
}
if ( blockWidth !== layoutWidth ) {
this.setState( { blockWidth: layoutWidth } );
}
}
getBlockForType() {
const { blockWidth } = this.state;
return (
<BlockForType
{ ...this.props }
onBlockFocus={ this.onFocus }
insertBlocksAfter={ this.insertBlocksAfter }
getBlockWidth={ this.getBlockWidth }
blockWidth={ blockWidth }
/>
);
}
renderBlockTitle() {
return (
<View style={ styles.blockTitle }>
<Text>BlockType: { this.props.name }</Text>
</View>
);
}
render() {
const {
attributes,
blockType,
clientId,
icon,
isSelected,
isValid,
order,
title,
isDimmed,
isTouchable,
onDeleteBlock,
isStackedHorizontally,
isParentSelected,
getStylesFromColorScheme,
marginVertical,
marginHorizontal,
isInnerBlockSelected,
name,
} = this.props;
if ( ! attributes || ! blockType ) {
return null;
}
const { blockWidth } = this.state;
const { align } = attributes;
const accessibilityLabel = getAccessibleBlockLabel(
blockType,
attributes,
order + 1
);
const { isFullWidth, isWider, isContainerRelated } = alignmentHelpers;
const accessible = ! ( isSelected || isInnerBlockSelected );
const screenWidth = Math.floor( Dimensions.get( 'window' ).width );
const isScreenWidthEqual = blockWidth === screenWidth;
const isScreenWidthWider = blockWidth < screenWidth;
const isFullWidthToolbar = isFullWidth( align ) || isScreenWidthEqual;
return (
<TouchableWithoutFeedback
onPress={ this.onFocus }
accessible={ accessible }
accessibilityRole={ 'button' }
>
<View
style={ { flex: 1 } }
accessibilityLabel={ accessibilityLabel }
>
<View
pointerEvents={ isTouchable ? 'auto' : 'box-only' }
accessibilityLabel={ accessibilityLabel }
style={ [
{ marginVertical, marginHorizontal, flex: 1 },
isDimmed && styles.dimmed,
] }
>
{ isSelected && (
<View
pointerEvents="box-none"
style={ [
styles.solidBorder,
isFullWidth( align ) &&
isScreenWidthWider &&
styles.borderFullWidth,
isFullWidth( align ) &&
isContainerRelated( name ) &&
isScreenWidthWider &&
styles.containerBorderFullWidth,
getStylesFromColorScheme(
styles.solidBorderColor,
styles.solidBorderColorDark
),
] }
/>
) }
{ isParentSelected && (
<View
style={ [
styles.dashedBorder,
getStylesFromColorScheme(
styles.dashedBorderColor,
styles.dashedBorderColorDark
),
] }
/>
) }
{ isValid ? (
this.getBlockForType()
) : (
<BlockInvalidWarning
blockTitle={ title }
icon={ icon }
/>
) }
<View
style={ [
styles.neutralToolbar,
! isFullWidthToolbar &&
isContainerRelated( name ) &&
isWider( screenWidth, 'mobile' ) &&
styles.containerToolbar,
] }
ref={ this.anchorNodeRef }
>
{ isSelected && (
<BlockMobileToolbar
clientId={ clientId }
onDelete={ onDeleteBlock }
isStackedHorizontally={
isStackedHorizontally
}
blockWidth={ blockWidth }
anchorNodeRef={ this.anchorNodeRef.current }
isFullWidth={ isFullWidthToolbar }
/>
) }
</View>
</View>
</View>
</TouchableWithoutFeedback>
);
}
}
// Helper function to memoize the wrapperProps since getEditWrapperProps always returns a new reference
const wrapperPropsCache = new WeakMap();
const emptyObj = {};
function getWrapperProps( value, getWrapperPropsFunction ) {
if ( ! getWrapperPropsFunction ) {
return emptyObj;
}
const cachedValue = wrapperPropsCache.get( value );
if ( ! cachedValue ) {
const wrapperProps = getWrapperPropsFunction( value );
wrapperPropsCache.set( value, wrapperProps );
return wrapperProps;
}
return cachedValue;
}
export default compose( [
withSelect( ( select, { clientId, rootClientId } ) => {
const {
getBlockIndex,
isBlockSelected,
__unstableGetBlockWithoutInnerBlocks,
getSelectedBlockClientId,
getLowestCommonAncestorWithSelectedBlock,
getBlockParents,
hasSelectedInnerBlock,
} = select( blockEditorStore );
const order = getBlockIndex( clientId, rootClientId );
const isSelected = isBlockSelected( clientId );
const isInnerBlockSelected = hasSelectedInnerBlock( clientId );
const block = __unstableGetBlockWithoutInnerBlocks( clientId );
const { name, attributes, isValid } = block || {};
const blockType = getBlockType( name || 'core/missing' );
const title = blockType.title;
const icon = blockType.icon;
const parents = getBlockParents( clientId, true );
const parentId = parents[ 0 ] || '';
const selectedBlockClientId = getSelectedBlockClientId();
const commonAncestor = getLowestCommonAncestorWithSelectedBlock(
clientId
);
const commonAncestorIndex = parents.indexOf( commonAncestor ) - 1;
const firstToSelectId = commonAncestor
? parents[ commonAncestorIndex ]
: parents[ parents.length - 1 ];
const isParentSelected =
// set false as a default value to prevent re-render when it's changed from null to false
( selectedBlockClientId || false ) &&
selectedBlockClientId === parentId;
const selectedParents = selectedBlockClientId
? getBlockParents( selectedBlockClientId )
: [];
const isDescendantOfParentSelected = selectedParents.includes(
parentId
);
const isTouchable =
isSelected ||
isDescendantOfParentSelected ||
isParentSelected ||
parentId === '';
return {
icon,
name: name || 'core/missing',
order,
title,
attributes,
blockType,
isSelected,
isInnerBlockSelected,
isValid,
isParentSelected,
firstToSelectId,
isTouchable,
wrapperProps: getWrapperProps(
attributes,
blockType.getEditWrapperProps
),
};
} ),
withDispatch( ( dispatch, ownProps, { select } ) => {
const {
insertBlocks,
mergeBlocks,
replaceBlocks,
selectBlock,
updateBlockAttributes,
} = dispatch( blockEditorStore );
return {
mergeBlocks( forward ) {
const { clientId } = ownProps;
const {
getPreviousBlockClientId,
getNextBlockClientId,
} = select( blockEditorStore );
if ( forward ) {
const nextBlockClientId = getNextBlockClientId( clientId );
if ( nextBlockClientId ) {
mergeBlocks( clientId, nextBlockClientId );
}
} else {
const previousBlockClientId = getPreviousBlockClientId(
clientId
);
if ( previousBlockClientId ) {
mergeBlocks( previousBlockClientId, clientId );
}
}
},
onInsertBlocks( blocks, index ) {
insertBlocks( blocks, index, ownProps.rootClientId );
},
onSelect( clientId = ownProps.clientId, initialPosition ) {
selectBlock( clientId, initialPosition );
},
onChange: ( attributes ) => {
updateBlockAttributes( ownProps.clientId, attributes );
},
onReplace( blocks, indexToSelect ) {
replaceBlocks( [ ownProps.clientId ], blocks, indexToSelect );
},
};
} ),
withPreferredColorScheme,
] )( BlockListBlock );