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-repeater-table.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-repeater-table.php
487 lines
1 <?php
2 /**
3 * ACF_Repeater_Table
4 *
5 * Helper class for rendering repeater tables.
6 *
7 */
8 class ACF_Repeater_Table {
9
10 /**
11 * The main field array used to render the repeater.
12 *
13 * @var array
14 */
15 private $field;
16
17 /**
18 * An array containing the subfields used in the repeater.
19 *
20 * @var array
21 */
22 private $sub_fields;
23
24 /**
25 * The value(s) of the repeater field.
26 *
27 * @var array
28 */
29 private $value;
30
31 /**
32 * If we should show the "Add Row" button.
33 *
34 * @var boolean
35 */
36 private $show_add = true;
37
38 /**
39 * If we should show the "Remove Row" button.
40 *
41 * @var boolean
42 */
43 private $show_remove = true;
44
45 /**
46 * If we should show the order of the fields.
47 *
48 * @var boolean
49 */
50 private $show_order = true;
51
52 /**
53 * Constructs the ACF_Repeater_Table class.
54 *
55 * @param array $field The main field array for the repeater being rendered.
56 */
57 public function __construct( $field ) {
58 $this->field = $field;
59 $this->sub_fields = $field['sub_fields'];
60
61 // Default to non-paginated repeaters.
62 if ( empty( $this->field['pagination'] ) ) {
63 $this->field['pagination'] = false;
64 }
65
66 // We don't yet support pagination inside other repeaters or flexible content fields.
67 if ( ! empty( $this->field['parent_repeater'] ) || ! empty( $this->field['parent_layout'] ) ) {
68 $this->field['pagination'] = false;
69 }
70
71 // We don't yet support pagination in frontend forms or inside blocks.
72 if ( ! is_admin() || acf_get_data( 'acf_inside_rest_call' ) || doing_action( 'wp_ajax_acf/ajax/fetch-block' ) ) {
73 $this->field['pagination'] = false;
74 }
75
76 $this->setup();
77 }
78
79 /**
80 * Sets up the field for rendering.
81 *
82 * @since 6.0.0
83 *
84 * @return void
85 */
86 private function setup() {
87 if ( $this->field['collapsed'] ) {
88 foreach ( $this->sub_fields as &$sub_field ) {
89 // Add target class.
90 if ( $sub_field['key'] == $this->field['collapsed'] ) {
91 $sub_field['wrapper']['class'] .= ' -collapsed-target';
92 }
93 }
94 }
95
96 if ( $this->field['max'] ) {
97 // If max 1 row, don't show order.
98 if ( 1 == $this->field['max'] ) {
99 $this->show_order = false;
100 }
101
102 // If max == min, don't show add or remove buttons.
103 if ( $this->field['max'] <= $this->field['min'] ) {
104 $this->show_remove = false;
105 $this->show_add = false;
106 }
107 }
108
109 if ( empty( $this->field['rows_per_page'] ) ) {
110 $this->field['rows_per_page'] = 20;
111 }
112
113 if ( (int) $this->field['rows_per_page'] < 1 ) {
114 $this->field['rows_per_page'] = 20;
115 }
116
117 $this->value = $this->prepare_value();
118 }
119
120 /**
121 * Prepares the repeater values for rendering.
122 *
123 * @since 6.0.0
124 *
125 * @return array
126 */
127 private function prepare_value() {
128 $value = is_array( $this->field['value'] ) ? $this->field['value'] : array();
129
130 if ( empty( $this->field['pagination'] ) ) {
131 // If there are fewer values than min, populate the extra values.
132 if ( $this->field['min'] ) {
133 $value = array_pad( $value, $this->field['min'], array() );
134 }
135
136 // If there are more values than max, remove some values.
137 if ( $this->field['max'] ) {
138 $value = array_slice( $value, 0, $this->field['max'] );
139 }
140 }
141
142 $value['acfcloneindex'] = array();
143
144 return $value;
145 }
146
147 /**
148 * Renders the full repeater table.
149 *
150 * @since 6.0.0
151 *
152 * @return void
153 */
154 public function render() {
155 // Attributes for main wrapper div.
156 $div = array(
157 'class' => 'acf-repeater -' . $this->field['layout'],
158 'data-min' => $this->field['min'],
159 'data-max' => $this->field['max'],
160 'data-pagination' => ! empty( $this->field['pagination'] ),
161 );
162
163 if ( $this->field['pagination'] ) {
164 $div['data-per_page'] = $this->field['rows_per_page'];
165 $div['data-total_rows'] = $this->field['total_rows'];
166 $div['data-orig_name'] = $this->field['orig_name'];
167 $div['data-nonce'] = wp_create_nonce( $this->field['key'] );
168 }
169
170 if ( empty( $this->value ) ) {
171 $div['class'] .= ' -empty';
172 }
173 ?>
174 <div <?php echo acf_esc_attrs( $div ); ?>>
175 <?php
176 acf_hidden_input(
177 array(
178 'name' => $this->field['name'],
179 'value' => '',
180 'class' => 'acf-repeater-hidden-input',
181 )
182 );
183 ?>
184 <table class="acf-table">
185 <?php $this->thead(); ?>
186 <tbody>
187 <?php $this->rows(); ?>
188 </tbody>
189 </table>
190 <?php $this->table_actions(); ?>
191 </div>
192 <?php
193 }
194
195 /**
196 * Renders the table head.
197 *
198 * @since 6.0.0
199 *
200 * @return void
201 */
202 public function thead() {
203 if ( 'table' !== $this->field['layout'] ) {
204 return;
205 }
206 ?>
207 <thead>
208 <tr>
209 <?php if ( $this->show_order ) : ?>
210 <th class="acf-row-handle"></th>
211 <?php endif; ?>
212
213 <?php
214 foreach ( $this->sub_fields as $sub_field ) :
215 // Prepare field (allow sub fields to be removed).
216 $sub_field = acf_prepare_field( $sub_field );
217 if ( ! $sub_field ) {
218 continue;
219 }
220
221 // Define attrs.
222 $attrs = array(
223 'class' => 'acf-th',
224 'data-name' => $sub_field['_name'],
225 'data-type' => $sub_field['type'],
226 'data-key' => $sub_field['key'],
227 );
228
229 if ( $sub_field['wrapper']['width'] ) {
230 $attrs['data-width'] = $sub_field['wrapper']['width'];
231 $attrs['style'] = 'width: ' . $sub_field['wrapper']['width'] . '%;';
232 }
233
234 // Remove "id" to avoid "for" attribute on <label>.
235 $sub_field['id'] = '';
236 ?>
237 <th <?php echo acf_esc_attrs( $attrs ); ?>>
238 <?php acf_render_field_label( $sub_field ); ?>
239 <?php acf_render_field_instructions( $sub_field ); ?>
240 </th>
241 <?php endforeach; ?>
242
243 <?php if ( $this->show_remove ) : ?>
244 <th class="acf-row-handle"></th>
245 <?php endif; ?>
246 </tr>
247 </thead>
248 <?php
249 }
250
251 /**
252 * Renders or returns rows for the repeater field table.
253 *
254 * @since 6.0.0
255 *
256 * @param boolean $return If we should return the rows or render them.
257 * @return array|void
258 */
259 public function rows( $return = false ) {
260 $rows = array();
261
262 // Don't include the clone when rendering via AJAX.
263 if ( $return && isset( $this->value['acfcloneindex'] ) ) {
264 unset( $this->value['acfcloneindex'] );
265 }
266
267 foreach ( $this->value as $i => $row ) {
268 $rows[ $i ] = $this->row( $i, $row, $return );
269 }
270
271 if ( $return ) {
272 return $rows;
273 }
274
275 echo implode( PHP_EOL, $rows ); //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML already escaped by generating functions.
276 }
277
278 /**
279 * Renders an individual row.
280 *
281 * @since 6.0.0
282 *
283 * @param integer $i The row number.
284 * @param array $row An array containing the row values.
285 * @param boolean $return If we should return the row or render it.
286 * @return string|void
287 */
288 public function row( $i, $row, $return = false ) {
289 if ( $return ) {
290 ob_start();
291 }
292
293 $id = "row-$i";
294 $class = 'acf-row';
295
296 if ( 'acfcloneindex' === $i ) {
297 $id = 'acfcloneindex';
298 $class .= ' acf-clone';
299 }
300
301 $el = 'td';
302 $before_fields = '';
303 $after_fields = '';
304
305 if ( 'row' === $this->field['layout'] ) {
306 $el = 'div';
307 $before_fields = '<td class="acf-fields -left">';
308 $after_fields = '</td>';
309 } elseif ( 'block' === $this->field['layout'] ) {
310 $el = 'div';
311 $before_fields = '<td class="acf-fields">';
312 $after_fields = '</td>';
313 }
314
315 printf(
316 '<tr class="%s" data-id="%s">',
317 esc_attr( $class ),
318 esc_attr( $id )
319 );
320
321 $this->row_handle( $i );
322
323 echo $before_fields; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- string only contains guarenteed safe HTML.
324
325 foreach ( $this->sub_fields as $sub_field ) {
326 if ( isset( $row[ $sub_field['key'] ] ) ) {
327 $sub_field['value'] = $row[ $sub_field['key'] ];
328 } elseif ( isset( $sub_field['default_value'] ) ) {
329 $sub_field['value'] = $sub_field['default_value'];
330 }
331
332 // Update prefix to allow for nested values.
333 $sub_field['prefix'] = $this->field['name'] . '[' . $id . ']';
334
335 acf_render_field_wrap( $sub_field, $el );
336 }
337
338 echo $after_fields; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- string only contains guarenteed safe HTML.
339
340 $this->row_actions();
341
342 echo '</tr>';
343
344 if ( $return ) {
345 return ob_get_clean();
346 }
347 }
348
349 /**
350 * Renders the row handle at the start of each row.
351 *
352 * @since 6.0.0
353 *
354 * @param integer $i The current row number.
355 * @return void
356 */
357 public function row_handle( $i ) {
358 if ( ! $this->show_order ) {
359 return;
360 }
361
362 $hr_row_num = intval( $i ) + 1;
363 $classes = 'acf-row-handle order';
364 $title = __( 'Drag to reorder', 'acf' );
365 $row_num_html = sprintf(
366 '<span class="acf-row-number" title="%s">%d</span>',
367 esc_html__( 'Click to reorder', 'acf' ),
368 $hr_row_num
369 );
370
371 if ( ! empty( $this->field['pagination'] ) ) {
372 $classes .= ' pagination';
373 $title = '';
374 $input = sprintf( '<input type="number" class="acf-order-input" value="%d" style="display: none;" />', $hr_row_num );
375 $row_num_html = '<div class="acf-order-input-wrap">' . $input . $row_num_html . '</div>';
376 }
377 ?>
378 <td class="<?php echo esc_attr( $classes ); ?>" title="<?php echo esc_attr( $title ); ?>">
379 <?php if ( $this->field['collapsed'] ) : ?>
380 <a class="acf-icon -collapse small" href="#" data-event="collapse-row" title="<?php esc_attr_e( 'Click to toggle', 'acf' ); ?>"></a>
381 <?php endif; ?>
382 <?php echo $row_num_html; ?><?php //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped where necessary on generation. ?>
383 </td>
384 <?php
385 }
386
387 /**
388 * Renders the actions displayed at the end of each row.
389 *
390 * @since 6.0.0
391 *
392 * @return void
393 */
394 public function row_actions() {
395 if ( ! $this->show_remove ) {
396 return;
397 }
398 ?>
399 <td class="acf-row-handle remove">
400 <a class="acf-icon -plus small acf-js-tooltip hide-on-shift" href="#" data-event="add-row" title="<?php esc_attr_e( 'Add row', 'acf' ); ?>"></a>
401 <a class="acf-icon -duplicate small acf-js-tooltip show-on-shift" href="#" data-event="duplicate-row" title="<?php esc_attr_e( 'Duplicate row', 'acf' ); ?>"></a>
402 <a class="acf-icon -minus small acf-js-tooltip" href="#" data-event="remove-row" title="<?php esc_attr_e( 'Remove row', 'acf' ); ?>"></a>
403 </td>
404 <?php
405 }
406
407 /**
408 * Renders the actions displayed underneath the table.
409 *
410 * @since 6.0.0
411 *
412 * @return void
413 */
414 public function table_actions() {
415 if ( ! $this->show_add ) {
416 return;
417 }
418 ?>
419 <div class="acf-actions">
420 <a class="acf-button acf-repeater-add-row button button-primary" href="#" data-event="add-row"><?php echo acf_esc_html( $this->field['button_label'] ); ?></a>
421 <?php $this->pagination(); ?>
422 <div class="clear"></div>
423 </div>
424 <?php
425 }
426
427 /**
428 * Renders the table pagination.
429 * Mostly lifted from the WordPress core WP_List_Table class.
430 *
431 * @since 6.0.0
432 *
433 * @return void
434 */
435 public function pagination() {
436 if ( empty( $this->field['pagination'] ) ) {
437 return;
438 }
439
440 $total_rows = isset( $this->field['total_rows'] ) ? (int) $this->field['total_rows'] : 0;
441 $total_pages = ceil( $total_rows / (int) $this->field['rows_per_page'] );
442 $total_pages = max( $total_pages, 1 );
443
444 $html_current_page = sprintf(
445 "%s<input class='current-page' id='current-page-selector' type='text' name='paged' value='%s' size='%d' aria-describedby='table-paging' />",
446 '<label for="current-page-selector" class="screen-reader-text">' . __( 'Current Page', 'acf' ) . '</label>',
447 1,
448 strlen( $total_pages )
449 );
450
451 $html_total_pages = sprintf( "<span class='acf-total-pages'>%s</span>", number_format_i18n( $total_pages ) );
452 ?>
453 <div class="acf-tablenav tablenav-pages">
454 <a class="first-page button acf-nav" aria-hidden="true" data-event="first-page" title="<?php esc_attr_e( 'First Page', 'acf' ); ?>">
455 <span class="screen-reader-text"><?php esc_html_e( 'First Page', 'acf' ); ?></span>
456 <span aria-hidden="true">&laquo;</span>
457 </a>
458 <a class="prev-page button acf-nav" aria-hidden="true" data-event="prev-page" title="<?php esc_attr_e( 'Previous Page', 'acf' ); ?>">
459 <span class="screen-reader-text"><?php esc_html_e( 'Previous Page', 'acf' ); ?></span>
460 <span aria-hidden="true">&lsaquo;</span>
461 </a>
462 <span class="paging-input">
463 <label for="current-page-selector" class="screen-reader-text"><?php esc_html_e( 'Current Page', 'acf' ); ?></label>
464 <span class="tablenav-paging-text" title="<?php esc_attr_e( 'Current Page', 'acf' ); ?>">
465 <?php
466 printf(
467 /* translators: 1: Current page, 2: Total pages. */
468 esc_html_x( '%1$s of %2$s', 'paging', 'acf' ),
469 $html_current_page, //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escape not necessary.
470 $html_total_pages //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escape not necessary.
471 );
472 ?>
473 </span>
474 </span>
475 <a class="next-page button acf-nav" data-event="next-page" title="<?php esc_attr_e( 'Next Page', 'acf' ); ?>">
476 <span class="screen-reader-text"><?php esc_html_e( 'Next Page', 'acf' ); ?></span>
477 <span aria-hidden="true">&rsaquo;</span>
478 </a>
479 <a class="last-page button acf-nav" data-event="last-page" title="<?php esc_attr_e( 'Last Page', 'acf' ); ?>">
480 <span class="screen-reader-text"><?php esc_html_e( 'Last Page', 'acf' ); ?></span>
481 <span aria-hidden="true">&raquo;</span>
482 </a>
483 </div>
484 <?php
485 }
486 }
487