class-acf-repeater-table.php 12 KB

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