123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489 |
- <?php
- /**
- * ACF_Repeater_Table
- *
- * Helper class for rendering repeater tables.
- *
- * @package ACF
- * @since 6.0.0
- */
- class ACF_Repeater_Table {
- /**
- * The main field array used to render the repeater.
- *
- * @var array
- */
- private $field;
- /**
- * An array containing the subfields used in the repeater.
- *
- * @var array
- */
- private $sub_fields;
- /**
- * The value(s) of the repeater field.
- *
- * @var array
- */
- private $value;
- /**
- * If we should show the "Add Row" button.
- *
- * @var bool
- */
- private $show_add = true;
- /**
- * If we should show the "Remove Row" button.
- *
- * @var bool
- */
- private $show_remove = true;
- /**
- * If we should show the order of the fields.
- *
- * @var bool
- */
- private $show_order = true;
- /**
- * Constructs the ACF_Repeater_Table class.
- *
- * @param array $field The main field array for the repeater being rendered.
- */
- public function __construct( $field ) {
- $this->field = $field;
- $this->sub_fields = $field['sub_fields'];
- // Default to non-paginated repeaters.
- if ( empty( $this->field['pagination'] ) ) {
- $this->field['pagination'] = false;
- }
- // We don't yet support pagination inside other repeaters or flexible content fields.
- if ( ! empty( $this->field['parent_repeater'] ) || ! empty( $this->field['parent_layout'] ) ) {
- $this->field['pagination'] = false;
- }
- // We don't yet support pagination in frontend forms or inside blocks.
- if ( ! is_admin() || acf_get_data( 'acf_inside_rest_call' ) || doing_action( 'wp_ajax_acf/ajax/fetch-block' ) ) {
- $this->field['pagination'] = false;
- }
- $this->setup();
- }
- /**
- * Sets up the field for rendering.
- *
- * @since 6.0.0
- *
- * @return void
- */
- private function setup() {
- if ( $this->field['collapsed'] ) {
- foreach ( $this->sub_fields as &$sub_field ) {
- // Add target class.
- if ( $sub_field['key'] == $this->field['collapsed'] ) {
- $sub_field['wrapper']['class'] .= ' -collapsed-target';
- }
- }
- }
- if ( $this->field['max'] ) {
- // If max 1 row, don't show order.
- if ( 1 == $this->field['max'] ) {
- $this->show_order = false;
- }
- // If max == min, don't show add or remove buttons.
- if ( $this->field['max'] <= $this->field['min'] ) {
- $this->show_remove = false;
- $this->show_add = false;
- }
- }
- if ( empty( $this->field['rows_per_page'] ) ) {
- $this->field['rows_per_page'] = 20;
- }
- if ( (int) $this->field['rows_per_page'] < 1 ) {
- $this->field['rows_per_page'] = 20;
- }
- $this->value = $this->prepare_value();
- }
- /**
- * Prepares the repeater values for rendering.
- *
- * @since 6.0.0
- *
- * @return array
- */
- private function prepare_value() {
- $value = is_array( $this->field['value'] ) ? $this->field['value'] : array();
- if ( empty( $this->field['pagination'] ) ) {
- // If there are fewer values than min, populate the extra values.
- if ( $this->field['min'] ) {
- $value = array_pad( $value, $this->field['min'], array() );
- }
- // If there are more values than max, remove some values.
- if ( $this->field['max'] ) {
- $value = array_slice( $value, 0, $this->field['max'] );
- }
- }
- $value['acfcloneindex'] = array();
- return $value;
- }
- /**
- * Renders the full repeater table.
- *
- * @since 6.0.0
- *
- * @return void
- */
- public function render() {
- // Attributes for main wrapper div.
- $div = array(
- 'class' => 'acf-repeater -' . $this->field['layout'],
- 'data-min' => $this->field['min'],
- 'data-max' => $this->field['max'],
- 'data-pagination' => ! empty( $this->field['pagination'] ),
- );
- if ( $this->field['pagination'] ) {
- $div['data-per_page'] = $this->field['rows_per_page'];
- $div['data-total_rows'] = $this->field['total_rows'];
- $div['data-orig_name'] = $this->field['orig_name'];
- }
- if ( empty( $this->value ) ) {
- $div['class'] .= ' -empty';
- }
- ?>
- <div <?php echo acf_esc_attrs( $div ); ?>>
- <?php
- acf_hidden_input(
- array(
- 'name' => $this->field['name'],
- 'value' => '',
- 'class' => 'acf-repeater-hidden-input',
- )
- );
- ?>
- <table class="acf-table">
- <?php $this->thead(); ?>
- <tbody>
- <?php $this->rows(); ?>
- </tbody>
- </table>
- <?php $this->table_actions(); ?>
- </div>
- <?php
- }
- /**
- * Renders the table head.
- *
- * @since 6.0.0
- *
- * @return void
- */
- public function thead() {
- if ( 'table' !== $this->field['layout'] ) {
- return;
- }
- ?>
- <thead>
- <tr>
- <?php if ( $this->show_order ) : ?>
- <th class="acf-row-handle"></th>
- <?php endif; ?>
- <?php
- foreach ( $this->sub_fields as $sub_field ) :
- // Prepare field (allow sub fields to be removed).
- $sub_field = acf_prepare_field( $sub_field );
- if ( ! $sub_field ) {
- continue;
- }
- // Define attrs.
- $attrs = array(
- 'class' => 'acf-th',
- 'data-name' => $sub_field['_name'],
- 'data-type' => $sub_field['type'],
- 'data-key' => $sub_field['key'],
- );
- if ( $sub_field['wrapper']['width'] ) {
- $attrs['data-width'] = $sub_field['wrapper']['width'];
- $attrs['style'] = 'width: ' . $sub_field['wrapper']['width'] . '%;';
- }
- // Remove "id" to avoid "for" attribute on <label>.
- $sub_field['id'] = '';
- ?>
- <th <?php echo acf_esc_attrs( $attrs ); ?>>
- <?php acf_render_field_label( $sub_field ); ?>
- <?php acf_render_field_instructions( $sub_field ); ?>
- </th>
- <?php endforeach; ?>
- <?php if ( $this->show_remove ) : ?>
- <th class="acf-row-handle"></th>
- <?php endif; ?>
- </tr>
- </thead>
- <?php
- }
- /**
- * Renders or returns rows for the repeater field table.
- *
- * @since 6.0.0
- *
- * @param bool $return If we should return the rows or render them.
- * @return array|void
- */
- public function rows( $return = false ) {
- $rows = array();
- // Don't include the clone when rendering via AJAX.
- if ( $return && isset( $this->value['acfcloneindex'] ) ) {
- unset( $this->value['acfcloneindex'] );
- }
- foreach ( $this->value as $i => $row ) {
- $rows[ $i ] = $this->row( $i, $row, $return );
- }
- if ( $return ) {
- return $rows;
- }
- echo implode( PHP_EOL, $rows );
- }
- /**
- * Renders an individual row.
- *
- * @since 6.0.0
- *
- * @param int $i The row number.
- * @param array $row An array containing the row values.
- * @param bool $return If we should return the row or render it.
- * @return string|void
- */
- public function row( $i, $row, $return = false ) {
- if ( $return ) {
- ob_start();
- }
- $id = "row-$i";
- $class = 'acf-row';
- if ( 'acfcloneindex' === $i ) {
- $id = 'acfcloneindex';
- $class .= ' acf-clone';
- }
- $el = 'td';
- $before_fields = '';
- $after_fields = '';
- if ( 'row' === $this->field['layout'] ) {
- $el = 'div';
- $before_fields = '<td class="acf-fields -left">';
- $after_fields = '</td>';
- } elseif ( 'block' === $this->field['layout'] ) {
- $el = 'div';
- $before_fields = '<td class="acf-fields">';
- $after_fields = '</td>';
- }
- printf(
- '<tr class="%s" data-id="%s">',
- esc_attr( $class ),
- esc_attr( $id )
- );
- $this->row_handle( $i );
- echo $before_fields;
- foreach ( $this->sub_fields as $sub_field ) {
- if ( isset( $row[ $sub_field['key'] ] ) ) {
- $sub_field['value'] = $row[ $sub_field['key'] ];
- } elseif ( isset( $sub_field['default_value'] ) ) {
- $sub_field['value'] = $sub_field['default_value'];
- }
- // Update prefix to allow for nested values.
- $sub_field['prefix'] = $this->field['name'] . '[' . $id . ']';
- acf_render_field_wrap( $sub_field, $el );
- }
- echo $after_fields;
- $this->row_actions();
- echo '</tr>';
- if ( $return ) {
- return ob_get_clean();
- }
- }
- /**
- * Renders the row handle at the start of each row.
- *
- * @since 6.0.0
- *
- * @param int $i The current row number.
- * @return void
- */
- public function row_handle( $i ) {
- if ( ! $this->show_order ) {
- return;
- }
- $hr_row_num = intval( $i ) + 1;
- $classes = 'acf-row-handle order';
- $title = __( 'Drag to reorder', 'acf' );
- $row_num_html = sprintf(
- '<span class="acf-row-number" title="%s">%d</span>',
- __( 'Click to reorder', 'acf' ),
- $hr_row_num
- );
- if ( ! empty( $this->field['pagination'] ) ) {
- $classes .= ' pagination';
- $title = '';
- $input = sprintf( '<input type="number" class="acf-order-input" value="%d" style="display: none;" />', $hr_row_num );
- $row_num_html = '<div class="acf-order-input-wrap">' . $input . $row_num_html . '</div>';
- }
- ?>
- <td class="<?php echo $classes; ?>" title="<?php echo $title; ?>">
- <?php if ( $this->field['collapsed'] ) : ?>
- <a class="acf-icon -collapse small" href="#" data-event="collapse-row" title="<?php _e( 'Click to toggle', 'acf' ); ?>"></a>
- <?php endif; ?>
- <?php echo $row_num_html; ?>
- </td>
- <?php
- }
- /**
- * Renders the actions displayed at the end of each row.
- *
- * @since 6.0.0
- *
- * @return void
- */
- public function row_actions() {
- if ( ! $this->show_remove ) {
- return;
- }
- ?>
- <td class="acf-row-handle remove">
- <a class="acf-icon -plus small acf-js-tooltip hide-on-shift" href="#" data-event="add-row" title="<?php _e( 'Add row', 'acf' ); ?>"></a>
- <a class="acf-icon -duplicate small acf-js-tooltip show-on-shift" href="#" data-event="duplicate-row" title="<?php _e( 'Duplicate row', 'acf' ); ?>"></a>
- <a class="acf-icon -minus small acf-js-tooltip" href="#" data-event="remove-row" title="<?php _e( 'Remove row', 'acf' ); ?>"></a>
- </td>
- <?php
- }
- /**
- * Renders the actions displayed underneath the table.
- *
- * @since 6.0.0
- *
- * @return void
- */
- public function table_actions() {
- if ( ! $this->show_add ) {
- return;
- }
- ?>
- <div class="acf-actions">
- <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>
- <?php $this->pagination(); ?>
- <div class="clear"></div>
- </div>
- <?php
- }
- /**
- * Renders the table pagination.
- * Mostly lifted from the WordPress core WP_List_Table class.
- *
- * @since 6.0.0
- *
- * @return void
- */
- public function pagination() {
- if ( empty( $this->field['pagination'] ) ) {
- return;
- }
- $total_rows = isset( $this->field['total_rows'] ) ? (int) $this->field['total_rows'] : 0;
- $total_pages = ceil( $total_rows / (int) $this->field['rows_per_page'] );
- $total_pages = max( $total_pages, 1 );
- $html_current_page = sprintf(
- "%s<input class='current-page' id='current-page-selector' type='text' name='paged' value='%s' size='%d' aria-describedby='table-paging' />",
- '<label for="current-page-selector" class="screen-reader-text">' . __( 'Current Page', 'acf' ) . '</label>',
- 1,
- strlen( $total_pages )
- );
- $html_total_pages = sprintf( "<span class='acf-total-pages'>%s</span>", number_format_i18n( $total_pages ) );
- ?>
- <div class="acf-tablenav tablenav-pages">
- <a class="first-page button acf-nav" aria-hidden="true" data-event="first-page" title="<?php esc_attr_e( 'First Page', 'acf' ); ?>">
- <span class="screen-reader-text"><?php esc_html_e( 'First Page', 'acf' ); ?></span>
- <span aria-hidden="true">«</span>
- </a>
- <a class="prev-page button acf-nav" aria-hidden="true" data-event="prev-page" title="<?php esc_attr_e( 'Previous Page', 'acf' ); ?>">
- <span class="screen-reader-text"><?php esc_html_e( 'Previous Page', 'acf' ); ?></span>
- <span aria-hidden="true">‹</span>
- </a>
- <span class="paging-input">
- <label for="current-page-selector" class="screen-reader-text"><?php esc_html_e( 'Current Page', 'acf' ); ?></label>
- <span class="tablenav-paging-text" title="<?php esc_attr_e( 'Current Page', 'acf' ); ?>">
- <?php
- printf(
- /* translators: 1: Current page, 2: Total pages. */
- _x( '%1$s of %2$s', 'paging' ),
- $html_current_page,
- $html_total_pages
- );
- ?>
- </span>
- </span>
- <a class="next-page button acf-nav" data-event="next-page" title="<?php esc_attr_e( 'Next Page', 'acf' ); ?>">
- <span class="screen-reader-text"><?php esc_html_e( 'Next Page', 'acf' ); ?></span>
- <span aria-hidden="true">›</span>
- </a>
- <a class="last-page button acf-nav" data-event="last-page" title="<?php esc_attr_e( 'Last Page', 'acf' ); ?>">
- <span class="screen-reader-text"><?php esc_html_e( 'Last Page', 'acf' ); ?></span>
- <span aria-hidden="true">»</span>
- </a>
- </div>
- <?php
- }
- }
|