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 / includes / fields / class-acf-field-repeater.php
secure-custom-fields / includes / fields Last commit date
FlexibleContent 2 months ago class-acf-field-accordion.php 2 months ago class-acf-field-button-group.php 2 months ago class-acf-field-checkbox.php 3 days ago class-acf-field-clone.php 2 months ago class-acf-field-color_picker.php 2 months ago class-acf-field-date_picker.php 2 months ago class-acf-field-date_time_picker.php 2 months ago class-acf-field-email.php 2 months ago class-acf-field-file.php 2 months ago class-acf-field-flexible-content.php 1 week ago class-acf-field-gallery.php 3 weeks ago class-acf-field-google-map.php 2 months ago class-acf-field-group.php 2 months ago class-acf-field-icon_picker.php 7 months ago class-acf-field-image.php 2 months ago class-acf-field-link.php 2 months ago class-acf-field-message.php 1 year ago class-acf-field-nav-menu.php 1 week ago class-acf-field-number.php 2 months ago class-acf-field-oembed.php 3 weeks ago class-acf-field-output.php 1 year ago class-acf-field-page_link.php 3 weeks ago class-acf-field-password.php 2 months ago class-acf-field-post_object.php 3 weeks ago class-acf-field-radio.php 3 days ago class-acf-field-range.php 2 months ago class-acf-field-relationship.php 3 weeks ago class-acf-field-repeater.php 3 weeks ago class-acf-field-select.php 3 days ago class-acf-field-separator.php 1 year ago class-acf-field-tab.php 1 year ago class-acf-field-taxonomy.php 3 weeks ago class-acf-field-text.php 3 weeks ago class-acf-field-textarea.php 3 weeks ago class-acf-field-time_picker.php 2 months ago class-acf-field-true_false.php 2 months ago class-acf-field-url.php 3 weeks ago class-acf-field-user.php 3 weeks ago class-acf-field-wysiwyg.php 2 months ago class-acf-field.php 2 months ago class-acf-repeater-table.php 1 year ago index.php 1 year ago
class-acf-field-repeater.php
1290 lines
1 <?php
2 /**
3 * ACF Repeater Field Class
4 *
5 * @package wordpress/secure-custom-fields
6 */
7
8 // phpcs:disable PEAR.NamingConventions.ValidClassName
9 if ( ! class_exists( 'acf_field_repeater' ) ) :
10 /**
11 * The Repeater Field.
12 */
13 class acf_field_repeater extends acf_field {
14 /**
15 * If we're currently rendering fields.
16 *
17 * @var boolean
18 */
19 public $is_rendering = false;
20
21 /**
22 * The post/page ID that we're rendering for.
23 *
24 * @var mixed
25 */
26 public $post_id = false;
27
28 /**
29 * This function will set up the field type data
30 *
31 * @date 5/03/2014
32 * @since ACF 5.0.0
33 */
34 public function initialize() {
35 $this->name = 'repeater';
36 $this->label = __( 'Repeater', 'secure-custom-fields' );
37 $this->category = 'layout';
38 $this->description = __( 'Provides a solution for repeating content such as slides, team members, and call-to-action tiles, by acting as a parent to a set of subfields which can be repeated again and again.', 'secure-custom-fields' );
39 $this->preview_image = acf_get_url() . '/assets/images/field-type-previews/field-preview-repeater.png';
40 $this->doc_url = 'https://developer.wordpress.org/secure-custom-fields/features/fields/repeater/';
41 $this->tutorial_url = 'https://developer.wordpress.org/secure-custom-fields/features/fields/repeater/repeater-tutorial/';
42 $this->pro = true;
43 $this->supports = array( 'bindings' => false );
44 $this->defaults = array(
45 'sub_fields' => array(),
46 'min' => 0,
47 'max' => 0,
48 'rows_per_page' => 20,
49 'layout' => 'table',
50 'button_label' => '',
51 'collapsed' => '',
52 );
53
54 // field filters
55 $this->add_field_filter( 'acf/prepare_field_for_export', array( $this, 'prepare_field_for_export' ) );
56 $this->add_field_filter( 'acf/prepare_field_for_import', array( $this, 'prepare_field_for_import' ) );
57
58 // filters
59 $this->add_filter( 'acf/validate_field', array( $this, 'validate_any_field' ) );
60 $this->add_filter( 'acf/pre_render_fields', array( $this, 'pre_render_fields' ), 10, 2 );
61
62 add_action( 'wp_ajax_acf/ajax/query_repeater', array( $this, 'ajax_get_rows' ) );
63 }
64
65 /**
66 * Localizes text for the repeater field.
67 *
68 * @date 16/12/2015
69 * @since ACF 5.3.2
70 */
71 public function input_admin_enqueue_scripts() {
72 acf_localize_text(
73 array(
74 'Minimum rows not reached ({min} rows)' => __( 'Minimum rows not reached ({min} rows)', 'secure-custom-fields' ),
75 'Maximum rows reached ({max} rows)' => __( 'Maximum rows reached ({max} rows)', 'secure-custom-fields' ),
76 'Error loading page' => __( 'Error loading page', 'secure-custom-fields' ),
77 'Order will be assigned upon save' => __( 'Order will be assigned upon save', 'secure-custom-fields' ),
78 )
79 );
80 }
81
82 /**
83 * Filters the field array after it's loaded from the database.
84 *
85 * @since ACF 3.6
86 * @date 23/01/13
87 *
88 * @param array $field The field array holding all the field options.
89 * @return array
90 */
91 public function load_field( $field ) {
92 $field['min'] = (int) $field['min'];
93 $field['max'] = (int) $field['max'];
94 $sub_fields = acf_get_fields( $field );
95
96 if ( $sub_fields ) {
97 $field['sub_fields'] = array_map(
98 function ( $sub_field ) use ( $field ) {
99 $sub_field['parent_repeater'] = $field['key'];
100 return $sub_field;
101 },
102 $sub_fields
103 );
104 }
105
106 if ( empty( $field['rows_per_page'] ) || (int) $field['rows_per_page'] < 1 ) {
107 $field['rows_per_page'] = 20;
108 }
109
110 if ( '' === $field['button_label'] ) {
111 $field['button_label'] = __( 'Add Row', 'secure-custom-fields' );
112 }
113
114 return $field;
115 }
116
117 /**
118 * Runs on the "acf/pre_render_fields" filter. Used to signify
119 * that we're currently rendering a repeater field.
120 *
121 * @since ACF 6.0.0
122 *
123 * @param array $fields The main field array.
124 * @param mixed $post_id The post ID for the field being rendered.
125 * @return array
126 */
127 public function pre_render_fields( $fields, $post_id = false ) {
128 if ( is_admin() ) {
129 $this->is_rendering = true;
130 $this->post_id = $post_id;
131 }
132
133 return $fields;
134 }
135
136 /**
137 * Create the HTML interface for your field
138 *
139 * @since ACF 3.6
140 * @date 23/01/13
141 *
142 * @param array $field An array holding all the field's data.
143 */
144 public function render_field( $field ) {
145 $_field = $field;
146 $field['orig_name'] = $this->get_field_name_from_input_name( $field['name'] );
147 $_field['name'] = $field['orig_name'];
148 $field['total_rows'] = (int) acf_get_metadata_by_field( $this->post_id, $_field );
149 $table = new ACF_Repeater_Table( $field );
150 $table->render();
151 }
152
153 /**
154 * Create extra options for your field. This is rendered when editing a field.
155 * The value of $field['name'] can be used (like bellow) to save extra data to the $field
156 *
157 * @since ACF 3.6
158 * @date 23/01/13
159 *
160 * @param array $field An array holding all the field's data.
161 */
162 public function render_field_settings( $field ) {
163 $args = array(
164 'fields' => $field['sub_fields'],
165 'parent' => $field['ID'],
166 'is_subfield' => true,
167 );
168 $supports_pagination = ( empty( $field['parent_repeater'] ) && empty( $field['parent_layout'] ) );
169 ?>
170 <div class="acf-field acf-field-setting-sub_fields" data-setting="repeater" data-name="sub_fields">
171 <div class="acf-label">
172 <label><?php esc_html_e( 'Sub Fields', 'secure-custom-fields' ); ?></label>
173 <p class="description"></p>
174 </div>
175 <div class="acf-input acf-input-sub">
176 <?php
177
178 acf_get_view( 'acf-field-group/fields', $args );
179
180 ?>
181 </div>
182 </div>
183 <?php
184 acf_render_field_setting(
185 $field,
186 array(
187 'label' => __( 'Layout', 'secure-custom-fields' ),
188 'instructions' => '',
189 'class' => 'acf-repeater-layout',
190 'type' => 'radio',
191 'name' => 'layout',
192 'layout' => 'horizontal',
193 'choices' => array(
194 'table' => __( 'Table', 'secure-custom-fields' ),
195 'block' => __( 'Block', 'secure-custom-fields' ),
196 'row' => __( 'Row', 'secure-custom-fields' ),
197 ),
198 )
199 );
200
201 if ( $supports_pagination ) {
202 acf_render_field_setting(
203 $field,
204 array(
205 'label' => __( 'Pagination', 'secure-custom-fields' ),
206 'instructions' => __( 'Useful for fields with a large number of rows.', 'secure-custom-fields' ),
207 'class' => 'acf-repeater-pagination',
208 'type' => 'true_false',
209 'name' => 'pagination',
210 'ui' => 1,
211 )
212 );
213
214 acf_render_field_setting(
215 $field,
216 array(
217 'label' => __( 'Rows Per Page', 'secure-custom-fields' ),
218 'instructions' => __( 'Set the number of rows to be displayed on a page.', 'secure-custom-fields' ),
219 'class' => 'acf-repeater-pagination-num-rows',
220 'type' => 'number',
221 'name' => 'rows_per_page',
222 'placeholder' => 20,
223 'ui' => 1,
224 'min' => 1,
225 'conditions' => array(
226 'field' => 'pagination',
227 'operator' => '==',
228 'value' => 1,
229 ),
230 )
231 );
232 }
233 }
234
235 /**
236 * Renders the field settings used in the "Validation" tab.
237 *
238 * @since ACF 6.0
239 *
240 * @param array $field The field settings array.
241 * @return void
242 */
243 public function render_field_validation_settings( $field ) {
244 $field['min'] = empty( $field['min'] ) ? '' : $field['min'];
245 $field['max'] = empty( $field['max'] ) ? '' : $field['max'];
246
247 acf_render_field_setting(
248 $field,
249 array(
250 'label' => __( 'Minimum Rows', 'secure-custom-fields' ),
251 'instructions' => '',
252 'type' => 'number',
253 'name' => 'min',
254 'placeholder' => '0',
255 )
256 );
257
258 acf_render_field_setting(
259 $field,
260 array(
261 'label' => __( 'Maximum Rows', 'secure-custom-fields' ),
262 'instructions' => '',
263 'type' => 'number',
264 'name' => 'max',
265 'placeholder' => '0',
266 )
267 );
268 }
269
270 /**
271 * Renders the field settings used in the "Presentation" tab.
272 *
273 * @since ACF 6.0
274 *
275 * @param array $field The field settings array.
276 * @return void
277 */
278 public function render_field_presentation_settings( $field ) {
279 $choices = array();
280 if ( $field['collapsed'] ) {
281 $sub_field = acf_get_field( $field['collapsed'] );
282
283 if ( $sub_field ) {
284 $choices[ $sub_field['key'] ] = $sub_field['label'];
285 }
286 }
287
288 acf_render_field_setting(
289 $field,
290 array(
291 'label' => __( 'Collapsed', 'secure-custom-fields' ),
292 'instructions' => __( 'Select a sub field to show when row is collapsed', 'secure-custom-fields' ),
293 'type' => 'select',
294 'name' => 'collapsed',
295 'allow_null' => 1,
296 'choices' => $choices,
297 )
298 );
299
300 acf_render_field_setting(
301 $field,
302 array(
303 'label' => __( 'Button Label', 'secure-custom-fields' ),
304 'instructions' => '',
305 'type' => 'text',
306 'name' => 'button_label',
307 'placeholder' => __( 'Add Row', 'secure-custom-fields' ),
308 )
309 );
310 }
311
312 /**
313 * Filters the field $value after it is loaded from the database.
314 *
315 * @since ACF 3.6
316 *
317 * @param mixed $value The value found in the database.
318 * @param mixed $post_id The $post_id from which the value was loaded.
319 * @param array $field The field array holding all the field options.
320 * @return array $value
321 */
322 public function load_value( $value, $post_id, $field ) {
323 // Bail early if we don't have enough info to load the field.
324 if ( empty( $value ) || ! is_numeric( $value ) || empty( $field['sub_fields'] ) ) {
325 return false;
326 }
327
328 $value = (int) $value;
329 $rows = array();
330 $offset = 0;
331 $paged = isset( $_POST['paged'] ) ? intval( $_POST['paged'] ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified elsewhere.
332
333 // Ensure pagination is disabled inside blocks.
334 if ( acf_get_data( 'acf_inside_rest_call' ) || doing_action( 'wp_ajax_acf/ajax/fetch-block' ) ) {
335 $field['pagination'] = false;
336 }
337
338 if ( ! empty( $field['pagination'] ) && $this->is_rendering ) {
339 $rows_per_page = isset( $field['rows_per_page'] ) ? (int) $field['rows_per_page'] : 20;
340
341 if ( $rows_per_page < 1 ) {
342 $rows_per_page = 20;
343 }
344
345 if ( doing_action( 'wp_ajax_acf/ajax/query_repeater' ) ) {
346 $offset = ( $paged - 1 ) * $rows_per_page; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified elsewhere.
347 $value = min( $value, $offset + $rows_per_page );
348 } else {
349 $value = min( $value, $rows_per_page );
350 }
351 }
352
353 for ( $i = $offset; $i < $value; $i++ ) {
354 $rows[ $i ] = array();
355
356 foreach ( array_keys( $field['sub_fields'] ) as $j ) {
357 $sub_field = $field['sub_fields'][ $j ];
358
359 // Bail early if no name (tab field).
360 if ( acf_is_empty( $sub_field['name'] ) ) {
361 continue;
362 }
363
364 // Update $sub_field name and value.
365 $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}";
366 $sub_value = acf_get_value( $post_id, $sub_field );
367 $rows[ $i ][ $sub_field['key'] ] = $sub_value;
368 }
369 }
370
371 return $rows;
372 }
373
374 /**
375 * This filter is applied to the $value after it is loaded from the db and before it is returned to the template
376 *
377 * @type filter
378 * @since ACF 3.6
379 *
380 * @param mixed $value The value which was loaded from the database.
381 * @param mixed $post_id The $post_id from which the value was loaded.
382 * @param array $field The field array holding all the field options.
383 * @param boolean $escape_html Should the field return a HTML safe formatted value.
384 * @return array $value The modified value.
385 */
386 public function format_value( $value, $post_id, $field, $escape_html = false ) {
387 // bail early if no value
388 if ( empty( $value ) ) {
389 return false;
390 }
391
392 // bail early if not array
393 if ( ! is_array( $value ) ) {
394 return false;
395 }
396
397 // bail early if no sub fields
398 if ( empty( $field['sub_fields'] ) ) {
399 return false;
400 }
401
402 // loop over rows
403 foreach ( array_keys( $value ) as $i ) {
404
405 // loop through sub fields
406 foreach ( array_keys( $field['sub_fields'] ) as $j ) {
407
408 // get sub field
409 $sub_field = $field['sub_fields'][ $j ];
410
411 // bail early if no name (tab)
412 if ( acf_is_empty( $sub_field['name'] ) ) {
413 continue;
414 }
415
416 // extract value
417 $sub_value = acf_extract_var( $value[ $i ], $sub_field['key'] );
418
419 // update $sub_field name
420 $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}";
421
422 // format value
423 $sub_value = acf_format_value( $sub_value, $post_id, $sub_field, $escape_html );
424
425 // append to $row
426 $value[ $i ][ $sub_field['_name'] ] = $sub_value;
427 }
428 }
429
430 return $value;
431 }
432
433 /**
434 * Validates values for the repeater field
435 *
436 * @date 11/02/2014
437 * @since ACF 5.0.0
438 *
439 * @param boolean $valid If the field is valid.
440 * @param mixed $value The value to validate.
441 * @param array $field The main field array.
442 * @param string $input The input element's name attribute.
443 * @return boolean
444 */
445 public function validate_value( $valid, $value, $field, $input ) {
446 // vars
447 $count = 0;
448
449 // check if is value (may be empty string)
450 if ( is_array( $value ) ) {
451
452 // remove acfcloneindex
453 if ( isset( $value['acfcloneindex'] ) ) {
454 unset( $value['acfcloneindex'] );
455 }
456
457 // count
458 $count = count( $value );
459 }
460
461 // validate required
462 if ( $field['required'] && ! $count ) {
463 $valid = false;
464 }
465
466 // min
467 $min = (int) $field['min'];
468 if ( empty( $field['pagination'] ) && $min && $count < $min ) {
469
470 // create error
471 $error = __( 'Minimum rows not reached ({min} rows)', 'secure-custom-fields' );
472 $error = str_replace( '{min}', $min, $error );
473
474 // return
475 return $error;
476 }
477
478 // validate value
479 if ( $count ) {
480
481 // bail early if no sub fields
482 if ( ! $field['sub_fields'] ) {
483 return $valid;
484 }
485
486 // loop rows
487 foreach ( $value as $i => $row ) {
488
489 // Skip rows that were deleted in paginated repeaters.
490 if ( false !== strpos( $i, '_deleted' ) ) {
491 continue;
492 }
493
494 // loop sub fields
495 foreach ( $field['sub_fields'] as $sub_field ) {
496
497 // vars
498 $k = $sub_field['key'];
499
500 // test sub field exists
501 if ( ! isset( $row[ $k ] ) ) {
502 continue;
503 }
504
505 // validate
506 acf_validate_value( $row[ $k ], $sub_field, "{$input}[{$i}][{$k}]" );
507 }
508 // end loop sub fields
509 }
510 // end loop rows
511 }
512
513 return $valid;
514 }
515
516 /**
517 * This function will update a value row.
518 *
519 * @date 15/2/17
520 * @since ACF 5.5.8
521 *
522 * @param array $row Row array.
523 * @param integer $i Row ID.
524 * @param array $field Array of fields.
525 * @param mixed $post_id Post ID.
526 * @return boolean
527 */
528 public function update_row( $row, $i, $field, $post_id ) {
529 // bail early if no layout reference
530 if ( ! is_array( $row ) ) {
531 return false;
532 }
533
534 // bail early if no layout
535 if ( empty( $field['sub_fields'] ) ) {
536 return false;
537 }
538
539 foreach ( $field['sub_fields'] as $sub_field ) {
540 $value = null;
541
542 if ( array_key_exists( $sub_field['key'], $row ) ) {
543 $value = $row[ $sub_field['key'] ];
544 } elseif ( array_key_exists( $sub_field['name'], $row ) ) {
545 $value = $row[ $sub_field['name'] ];
546 } else {
547 // Value does not exist.
548 continue;
549 }
550
551 // modify name for save
552 $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}";
553
554 // update field
555 acf_update_value( $value, $post_id, $sub_field );
556 }
557
558 return true;
559 }
560
561 /**
562 * This function will delete a value row.
563 *
564 * @date 15/2/17
565 * @since ACF 5.5.8
566 *
567 * @param integer $i Row ID.
568 * @param array $field Array of fields.
569 * @param mixed $post_id Post ID.
570 * @return boolean
571 */
572 public function delete_row( $i, $field, $post_id ) {
573 // bail early if no sub fields
574 if ( empty( $field['sub_fields'] ) ) {
575 return false;
576 }
577
578 foreach ( $field['sub_fields'] as $sub_field ) {
579 // modify name for delete
580 $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}";
581
582 // delete value
583 acf_delete_value( $post_id, $sub_field );
584 }
585
586 return true;
587 }
588
589 /**
590 * Filters the $value before it is updated in the database.
591 *
592 * @since ACF 3.6
593 * @date 23/01/13
594 *
595 * @param mixed $value The value which will be saved in the database.
596 * @param mixed $post_id The $post_id of which the value will be saved.
597 * @param array $field The field array holding all the field options.
598 * @return mixed $value
599 */
600 public function update_value( $value, $post_id, $field ) {
601 // Bail early if no sub fields.
602 if ( empty( $field['sub_fields'] ) ) {
603 return $value;
604 }
605
606 if ( ! is_array( $value ) ) {
607 $value = array();
608 }
609
610 if ( isset( $value['acfcloneindex'] ) ) {
611 unset( $value['acfcloneindex'] );
612 }
613
614 $new_value = 0;
615 $old_value = (int) acf_get_metadata_by_field( $post_id, $field );
616
617 if ( ! empty( $field['pagination'] ) && did_action( 'acf/save_post' ) && ! isset( $_POST['_acf_form'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Value not used.
618 $old_rows = acf_get_value( $post_id, $field );
619 $old_rows = is_array( $old_rows ) ? $old_rows : array();
620 $edited_rows = array();
621 $deleted_rows = array();
622 $reordered_rows = array();
623 $new_rows = array();
624
625 // Categorize the submitted values, so we know what to do with them.
626 foreach ( $value as $key => $row ) {
627 if ( ! is_array( $row ) ) {
628 continue;
629 }
630
631 // Check if this is a new row.
632 if ( false === strpos( $key, 'row' ) ) {
633 unset( $row['acf_changed'] );
634
635 // Check if this new row was inserted before an existing row.
636 $reordered_row_num = isset( $row['acf_reordered'] ) ? (int) $row['acf_reordered'] : false;
637
638 if ( false !== $reordered_row_num && $reordered_row_num <= $old_value ) {
639 $reordered_rows[ $key ] = $reordered_row_num;
640 } else {
641 $new_rows[ $key ] = $row;
642 }
643
644 continue;
645 }
646
647 $row_num = (int) str_replace( 'row-', '', $key );
648
649 if ( isset( $row['acf_deleted'] ) ) {
650 $deleted_rows[] = $row_num;
651 } elseif ( isset( $row['acf_reordered'] ) ) {
652 $reordered_rows[ $row_num ] = (int) $row['acf_reordered'];
653 } else {
654 unset( $row['acf_changed'] );
655 $edited_rows[ $row_num ] = $row;
656 }
657 }
658
659 // Process any row deletions first, but don't remove their keys yet.
660 foreach ( $deleted_rows as $deleted_row ) {
661 $this->delete_row( $deleted_row, $field, $post_id );
662 $old_rows[ $deleted_row ] = 'acf_deleted';
663 }
664
665 // Update any existing rows that were edited.
666 foreach ( $edited_rows as $key => $row ) {
667 if ( array_key_exists( $key, $old_rows ) ) {
668 $old_rows[ $key ] = $row;
669 }
670 }
671
672 $rows_to_move = array();
673 $new_rows_to_move = array();
674 foreach ( $reordered_rows as $old_order => $new_order ) {
675 if ( is_int( $old_order ) ) {
676 $rows_to_move[ $new_order ][] = $value[ 'row-' . $old_order ];
677 unset( $old_rows[ $old_order ] );
678 } else {
679 $new_rows_to_move[ $new_order ][] = $value[ $old_order ];
680 }
681 }
682
683 // Iterate over existing moved rows first.
684 if ( ! empty( $rows_to_move ) ) {
685 ksort( $rows_to_move );
686 foreach ( $rows_to_move as $key => $values ) {
687 array_splice( $old_rows, $key, 0, $values );
688 }
689 }
690
691 // Iterate over inserted/duplicated rows in reverse order, so they're inserted into the correct spot.
692 if ( ! empty( $new_rows_to_move ) ) {
693 krsort( $new_rows_to_move );
694 foreach ( $new_rows_to_move as $key => $values ) {
695 array_splice( $old_rows, $key, 0, $values );
696 }
697 }
698
699 // Append any new rows.
700 foreach ( $new_rows as $new_row ) {
701 $old_rows[] = $new_row;
702 }
703
704 // Update the rows in the database.
705 $new_row_num = 0;
706 foreach ( $old_rows as $key => $row ) {
707 if ( 'acf_deleted' === $row ) {
708 unset( $old_rows[ $key ] );
709 continue;
710 }
711
712 $this->update_row( $row, $new_row_num, $field, $post_id );
713 ++$new_row_num;
714 }
715
716 // Calculate the total number of rows that will be saved after this update.
717 $new_value = count( $old_rows );
718 } else {
719 $i = -1;
720
721 foreach ( $value as $row ) {
722 ++$i;
723
724 // Bail early if no row.
725 if ( ! is_array( $row ) ) {
726 continue;
727 }
728
729 $this->update_row( $row, $i, $field, $post_id );
730 ++$new_value;
731 }
732 }
733
734 // Remove old rows.
735 if ( $old_value > $new_value ) {
736 for ( $i = $new_value; $i < $old_value; $i++ ) {
737 $this->delete_row( $i, $field, $post_id );
738 }
739 }
740
741 // Save empty string for empty value.
742 if ( empty( $new_value ) ) {
743 $new_value = '';
744 }
745
746 return $new_value;
747 }
748
749 /**
750 * Deletes any subfields after the field has been deleted.
751 *
752 * @date 4/04/2014
753 * @since ACF 5.0.0
754 *
755 * @param array $field The main field array.
756 * @return void
757 */
758 public function delete_field( $field ) {
759 // Bail early if no subfields.
760 if ( empty( $field['sub_fields'] ) ) {
761 return;
762 }
763
764 // Delete any subfields.
765 foreach ( $field['sub_fields'] as $sub_field ) {
766 acf_delete_field( $sub_field['ID'] );
767 }
768 }
769
770 /**
771 * Deletes a value from the database.
772 *
773 * @date 1/07/2015
774 * @since ACF 5.2.3
775 *
776 * @param integer $post_id The post ID to delete the value from.
777 * @param string $key The meta name/key (unused).
778 * @param array $field The main field array.
779 * @return void
780 */
781 public function delete_value( $post_id, $key, $field ) {
782 // Get the old value from the database.
783 $old_value = (int) acf_get_metadata_by_field( $post_id, $field );
784
785 // Bail early if no rows or no subfields.
786 if ( ! $old_value || empty( $field['sub_fields'] ) ) {
787 return;
788 }
789
790 for ( $i = 0; $i < $old_value; $i++ ) {
791 $this->delete_row( $i, $field, $post_id );
792 }
793 }
794
795 /**
796 * This filter is applied to the $field before it is saved to the database.
797 *
798 * @since ACF 3.6
799 *
800 * @param array $field The field array holding all the field options.
801 * @return array
802 */
803 public function update_field( $field ) {
804 unset( $field['sub_fields'] );
805 return $field;
806 }
807
808 /**
809 * This filter is applied to the $field before it is duplicated and saved to the database.
810 *
811 * @since ACF 3.6
812 * @date 23/01/13
813 *
814 * @param array $field The field array holding all the field options.
815 * @return array
816 */
817 public function duplicate_field( $field ) {
818 // get sub fields
819 $sub_fields = acf_extract_var( $field, 'sub_fields' );
820
821 // save field to get ID
822 $field = acf_update_field( $field );
823
824 // duplicate sub fields
825 acf_duplicate_fields( $sub_fields, $field['ID'] );
826
827 return $field;
828 }
829
830 /**
831 * This function will translate field settings.
832 *
833 * @date 8/03/2016
834 * @since ACF 5.3.2
835 *
836 * @param array $field The main field array.
837 * @return array
838 */
839 public function translate_field( $field ) {
840 $field['button_label'] = acf_translate( $field['button_label'] );
841 return $field;
842 }
843
844 /**
845 * This function will add compatibility for the 'column_width' setting
846 *
847 * @date 30/1/17
848 * @since ACF 5.5.6
849 *
850 * @param array $field The main field array.
851 * @return array
852 */
853 public function validate_any_field( $field ) {
854 // width has changed
855 if ( isset( $field['column_width'] ) ) {
856 $field['wrapper']['width'] = acf_extract_var( $field, 'column_width' );
857 }
858
859 return $field;
860 }
861
862 /**
863 * Prepares the field for export.
864 *
865 * @date 11/03/2014
866 * @since ACF 5.0.0
867 *
868 * @param array $field The field settings.
869 * @return array
870 */
871 public function prepare_field_for_export( $field ) {
872 // Check for subfields.
873 if ( ! empty( $field['sub_fields'] ) ) {
874 $field['sub_fields'] = acf_prepare_fields_for_export( $field['sub_fields'] );
875 }
876
877 return $field;
878 }
879
880 /**
881 * Returns a flat array of fields containing all subfields ready for import.
882 *
883 * @date 11/03/2014
884 * @since ACF 5.0.0
885 *
886 * @param array $field The field settings.
887 * @return array
888 */
889 public function prepare_field_for_import( $field ) {
890 // Check for sub fields.
891 if ( ! empty( $field['sub_fields'] ) ) {
892 $sub_fields = acf_extract_var( $field, 'sub_fields' );
893
894 // Modify sub fields.
895 foreach ( $sub_fields as $i => $sub_field ) {
896 $sub_fields[ $i ]['parent'] = $field['key'];
897 $sub_fields[ $i ]['menu_order'] = $i;
898 }
899
900 // Return array of [field, sub_1, sub_2, ...].
901 return array_merge( array( $field ), $sub_fields );
902 }
903
904 return $field;
905 }
906
907 /**
908 * Additional validation for the repeater field when submitted via REST.
909 *
910 * @param boolean $valid The current validity boolean.
911 * @param integer $value The value of the field.
912 * @param array $field The field array.
913 * @return boolean|WP
914 */
915 public function validate_rest_value( $valid, $value, $field ) {
916 if ( ! is_array( $value ) && is_null( $value ) ) {
917 $param = sprintf( '%s[%s]', $field['prefix'], $field['name'] );
918 $data = array(
919 'param' => $param,
920 'value' => $value,
921 );
922 /* translators: 1: submitted value */
923 $error = sprintf( __( '%s must be of type array or null.', 'secure-custom-fields' ), $param );
924 return new WP_Error( 'rest_invalid_param', $error, $param );
925 }
926
927 return $valid;
928 }
929
930 /**
931 * Return the schema array for the REST API.
932 *
933 * @param array $field Schema array for REST API.
934 * @return array
935 */
936 public function get_rest_schema( array $field ) {
937 $schema = array(
938 'type' => array( 'array', 'null' ),
939 'required' => ! empty( $field['required'] ),
940 'items' => array(
941 'type' => 'object',
942 'properties' => array(),
943 ),
944 );
945
946 foreach ( $field['sub_fields'] as $sub_field ) {
947 $sub_field_schema = acf_get_field_rest_schema( $sub_field );
948
949 if ( $sub_field_schema ) {
950 $schema['items']['properties'][ $sub_field['name'] ] = $sub_field_schema;
951 }
952 }
953
954 if ( ! empty( $field['min'] ) ) {
955 $schema['minItems'] = (int) $field['min'];
956 }
957
958 if ( ! empty( $field['max'] ) ) {
959 $schema['maxItems'] = (int) $field['max'];
960 }
961
962 return $schema;
963 }
964
965 /**
966 * Apply basic formatting to prepare the value for default REST output.
967 *
968 * @param mixed $value Value for the REST output.
969 * @param integer|string $post_id Post ID.
970 * @param array $field Array of fields to be returned.
971 * @return array|mixed
972 */
973 public function format_value_for_rest( $value, $post_id, array $field ) {
974 if ( empty( $value ) || ! is_array( $value ) || empty( $field['sub_fields'] ) ) {
975 return null;
976 }
977
978 // Loop through each row and within that, each sub field to process sub fields individually.
979 foreach ( $value as &$row ) {
980 foreach ( $field['sub_fields'] as $sub_field ) {
981
982 // Bail early if the field has no name (tab).
983 if ( acf_is_empty( $sub_field['name'] ) ) {
984 continue;
985 }
986
987 // Extract the sub field 'field_key'=>'value' pair from the $row and format it.
988 $sub_value = acf_extract_var( $row, $sub_field['key'] );
989 $sub_value = acf_format_value_for_rest( $sub_value, $post_id, $sub_field );
990
991 // Add the sub field value back to the $row but mapped to the field name instead
992 // of the key reference.
993 $row[ $sub_field['name'] ] = $sub_value;
994 }
995 }
996
997 return $value;
998 }
999
1000 /**
1001 * Takes the provided input name and turns it into a field name that
1002 * works with repeater fields that are subfields of other fields.
1003 *
1004 * @param string $input_name The name attribute used in the repeater.
1005 * @return string|boolean
1006 */
1007 public function get_field_name_from_input_name( $input_name ) {
1008 $parts = array();
1009 preg_match_all( '/\[([^\]]*)\]/', is_null( $input_name ) ? '' : $input_name, $parts );
1010
1011 if ( ! isset( $parts[1] ) ) {
1012 return false;
1013 }
1014
1015 $field_keys = $parts[1];
1016 $name_parts = array();
1017
1018 foreach ( $field_keys as $field_key ) {
1019 // Preserve acfcloneindex
1020 if ( 'acfcloneindex' === $field_key ) {
1021 $name_parts[] = 'acfcloneindex';
1022 continue;
1023 }
1024
1025 // Handle row numbers (row-0, row-1, etc.)
1026 if ( strpos( $field_key, 'row-' ) === 0 ) {
1027 $row_num = substr( $field_key, 4 );
1028 if ( is_numeric( $row_num ) ) {
1029 $name_parts[] = (int) $row_num;
1030 continue;
1031 }
1032 }
1033
1034 // Handle compound keys (field_..._field_...)
1035 $compound_keys = preg_split( '/_field_/', $field_key );
1036 if ( count( $compound_keys ) > 1 ) {
1037 foreach ( $compound_keys as $i => $sub_key ) {
1038 if ( $i > 0 ) {
1039 $sub_key = 'field_' . $sub_key;
1040 }
1041
1042 // Seamless clone fields use compound keys which can be skipped.
1043 $field = acf_get_field( $sub_key );
1044 if ( $field && 'clone' === $field['type'] && 'seamless' === $field['display'] ) {
1045 continue;
1046 }
1047
1048 $name_parts[] = $field && ! empty( $field['name'] ) ? $field['name'] : $sub_key;
1049 }
1050 continue;
1051 }
1052
1053 // Handle standard field keys
1054 if ( strpos( $field_key, 'field_' ) === 0 ) {
1055
1056 // Skip clone fields with prefix_name disabled.
1057 $field = acf_get_field( $field_key );
1058 if ( $field && 'clone' === $field['type'] && empty( $field['prefix_name'] ) ) {
1059 continue;
1060 }
1061
1062 $name_parts[] = $field && ! empty( $field['name'] ) ? $field['name'] : $field_key;
1063 continue;
1064 }
1065
1066 // Fallback: just add as is
1067 $name_parts[] = $field_key;
1068 }
1069
1070 return implode( '_', $name_parts );
1071 }
1072
1073 /**
1074 * Returns an array of rows used to populate the repeater table over AJAX.
1075 *
1076 * @since ACF 6.0.0
1077 *
1078 * @return void
1079 */
1080 public function ajax_get_rows() {
1081 $args = acf_request_args(
1082 array(
1083 'field_name' => '',
1084 'field_key' => '',
1085 'post_id' => 0,
1086 'rows_per_page' => 0,
1087 'refresh' => false,
1088 'nonce' => '',
1089 'options_page_slug' => '',
1090 )
1091 );
1092
1093 if ( ! acf_verify_ajax( $args['nonce'], $args['field_key'], true, 'repeater' ) ) {
1094 $error = array( 'error' => __( 'Invalid nonce.', 'secure-custom-fields' ) );
1095 wp_send_json_error( $error, 401 );
1096 }
1097
1098 $post_id_info = acf_decode_post_id( acf_get_valid_post_id( $args['post_id'] ) );
1099 if ( ! acf_current_user_can_edit_in_context( $post_id_info, sanitize_text_field( $args['options_page_slug'] ) ) ) {
1100 $error = array( 'error' => __( 'Sorry, you do not have permission to do that.', 'secure-custom-fields' ) );
1101 wp_send_json_error( $error, 403 );
1102 }
1103
1104 if ( ! acf_current_user_can_edit_post( (int) $args['post_id'] ) ) {
1105 $error = array( 'error' => __( 'Sorry, you do not have permission to do that.', 'secure-custom-fields' ) );
1106 wp_send_json_error( $error, 404 );
1107 }
1108
1109 if ( '' === $args['field_name'] || '' === $args['field_key'] ) {
1110 $error = array( 'error' => __( 'Invalid field key or name.', 'secure-custom-fields' ) );
1111 wp_send_json_error( $error, 404 );
1112 }
1113
1114 $field = acf_get_field( $args['field_key'] );
1115 $post_id = acf_get_valid_post_id( $args['post_id'] );
1116 $response = array();
1117
1118 if ( ! $field || ! $post_id ) {
1119 $error = array( 'error' => __( 'There was an error retrieving the field.', 'secure-custom-fields' ) );
1120 wp_send_json_error( $error, 404 );
1121 }
1122
1123 // Make sure we have a valid field.
1124 $field = acf_validate_field( $field );
1125
1126 if ( empty( $field['pagination'] ) ) {
1127 $error = array( 'error' => __( 'Pagination is not enabled for this field.', 'secure-custom-fields' ) );
1128 wp_send_json_error( $error, 400 );
1129 }
1130
1131 // Make sure that we only get a subset of the rows.
1132 $this->is_rendering = true;
1133
1134 $args['rows_per_page'] = (int) $args['rows_per_page'];
1135
1136 if ( $args['rows_per_page'] ) {
1137 $field['rows_per_page'] = $args['rows_per_page'];
1138 }
1139
1140 /**
1141 * We have to swap out the field name with the one sent via JS,
1142 * as the repeater could be inside a subfield.
1143 */
1144 $field['name'] = $args['field_name'];
1145 $field['prefix'] = $args['field_prefix'];
1146 $field['value'] = acf_get_value( $post_id, $field );
1147
1148 if ( $args['refresh'] ) {
1149 $response['total_rows'] = (int) acf_get_metadata_by_field( $post_id, $field );
1150 }
1151
1152 // Render the rows to be sent back via AJAX.
1153 $field = acf_prepare_field( $field );
1154 $repeater_table = new ACF_Repeater_Table( $field );
1155 $response['rows'] = $repeater_table->rows( true );
1156
1157 wp_send_json_success( $response );
1158 }
1159
1160 /**
1161 * Returns an array of JSON-LD Property output types that are supported by this field type.
1162 *
1163 * @since 6.8
1164 *
1165 * @return string[]
1166 */
1167 public function get_jsonld_output_types(): array {
1168 return array( 'Thing', 'ItemList' );
1169 }
1170
1171 /**
1172 * Formats the field value for JSON-LD output.
1173 *
1174 * @since 6.8.0
1175 *
1176 * @param mixed $value The value of the field.
1177 * @param integer|string $post_id The ID of the post.
1178 * @param array $field The field array.
1179 * @return mixed
1180 */
1181 public function format_value_for_jsonld( $value, $post_id, $field ) {
1182 if ( empty( $value ) || ! is_array( $value ) || empty( $field['sub_fields'] ) ) {
1183 return null;
1184 }
1185
1186 // Get output format with fallback.
1187 $output_format = $field['schema_output_format'] ?? '';
1188 if ( empty( $output_format ) ) {
1189 $property = $field['schema_property'] ?? '';
1190 $output_format = \SCF\AI\GEO\Schema::get_default_output_format( $this->name, $property );
1191 }
1192
1193 // Get all Schema.org types for type detection.
1194 $all_types = array_keys( \SCF\AI\GEO\SchemaData::get_type_hierarchy() );
1195 $all_types[] = 'Thing';
1196
1197 $items = array();
1198
1199 // Process each row.
1200 foreach ( $value as $row ) {
1201 if ( ! is_array( $row ) ) {
1202 continue;
1203 }
1204
1205 $row_data = array();
1206 $row_type = null;
1207
1208 // Add @type to each row item if we have a specific output format.
1209 // For ItemList, items don't get the @type directly (they get wrapped in ListItem).
1210 if ( ! empty( $output_format ) && 'ItemList' !== $output_format ) {
1211 $row_type = $output_format;
1212 }
1213
1214 // Process each sub field in the row.
1215 foreach ( $field['sub_fields'] as $sub_field ) {
1216 // Get the sub field value - try both key and name.
1217 $sub_value = $row[ $sub_field['key'] ] ?? $row[ $sub_field['_name'] ] ?? $row[ $sub_field['name'] ] ?? null;
1218
1219 if ( null === $sub_value ) {
1220 continue;
1221 }
1222
1223 // Check if sub field has a schema property mapping.
1224 $schema_property = $sub_field['schema_property'] ?? '';
1225
1226 if ( empty( $schema_property ) ) {
1227 // No schema property - skip this field for JSON-LD output.
1228 continue;
1229 }
1230
1231 // Check if schema_property is actually a Schema.org type (e.g., HowToStep).
1232 // If so, use it as the row's @type instead of as a property.
1233 if ( in_array( $schema_property, $all_types, true ) ) {
1234 $row_type = $schema_property;
1235 continue;
1236 }
1237
1238 // Parse qualified property (e.g., "Thing.name" -> "name") to strip type prefix.
1239 $property_name = \SCF\AI\GEO\Schema::get_property_name( $schema_property );
1240
1241 // Format the sub field value for JSON-LD.
1242 $formatted_value = \SCF\AI\GEO\GEO::format_field_value_for_jsonld( $sub_value, $sub_field );
1243
1244 if ( null !== $formatted_value ) {
1245 $row_data[ $property_name ] = $formatted_value;
1246 }
1247 }
1248
1249 // Add @type if we determined one.
1250 if ( $row_type ) {
1251 $row_data = array_merge( array( '@type' => $row_type ), $row_data );
1252 }
1253
1254 // If row has only one property and no @type, extract just the value for cleaner output.
1255 if ( count( $row_data ) === 1 && ! isset( $row_data['@type'] ) ) {
1256 $items[] = reset( $row_data );
1257 } elseif ( ! empty( $row_data ) ) {
1258 // Only add row if it has content.
1259 $items[] = $row_data;
1260 }
1261 }
1262
1263 if ( empty( $items ) ) {
1264 return null;
1265 }
1266
1267 // For ItemList, wrap items in the proper structure.
1268 if ( 'ItemList' === $output_format ) {
1269 $list_items = array();
1270 foreach ( $items as $position => $item ) {
1271 $list_items[] = array(
1272 '@type' => 'ListItem',
1273 'position' => $position + 1,
1274 'item' => $item,
1275 );
1276 }
1277 return array(
1278 '@type' => 'ItemList',
1279 'itemListElement' => $list_items,
1280 );
1281 }
1282
1283 return $items;
1284 }
1285 }
1286
1287 // initialize
1288 acf_register_field_type( 'acf_field_repeater' );
1289 endif; // class_exists check
1290