admin
1 year ago
fields
1 year ago
locations
1 year ago
post-types
1 year ago
README.md
1 year ago
acf-pro.php
1 year ago
acf-ui-options-page-functions.php
1 year ago
blocks-auto-inline-editing.php
6 months ago
blocks.php
1 year ago
options-page.php
1 year ago
blocks-auto-inline-editing.php
263 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Applying auto inline editing to SCF blocks. |
| 4 | * |
| 5 | * @package wordpress/secure-custom-fields |
| 6 | * |
| 7 | * phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.Found |
| 8 | */ |
| 9 | |
| 10 | /** |
| 11 | * Applying auto inline editing to SCF blocks. |
| 12 | * |
| 13 | * @package wordpress/secure-custom-fields |
| 14 | */ |
| 15 | |
| 16 | namespace SCF\Blocks\AutoInlineEditing; |
| 17 | |
| 18 | // Exit if accessed directly. |
| 19 | defined( 'ABSPATH' ) || exit; |
| 20 | |
| 21 | /** |
| 22 | * Returns an array of field type names which support contenteditable (allows typing text) attribute. |
| 23 | * |
| 24 | * @return array |
| 25 | */ |
| 26 | function get_allowed_contenteditable_fields(): array { |
| 27 | return array( 'text', 'textarea' ); |
| 28 | } |
| 29 | |
| 30 | /** |
| 31 | * Returns an array of field type names will be ignored by the automatic application of inline editing attributes. |
| 32 | * |
| 33 | * @return array |
| 34 | */ |
| 35 | function get_non_auto_inline_editing_fields(): array { |
| 36 | return array( 'repeater', 'flexible-content' ); |
| 37 | } |
| 38 | |
| 39 | /** |
| 40 | * This function populates a global variable called acf_fields_used_in_block_render_template, which is an array |
| 41 | * where each key is the value entered for the field, and the value is the field data, including the current value. |
| 42 | * |
| 43 | * @param mixed $field_value The field_value. |
| 44 | * @param string $post_id The post ID for this value. |
| 45 | * @param array $field The field array. |
| 46 | * |
| 47 | * @return mixed |
| 48 | */ |
| 49 | function populate_auto_inline_editing_values( $field_value, $post_id, $field ) { |
| 50 | |
| 51 | global $acf_fields_used_in_block_render_template, $acf_blocks_doing_auto_inline_editing; |
| 52 | |
| 53 | if ( ! $acf_blocks_doing_auto_inline_editing || ! empty( $field['parent_repeater'] ) ) { |
| 54 | return $field_value; |
| 55 | } |
| 56 | |
| 57 | // Add this field and its value to the global variable so we can grab it when rendering later in apply_inline_editing_attributes_to_render_template. |
| 58 | if ( ! is_array( $field_value ) ) { |
| 59 | if ( empty( $field_value ) ) { |
| 60 | $field_value = 'acf_auto_inline_editing_field_name_' . $field['name']; |
| 61 | } |
| 62 | |
| 63 | $field['value'] = $field_value; |
| 64 | |
| 65 | // Note: If 2 fields happen to have the exact same value it's most-likely fine, but there are edge cases. |
| 66 | // Because in the DOM they get applied top-to-bottom, we also check top-to-bottom when pulling them from this array. |
| 67 | // |
| 68 | // A small, known edge case exists here if someone calls $a = get_field('a') and $b = get_field('b'), but then renders $b before $a. |
| 69 | // If BOTH field $a and field $b have the exact same value AND are rendered in a different order than they were called, you would end up |
| 70 | // with a scenario where editing the inline value of $a actually edits the value for $b. |
| 71 | // Regardless, it would likely be obvious to the block developer because you would see it, |
| 72 | // both in the field value in the block sidebar, and also inline/preview, wherever field a/b are used. |
| 73 | $acf_fields_used_in_block_render_template[] = $field; |
| 74 | } |
| 75 | |
| 76 | return $field_value; |
| 77 | } |
| 78 | add_filter( 'acf/format_value', __NAMESPACE__ . '\populate_auto_inline_editing_values', 10, 3 ); |
| 79 | |
| 80 | /** |
| 81 | * Applies inline editing attributes to dom elements if they contain field values. |
| 82 | * |
| 83 | * @param string $path The path to the render template for this block. |
| 84 | * @param array $block The block data. |
| 85 | * @param boolean $is_preview Whether we are in the block editor or not. |
| 86 | * @return string |
| 87 | */ |
| 88 | function apply_inline_editing_attributes_to_render_template( $path, $block, $is_preview ): string { |
| 89 | global $acf_fields_used_in_block_render_template, $acf_blocks_doing_auto_inline_editing; |
| 90 | |
| 91 | // Suppress unused variable warning - parameter is part of the function signature for consistency. |
| 92 | unset( $is_preview ); |
| 93 | |
| 94 | // Don't apply autoInlineEditing if the current PHP doesn't include DOMDocument or DOMXPath. |
| 95 | if ( ! class_exists( 'DOMDocument' ) || ! class_exists( 'DOMXPath' ) ) { |
| 96 | ob_start(); |
| 97 | include $path; |
| 98 | return ob_get_clean(); |
| 99 | } |
| 100 | |
| 101 | $allowed_contenteditable_field_types = get_allowed_contenteditable_fields(); |
| 102 | $non_auto_inline_editing_fields = get_non_auto_inline_editing_fields(); |
| 103 | |
| 104 | $acf_fields_used_in_block_render_template = array(); |
| 105 | |
| 106 | $acf_blocks_doing_auto_inline_editing = true; |
| 107 | |
| 108 | ob_start(); |
| 109 | include $path; |
| 110 | $html = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head>' . ob_get_clean(); |
| 111 | |
| 112 | $acf_blocks_doing_auto_inline_editing = false; |
| 113 | |
| 114 | // Load the HTML into DOMDocument |
| 115 | $dom = new \DOMDocument(); |
| 116 | libxml_use_internal_errors( true ); // Suppress warnings for invalid HTML |
| 117 | $dom->loadHTML( $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); |
| 118 | libxml_clear_errors(); |
| 119 | |
| 120 | // Get all elements |
| 121 | $xpath = new \DOMXPath( $dom ); |
| 122 | $elements = $xpath->query( '//*' ); |
| 123 | |
| 124 | // Iterate over elements and modify based on text content |
| 125 | foreach ( $elements as $element ) { |
| 126 | $field_names_for_popover = array(); |
| 127 | |
| 128 | $top_level_text = ''; |
| 129 | |
| 130 | // phpcs:ignore WordPress.NamingConventions.ValidVariableName |
| 131 | if ( empty( $element->childNodes ) ) { |
| 132 | continue; |
| 133 | } |
| 134 | |
| 135 | // Loop through the child nodes of the current element |
| 136 | // phpcs:ignore WordPress.NamingConventions.ValidVariableName |
| 137 | foreach ( $element->childNodes as $child ) { |
| 138 | // Check if the child node is a text node |
| 139 | // phpcs:ignore WordPress.NamingConventions.ValidVariableName |
| 140 | if ( XML_TEXT_NODE === $child->nodeType ) { |
| 141 | // phpcs:ignore WordPress.NamingConventions.ValidVariableName |
| 142 | $top_level_text .= $child->nodeValue; |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | $top_level_text = trim( $top_level_text ); |
| 147 | |
| 148 | if ( ! empty( $top_level_text ) ) { |
| 149 | $acf_field_found = false; |
| 150 | |
| 151 | // Loop through each field used in this render template. |
| 152 | foreach ( $acf_fields_used_in_block_render_template as $key => $field_data ) { |
| 153 | if ( ! $field_data['name'] ) { |
| 154 | continue; |
| 155 | } |
| 156 | |
| 157 | // If the value for this field matches the text in the dom, apply the inline editing attributes. |
| 158 | if ( $field_data['value'] === $top_level_text ) { |
| 159 | $acf_field_found = true; |
| 160 | $field_slug = $field_data['name']; |
| 161 | $field_value = $field_data['value']; |
| 162 | $field_type = $field_data['type']; |
| 163 | $field_placeholder_text = ! empty( $field_data['placeholder'] ) ? $field_data['placeholder'] : __( 'Type to edit...', 'secure-custom-fields' ); |
| 164 | |
| 165 | if ( ! in_array( $field_type, $non_auto_inline_editing_fields, true ) ) { |
| 166 | if ( in_array( $field_type, $allowed_contenteditable_field_types, true ) ) { |
| 167 | // Add the contenteditable things. |
| 168 | if ( ! $element->getAttribute( 'data-acf-inline-contenteditable' ) ) { |
| 169 | $element->setAttribute( 'role', 'button' ); |
| 170 | $element->setAttribute( 'data-acf-inline-contenteditable', true ); |
| 171 | |
| 172 | $element->setAttribute( 'data-acf-inline-contenteditable-field-slug', str_replace( 'acf_auto_inline_editing_field_name_', '', $field_slug ) ); |
| 173 | $element->setAttribute( 'data-acf-placeholder', $field_placeholder_text ); |
| 174 | } |
| 175 | |
| 176 | // phpcs:ignore WordPress.NamingConventions.ValidVariableName |
| 177 | if ( 'acf_auto_inline_editing_field_name_' . $field_slug !== $field_value ) { |
| 178 | // phpcs:ignore WordPress.NamingConventions.ValidVariableName |
| 179 | $element->nodeValue = $field_value; |
| 180 | } else { |
| 181 | // phpcs:ignore WordPress.NamingConventions.ValidVariableName |
| 182 | $element->nodeValue = ''; |
| 183 | } |
| 184 | } else { |
| 185 | // Make the field popover instead of contenteditable. |
| 186 | $field_names_for_popover[] = str_replace( 'acf_auto_inline_editing_field_name_', '', $field_slug ); |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | // We found a matching field so we can stop looping. |
| 191 | break; |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | if ( ! $acf_field_found ) { |
| 196 | foreach ( $acf_fields_used_in_block_render_template as $key => $field_data ) { |
| 197 | // Remove the acf_auto_inline_editing_field_name_ placeholder from the text node. |
| 198 | if ( strpos( $top_level_text, 'acf_auto_inline_editing_field_name_' . $field_data['name'] ) !== false ) { |
| 199 | // phpcs:ignore WordPress.NamingConventions.ValidVariableName |
| 200 | $element->nodeValue = str_replace( 'acf_auto_inline_editing_field_name_' . $field_data['name'], '', $top_level_text ); |
| 201 | } |
| 202 | } |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | // If the value for this field matches the field slug, remove it. |
| 207 | // phpcs:ignore WordPress.NamingConventions.ValidVariableName |
| 208 | if ( str_starts_with( $top_level_text, 'acf_auto_inline_editing_field_name_' ) ) { |
| 209 | // phpcs:ignore WordPress.NamingConventions.ValidVariableName |
| 210 | $element->textContent = ''; |
| 211 | } |
| 212 | |
| 213 | // Loop over each attribute. If an attribute comes from acf, make it popup when parent is selected. |
| 214 | foreach ( $element->attributes as $attribute ) { |
| 215 | if ( 'data-acf-inline-contenteditable-field-slug' === $attribute->name ) { |
| 216 | continue; |
| 217 | } |
| 218 | $attribute_value = trim( $attribute->value ); |
| 219 | |
| 220 | foreach ( $acf_fields_used_in_block_render_template as $field_data ) { |
| 221 | if ( empty( $field_data['name'] ) ) { |
| 222 | continue; |
| 223 | } |
| 224 | |
| 225 | $field_slug = $field_data['name']; |
| 226 | $field_value = $field_data['value']; |
| 227 | |
| 228 | if ( ! is_array( $field_value ) && $attribute_value === $field_value ) { |
| 229 | $field_names_for_popover[] = str_replace( 'acf_auto_inline_editing_field_name_', '', $field_slug ); |
| 230 | } |
| 231 | |
| 232 | if ( strpos( $attribute_value, 'acf_auto_inline_editing_field_name_' ) !== false ) { |
| 233 | $attribute_value = str_replace( 'acf_auto_inline_editing_field_name_' . $field_slug, '', $attribute_value ); |
| 234 | $element->setAttribute( $attribute->name, $attribute_value ); |
| 235 | } |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | $field_names_for_popover = array_unique( $field_names_for_popover ); |
| 240 | |
| 241 | // Don't add popover fields to the top level element unless it has text content (as opposed to html/non-text content, which is what most top level elements contain). |
| 242 | // phpcs:ignore WordPress.NamingConventions.ValidVariableName |
| 243 | $is_top_level = isset( $element->parentNode->tagName ) && 'body' === $element->parentNode->tagName; |
| 244 | |
| 245 | if ( ! $is_top_level && ! empty( $field_names_for_popover ) ) { |
| 246 | $preexisting_inline_fields_uid = $element->getAttribute( 'data-acf-inline-fields-uid' ); |
| 247 | if ( ! $preexisting_inline_fields_uid ) { |
| 248 | $element->setAttribute( 'data-acf-inline-fields-uid', implode( '__', $field_names_for_popover ) . '__' . $block['id'] ); |
| 249 | $element->setAttribute( |
| 250 | 'data-acf-inline-fields', |
| 251 | wp_json_encode( $field_names_for_popover ), |
| 252 | ); |
| 253 | } |
| 254 | } |
| 255 | } |
| 256 | // phpcs:ignore WordPress.NamingConventions.ValidVariableName |
| 257 | $dom->preserveWhiteSpace = true; |
| 258 | // phpcs:ignore WordPress.NamingConventions.ValidVariableName |
| 259 | $dom->formatOutput = false; |
| 260 | |
| 261 | return str_replace( '<meta charset="UTF-8">', '', $dom->saveHTML() ); |
| 262 | } |
| 263 |