UNPKG

@wordpress/block-editor

Version:
325 lines (310 loc) 8.86 kB
/** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; import { getBlockBindingsSource, getBlockBindingsSources, getBlockType, } from '@wordpress/blocks'; import { __experimentalItemGroup as ItemGroup, __experimentalItem as Item, __experimentalText as Text, __experimentalToolsPanel as ToolsPanel, __experimentalToolsPanelItem as ToolsPanelItem, __experimentalVStack as VStack, privateApis as componentsPrivateApis, } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { useContext, Fragment } from '@wordpress/element'; import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies */ import { canBindAttribute, getBindableAttributes, useBlockBindingsUtils, } from '../utils/block-bindings'; import { unlock } from '../lock-unlock'; import InspectorControls from '../components/inspector-controls'; import BlockContext from '../components/block-context'; import { useBlockEditContext } from '../components/block-edit'; import { store as blockEditorStore } from '../store'; const { Menu } = unlock( componentsPrivateApis ); const EMPTY_OBJECT = {}; const useToolsPanelDropdownMenuProps = () => { const isMobile = useViewportMatch( 'medium', '<' ); return ! isMobile ? { popoverProps: { placement: 'left-start', // For non-mobile, inner sidebar width (248px) - button width (24px) - border (1px) + padding (16px) + spacing (20px) offset: 259, }, } : {}; }; function BlockBindingsPanelMenuContent( { fieldsList, attribute, binding } ) { const { clientId } = useBlockEditContext(); const registeredSources = getBlockBindingsSources(); const { updateBlockBindings } = useBlockBindingsUtils(); const currentKey = binding?.args?.key; const attributeType = useSelect( ( select ) => { const { name: blockName } = select( blockEditorStore ).getBlock( clientId ); const _attributeType = getBlockType( blockName ).attributes?.[ attribute ]?.type; return _attributeType === 'rich-text' ? 'string' : _attributeType; }, [ clientId, attribute ] ); return ( <> { Object.entries( fieldsList ).map( ( [ name, fields ], i ) => ( <Fragment key={ name }> <Menu.Group> { Object.keys( fieldsList ).length > 1 && ( <Menu.GroupLabel> { registeredSources[ name ].label } </Menu.GroupLabel> ) } { Object.entries( fields ) .filter( ( [ , args ] ) => args?.type === attributeType ) .map( ( [ key, args ] ) => ( <Menu.RadioItem key={ key } onChange={ () => updateBlockBindings( { [ attribute ]: { source: name, args: { key }, }, } ) } name={ attribute + '-binding' } value={ key } checked={ key === currentKey } > <Menu.ItemLabel> { args?.label } </Menu.ItemLabel> <Menu.ItemHelpText> { args?.value } </Menu.ItemHelpText> </Menu.RadioItem> ) ) } </Menu.Group> { i !== Object.keys( fieldsList ).length - 1 && ( <Menu.Separator /> ) } </Fragment> ) ) } </> ); } function BlockBindingsAttribute( { attribute, binding, fieldsList } ) { const { source: sourceName, args } = binding || {}; const sourceProps = getBlockBindingsSource( sourceName ); const isSourceInvalid = ! sourceProps; return ( <VStack className="block-editor-bindings__item" spacing={ 0 }> <Text truncate>{ attribute }</Text> { !! binding && ( <Text truncate variant={ ! isSourceInvalid && 'muted' } isDestructive={ isSourceInvalid } > { isSourceInvalid ? __( 'Invalid source' ) : fieldsList?.[ sourceName ]?.[ args?.key ]?.label || sourceProps?.label || sourceName } </Text> ) } </VStack> ); } function ReadOnlyBlockBindingsPanelItems( { bindings, fieldsList } ) { return ( <> { Object.entries( bindings ).map( ( [ attribute, binding ] ) => ( <Item key={ attribute }> <BlockBindingsAttribute attribute={ attribute } binding={ binding } fieldsList={ fieldsList } /> </Item> ) ) } </> ); } function EditableBlockBindingsPanelItems( { attributes, bindings, fieldsList, } ) { const { updateBlockBindings } = useBlockBindingsUtils(); const isMobile = useViewportMatch( 'medium', '<' ); return ( <> { attributes.map( ( attribute ) => { const binding = bindings[ attribute ]; return ( <ToolsPanelItem key={ attribute } hasValue={ () => !! binding } label={ attribute } onDeselect={ () => { updateBlockBindings( { [ attribute ]: undefined, } ); } } > <Menu placement={ isMobile ? 'bottom-start' : 'left-start' } > <Menu.TriggerButton render={ <Item /> }> <BlockBindingsAttribute attribute={ attribute } binding={ binding } fieldsList={ fieldsList } /> </Menu.TriggerButton> <Menu.Popover gutter={ isMobile ? 8 : 36 }> <BlockBindingsPanelMenuContent fieldsList={ fieldsList } attribute={ attribute } binding={ binding } /> </Menu.Popover> </Menu> </ToolsPanelItem> ); } ) } </> ); } export const BlockBindingsPanel = ( { name: blockName, metadata } ) => { const blockContext = useContext( BlockContext ); const { removeAllBlockBindings } = useBlockBindingsUtils(); const bindableAttributes = getBindableAttributes( blockName ); const dropdownMenuProps = useToolsPanelDropdownMenuProps(); // `useSelect` is used purposely here to ensure `getFieldsList` // is updated whenever there are updates in block context. // `source.getFieldsList` may also call a selector via `select`. const _fieldsList = {}; const { fieldsList, canUpdateBlockBindings } = useSelect( ( select ) => { if ( ! bindableAttributes || bindableAttributes.length === 0 ) { return EMPTY_OBJECT; } const registeredSources = getBlockBindingsSources(); Object.entries( registeredSources ).forEach( ( [ sourceName, { getFieldsList, usesContext } ] ) => { if ( getFieldsList ) { // Populate context. const context = {}; if ( usesContext?.length ) { for ( const key of usesContext ) { context[ key ] = blockContext[ key ]; } } const sourceList = getFieldsList( { select, context, } ); // Only add source if the list is not empty. if ( Object.keys( sourceList || {} ).length ) { _fieldsList[ sourceName ] = { ...sourceList }; } } } ); return { fieldsList: Object.values( _fieldsList ).length > 0 ? _fieldsList : EMPTY_OBJECT, canUpdateBlockBindings: select( blockEditorStore ).getSettings() .canUpdateBlockBindings, }; }, [ blockContext, bindableAttributes ] ); // Return early if there are no bindable attributes. if ( ! bindableAttributes || bindableAttributes.length === 0 ) { return null; } // Filter bindings to only show bindable attributes and remove pattern overrides. const { bindings } = metadata || {}; const filteredBindings = { ...bindings }; Object.keys( filteredBindings ).forEach( ( key ) => { if ( ! canBindAttribute( blockName, key ) || filteredBindings[ key ].source === 'core/pattern-overrides' ) { delete filteredBindings[ key ]; } } ); // Lock the UI when the user can't update bindings or there are no fields to connect to. const readOnly = ! canUpdateBlockBindings || ! Object.keys( fieldsList ).length; if ( readOnly && Object.keys( filteredBindings ).length === 0 ) { return null; } return ( <InspectorControls group="bindings"> <ToolsPanel label={ __( 'Attributes' ) } resetAll={ () => { removeAllBlockBindings(); } } dropdownMenuProps={ dropdownMenuProps } className="block-editor-bindings__panel" > <ItemGroup isBordered isSeparated> { readOnly ? ( <ReadOnlyBlockBindingsPanelItems bindings={ filteredBindings } fieldsList={ fieldsList } /> ) : ( <EditableBlockBindingsPanelItems attributes={ bindableAttributes } bindings={ filteredBindings } fieldsList={ fieldsList } /> ) } </ItemGroup> { /* Use a div element to make the ToolsPanelHiddenInnerWrapper toggle the visibility of this help text automatically. */ } <Text as="div" variant="muted"> <p> { __( 'Attributes connected to custom fields or other dynamic data.' ) } </p> </Text> </ToolsPanel> </InspectorControls> ); }; export default { edit: BlockBindingsPanel, attributeKeys: [ 'metadata' ], hasSupport() { return true; }, };