PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / 6.9.1
Secure Custom Fields v6.9.1
6.9.1 6.9.0 6.8.9 6.8.7 6.8.8 6.8.6 6.8.4 6.8.5 trunk 6.4.0-beta1 6.4.0-beta2 6.4.1 6.4.1-beta3 6.4.1-beta4 6.4.1-beta5 6.4.1-beta6 6.4.1-beta7 6.4.2 6.5.0 6.5.1 6.5.2 6.5.3 6.5.4 6.5.5 6.5.6 6.5.7 6.6.0 6.7.0 6.7.1 6.8.0 6.8.1 6.8.2 6.8.3
secure-custom-fields / pro / blocks-auto-inline-editing.php
secure-custom-fields / pro Last commit date
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