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