block-edit.js
3 weeks ago
block-form.js
2 months ago
block-placeholder.js
6 months ago
block-preview.js
6 months ago
block-toolbar-fields.js
2 months ago
error-boundary.js
2 months ago
inline-editing-toolbar.js
6 months ago
jsx-parser.js
2 months ago
popover-wrapper.js
2 months ago
block-toolbar-fields.js
263 lines
| 1 | /** |
| 2 | * BlockToolbarFields Component |
| 3 | * Renders field buttons in the WordPress block toolbar (top toolbar) |
| 4 | * Allows quick access to edit specific fields from the block toolbar |
| 5 | */ |
| 6 | |
| 7 | /* global acf */ |
| 8 | |
| 9 | import { useState, useRef } from '@wordpress/element'; |
| 10 | import { BlockControls } from '@wordpress/blockEditor'; |
| 11 | import { ToolbarGroup, ToolbarButton, Modal } from '@wordpress/components'; |
| 12 | import { PopoverWrapper } from './popover-wrapper'; |
| 13 | |
| 14 | /** |
| 15 | * BlockToolbarFields component |
| 16 | * Displays field editing buttons in the WordPress block toolbar |
| 17 | * |
| 18 | * @param {Object} props - Component props |
| 19 | * @param {Array} props.blockToolbarFields - Array of field names or field config objects to show in toolbar |
| 20 | * @param {Array} props.blockFieldInfo - Array of all field information |
| 21 | * @param {Function} props.setCurrentBlockFormContainer - Setter for form container ref |
| 22 | * @param {Document|HTMLIFrameElement} props.gutenbergIframeOrDocument - Document or iframe reference |
| 23 | * @param {Function} props.setBlockFormModalOpen - Setter for block form modal state |
| 24 | * @param {boolean} props.blockFormModalOpen - Whether block form modal is open |
| 25 | * @param {boolean} props.hideExpandedEditorBtnInToolbar - Whether to hide the expanded editor button in the toolbar |
| 26 | * @return {JSX.Element} - Rendered toolbar controls |
| 27 | */ |
| 28 | export const BlockToolbarFields = ( { |
| 29 | blockToolbarFields, |
| 30 | blockFieldInfo, |
| 31 | setCurrentBlockFormContainer, |
| 32 | gutenbergIframeOrDocument, |
| 33 | setBlockFormModalOpen, |
| 34 | blockFormModalOpen, |
| 35 | hideExpandedEditorBtnInToolbar, |
| 36 | } ) => { |
| 37 | const [ selectedFieldKey, setSelectedFieldKey ] = useState( null ); |
| 38 | const [ selectedFieldButtonRef, setSelectedFieldButtonRef ] = useState(); |
| 39 | const [ usePopover, setUsePopover ] = useState( true ); |
| 40 | const fieldPopoverContainerRef = useRef(); |
| 41 | |
| 42 | /** |
| 43 | * Get field type class name from field name |
| 44 | * |
| 45 | * @param {string} fieldName Field name. |
| 46 | * @return {string} Field type class name. |
| 47 | */ |
| 48 | const getFieldTypeClassName = ( fieldName ) => { |
| 49 | if ( ! fieldName || ! blockFieldInfo ) { |
| 50 | return ''; |
| 51 | } |
| 52 | const field = blockFieldInfo.find( ( f ) => f.name === fieldName ); |
| 53 | return field?.type ? field.type.replace( /_/g, '-' ) : ''; |
| 54 | }; |
| 55 | |
| 56 | /** |
| 57 | * Get field info object from field name |
| 58 | * |
| 59 | * @param {string} fieldName Field name. |
| 60 | * @return {Object|null} Field info object. |
| 61 | */ |
| 62 | const getFieldInfo = ( fieldName ) => { |
| 63 | return fieldName && blockFieldInfo |
| 64 | ? blockFieldInfo.find( ( f ) => f.name === fieldName ) |
| 65 | : null; |
| 66 | }; |
| 67 | |
| 68 | /** |
| 69 | * Get field label from field name |
| 70 | * |
| 71 | * @param {string} fieldName Field name. |
| 72 | * @return {string} Field label. |
| 73 | */ |
| 74 | const getFieldLabel = ( fieldName ) => { |
| 75 | if ( ! fieldName || ! blockFieldInfo ) { |
| 76 | return fieldName || ''; |
| 77 | } |
| 78 | const field = blockFieldInfo.find( ( f ) => f.name === fieldName ); |
| 79 | // Fallback to fieldName when label not found to ensure toolbar shows a title |
| 80 | return field?.label || fieldName || ''; |
| 81 | }; |
| 82 | return ( |
| 83 | <BlockControls> |
| 84 | { /* Edit Block button */ } |
| 85 | { ! hideExpandedEditorBtnInToolbar && ( |
| 86 | <ToolbarGroup> |
| 87 | <ToolbarButton |
| 88 | className="components-icon-button components-toolbar__control" |
| 89 | label={ acf.__( 'Edit Block' ) } |
| 90 | icon="edit" |
| 91 | onClick={ () => { |
| 92 | setBlockFormModalOpen( true ); |
| 93 | } } |
| 94 | isPressed={ blockFormModalOpen } |
| 95 | /> |
| 96 | </ToolbarGroup> |
| 97 | ) } |
| 98 | |
| 99 | { /* Field buttons */ } |
| 100 | { blockToolbarFields.length > 0 && ( |
| 101 | <ToolbarGroup> |
| 102 | { /* Style to show selected field in form */ } |
| 103 | { ( () => { |
| 104 | const styleContent = `[data-name="${ selectedFieldKey }"]{ display: block!important; }`; |
| 105 | return <style>{ styleContent }</style>; |
| 106 | } )() } |
| 107 | |
| 108 | { /* Render field buttons */ } |
| 109 | { blockToolbarFields && |
| 110 | blockToolbarFields.map( ( field ) => { |
| 111 | let fieldName = ''; |
| 112 | let fieldIconSvg = null; |
| 113 | let fieldLabel = null; |
| 114 | |
| 115 | // Handle field config object or simple string |
| 116 | if ( typeof field === 'object' ) { |
| 117 | fieldName = field.fieldName |
| 118 | ? field.fieldName |
| 119 | : field.index; |
| 120 | fieldIconSvg = field.fieldIcon |
| 121 | ? window.atob( field.fieldIcon ) |
| 122 | : null; |
| 123 | fieldLabel = field.fieldLabel |
| 124 | ? field.fieldLabel |
| 125 | : fieldName; |
| 126 | } else { |
| 127 | fieldName = field; |
| 128 | } |
| 129 | |
| 130 | return ( |
| 131 | <ToolbarButton |
| 132 | key={ fieldName } |
| 133 | icon={ |
| 134 | fieldIconSvg ? ( |
| 135 | <i |
| 136 | dangerouslySetInnerHTML={ { |
| 137 | __html: fieldIconSvg, |
| 138 | } } |
| 139 | /> |
| 140 | ) : ( |
| 141 | <i |
| 142 | className={ `field-type-icon field-type-icon-${ getFieldTypeClassName( |
| 143 | fieldName |
| 144 | ) }` } |
| 145 | /> |
| 146 | ) |
| 147 | } |
| 148 | label={ |
| 149 | fieldLabel || getFieldLabel( fieldName ) |
| 150 | } |
| 151 | isPressed={ fieldName === selectedFieldKey } |
| 152 | ref={ |
| 153 | fieldName === selectedFieldKey |
| 154 | ? setSelectedFieldButtonRef |
| 155 | : null |
| 156 | } |
| 157 | onMouseDown={ () => { |
| 158 | if ( selectedFieldKey === fieldName ) { |
| 159 | setSelectedFieldKey( null ); |
| 160 | } else { |
| 161 | setSelectedFieldKey( null ); |
| 162 | setTimeout( () => { |
| 163 | const fieldInfo = |
| 164 | getFieldInfo( fieldName ); |
| 165 | // Use modal for complex field types |
| 166 | if ( |
| 167 | fieldInfo?.type === |
| 168 | 'flexible_content' || |
| 169 | fieldInfo?.type === |
| 170 | 'repeater' |
| 171 | ) { |
| 172 | setUsePopover( false ); |
| 173 | } else { |
| 174 | setUsePopover( true ); |
| 175 | } |
| 176 | setSelectedFieldKey( |
| 177 | fieldName |
| 178 | ); |
| 179 | } ); |
| 180 | } |
| 181 | } } |
| 182 | /> |
| 183 | ); |
| 184 | } ) } |
| 185 | |
| 186 | { /* Popover for simple field types */ } |
| 187 | { selectedFieldKey && |
| 188 | selectedFieldButtonRef && |
| 189 | usePopover && ( |
| 190 | <PopoverWrapper |
| 191 | focusOnMount={ false } |
| 192 | className="acf-inline-fields-popover" |
| 193 | anchor={ selectedFieldButtonRef } |
| 194 | animate={ true } |
| 195 | onClose={ ( event ) => { |
| 196 | // Don't close on certain events |
| 197 | if ( |
| 198 | event.key !== 'Escape' && |
| 199 | ( event?.target.closest( |
| 200 | '.media-modal' |
| 201 | ) || |
| 202 | event?.target.closest( |
| 203 | '.acf-tooltip' |
| 204 | ) || |
| 205 | ( event?.target && |
| 206 | fieldPopoverContainerRef?.current && |
| 207 | fieldPopoverContainerRef?.current.contains( |
| 208 | event.target |
| 209 | ) ) || |
| 210 | ( selectedFieldButtonRef?.current && |
| 211 | selectedFieldButtonRef?.current.contains( |
| 212 | event?.target |
| 213 | ) ) ) |
| 214 | ) { |
| 215 | return false; |
| 216 | } |
| 217 | setSelectedFieldKey( null ); |
| 218 | return true; |
| 219 | } } |
| 220 | variant="unstyled" |
| 221 | gutenbergIframeOrDocument={ |
| 222 | gutenbergIframeOrDocument |
| 223 | } |
| 224 | > |
| 225 | <div ref={ fieldPopoverContainerRef }> |
| 226 | <div |
| 227 | className="acf-inline-fields-popover-inner" |
| 228 | style={ { minWidth: '300px' } } |
| 229 | ref={ setCurrentBlockFormContainer } |
| 230 | /> |
| 231 | </div> |
| 232 | </PopoverWrapper> |
| 233 | ) } |
| 234 | |
| 235 | { /* Modal for complex field types */ } |
| 236 | { selectedFieldKey && |
| 237 | selectedFieldButtonRef && |
| 238 | ! usePopover && ( |
| 239 | <Modal |
| 240 | className="acf-block-form-modal" |
| 241 | isFullScreen={ true } |
| 242 | title={ |
| 243 | getFieldInfo( selectedFieldKey )?.label |
| 244 | } |
| 245 | onRequestClose={ () => { |
| 246 | setSelectedFieldKey( null ); |
| 247 | } } |
| 248 | > |
| 249 | <div ref={ fieldPopoverContainerRef }> |
| 250 | <div |
| 251 | className="acf-inline-fields-popover-inner" |
| 252 | style={ { minWidth: '300px' } } |
| 253 | ref={ setCurrentBlockFormContainer } |
| 254 | /> |
| 255 | </div> |
| 256 | </Modal> |
| 257 | ) } |
| 258 | </ToolbarGroup> |
| 259 | ) } |
| 260 | </BlockControls> |
| 261 | ); |
| 262 | }; |
| 263 |