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