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