class-acf-field-repeater.php 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103
  1. <?php
  2. if ( ! class_exists( 'acf_field_repeater' ) ) :
  3. class acf_field_repeater extends acf_field {
  4. /**
  5. * If we're currently rendering fields.
  6. *
  7. * @var bool
  8. */
  9. public $is_rendering = false;
  10. /**
  11. * The post/page ID that we're rendering for.
  12. *
  13. * @var mixed
  14. */
  15. public $post_id = false;
  16. /**
  17. * This function will set up the field type data
  18. *
  19. * @date 5/03/2014
  20. * @since 5.0.0
  21. */
  22. public function initialize() {
  23. $this->name = 'repeater';
  24. $this->label = __( 'Repeater', 'acf' );
  25. $this->category = 'layout';
  26. $this->defaults = array(
  27. 'sub_fields' => array(),
  28. 'min' => 0,
  29. 'max' => 0,
  30. 'rows_per_page' => 20,
  31. 'layout' => 'table',
  32. 'button_label' => '',
  33. 'collapsed' => '',
  34. );
  35. // field filters
  36. $this->add_field_filter( 'acf/prepare_field_for_export', array( $this, 'prepare_field_for_export' ) );
  37. $this->add_field_filter( 'acf/prepare_field_for_import', array( $this, 'prepare_field_for_import' ) );
  38. // filters
  39. $this->add_filter( 'acf/validate_field', array( $this, 'validate_any_field' ) );
  40. $this->add_filter( 'acf/pre_render_fields', array( $this, 'pre_render_fields' ), 10, 2 );
  41. add_action( 'wp_ajax_acf/ajax/query_repeater', array( $this, 'ajax_get_rows' ) );
  42. }
  43. /**
  44. * Localizes text for the repeater field.
  45. *
  46. * @date 16/12/2015
  47. * @since 5.3.2
  48. */
  49. public function input_admin_enqueue_scripts() {
  50. acf_localize_text(
  51. array(
  52. 'Minimum rows reached ({min} rows)' => __( 'Minimum rows reached ({min} rows)', 'acf' ),
  53. 'Maximum rows reached ({max} rows)' => __( 'Maximum rows reached ({max} rows)', 'acf' ),
  54. 'Error loading page' => __( 'Error loading page', 'acf' ),
  55. 'Order will be assigned upon save' => __( 'Order will be assigned upon save', 'acf' ),
  56. )
  57. );
  58. }
  59. /**
  60. * Filters the field array after it's loaded from the database.
  61. *
  62. * @since 3.6
  63. * @date 23/01/13
  64. *
  65. * @param array $field The field array holding all the field options.
  66. * @return array
  67. */
  68. public function load_field( $field ) {
  69. $field['min'] = (int) $field['min'];
  70. $field['max'] = (int) $field['max'];
  71. $sub_fields = acf_get_fields( $field );
  72. if ( $sub_fields ) {
  73. $field['sub_fields'] = array_map(
  74. function( $sub_field ) use ( $field ) {
  75. $sub_field['parent_repeater'] = $field['key'];
  76. return $sub_field;
  77. },
  78. $sub_fields
  79. );
  80. }
  81. if ( empty( $field['rows_per_page'] ) || (int) $field['rows_per_page'] < 1 ) {
  82. $field['rows_per_page'] = 20;
  83. }
  84. if ( '' === $field['button_label'] ) {
  85. $field['button_label'] = __( 'Add Row', 'acf' );
  86. }
  87. return $field;
  88. }
  89. /**
  90. * Runs on the "acf/pre_render_fields" filter. Used to signify
  91. * that we're currently rendering a repeater field.
  92. *
  93. * @since 6.0.0
  94. *
  95. * @param array $fields The main field array.
  96. * @param mixed $post_id The post ID for the field being rendered.
  97. * @return array
  98. */
  99. public function pre_render_fields( $fields, $post_id = false ) {
  100. if ( is_admin() ) {
  101. $this->is_rendering = true;
  102. $this->post_id = $post_id;
  103. }
  104. return $fields;
  105. }
  106. /**
  107. * Create the HTML interface for your field
  108. *
  109. * @since 3.6
  110. * @date 23/01/13
  111. *
  112. * @param array $field An array holding all the field's data.
  113. */
  114. public function render_field( $field ) {
  115. $field['orig_name'] = $this->get_field_name_from_input_name( $field['name'] );
  116. $field['total_rows'] = (int) acf_get_metadata( $this->post_id, $field['orig_name'] );
  117. $table = new ACF_Repeater_Table( $field );
  118. $table->render();
  119. }
  120. /**
  121. * Create extra options for your field. This is rendered when editing a field.
  122. * The value of $field['name'] can be used (like bellow) to save extra data to the $field
  123. *
  124. * @since 3.6
  125. * @date 23/01/13
  126. *
  127. * @param array $field An array holding all the field's data.
  128. */
  129. function render_field_settings( $field ) {
  130. $args = array(
  131. 'fields' => $field['sub_fields'],
  132. 'parent' => $field['ID'],
  133. 'is_subfield' => true,
  134. );
  135. $supports_pagination = ( empty( $field['parent_repeater'] ) && empty( $field['parent_layout'] ) );
  136. ?>
  137. <div class="acf-field acf-field-setting-sub_fields" data-setting="repeater" data-name="sub_fields">
  138. <div class="acf-label">
  139. <label><?php _e( 'Sub Fields', 'acf' ); ?></label>
  140. <p class="description"></p>
  141. </div>
  142. <div class="acf-input acf-input-sub">
  143. <?php
  144. acf_get_view( 'field-group-fields', $args );
  145. ?>
  146. </div>
  147. </div>
  148. <?php
  149. acf_render_field_setting(
  150. $field,
  151. array(
  152. 'label' => __( 'Layout', 'acf' ),
  153. 'instructions' => '',
  154. 'class' => 'acf-repeater-layout',
  155. 'type' => 'radio',
  156. 'name' => 'layout',
  157. 'layout' => 'horizontal',
  158. 'choices' => array(
  159. 'table' => __( 'Table', 'acf' ),
  160. 'block' => __( 'Block', 'acf' ),
  161. 'row' => __( 'Row', 'acf' ),
  162. ),
  163. )
  164. );
  165. if ( $supports_pagination ) {
  166. acf_render_field_setting(
  167. $field,
  168. array(
  169. 'label' => __( 'Pagination', 'acf' ),
  170. 'instructions' => __( 'Useful for fields with a large number of rows.', 'acf' ),
  171. 'class' => 'acf-repeater-pagination',
  172. 'type' => 'true_false',
  173. 'name' => 'pagination',
  174. 'ui' => 1,
  175. )
  176. );
  177. acf_render_field_setting(
  178. $field,
  179. array(
  180. 'label' => __( 'Rows Per Page', 'acf' ),
  181. 'instructions' => __( 'Set the number of rows to be displayed on a page.', 'acf' ),
  182. 'class' => 'acf-repeater-pagination-num-rows',
  183. 'type' => 'number',
  184. 'name' => 'rows_per_page',
  185. 'placeholder' => 20,
  186. 'ui' => 1,
  187. 'min' => 1,
  188. 'conditions' => array(
  189. 'field' => 'pagination',
  190. 'operator' => '==',
  191. 'value' => 1,
  192. ),
  193. )
  194. );
  195. }
  196. }
  197. /**
  198. * Renders the field settings used in the "Validation" tab.
  199. *
  200. * @since 6.0
  201. *
  202. * @param array $field The field settings array.
  203. * @return void
  204. */
  205. function render_field_validation_settings( $field ) {
  206. $field['min'] = empty( $field['min'] ) ? '' : $field['min'];
  207. $field['max'] = empty( $field['max'] ) ? '' : $field['max'];
  208. acf_render_field_setting(
  209. $field,
  210. array(
  211. 'label' => __( 'Minimum Rows', 'acf' ),
  212. 'instructions' => '',
  213. 'type' => 'number',
  214. 'name' => 'min',
  215. 'placeholder' => '0',
  216. )
  217. );
  218. acf_render_field_setting(
  219. $field,
  220. array(
  221. 'label' => __( 'Maximum Rows', 'acf' ),
  222. 'instructions' => '',
  223. 'type' => 'number',
  224. 'name' => 'max',
  225. 'placeholder' => '0',
  226. )
  227. );
  228. }
  229. /**
  230. * Renders the field settings used in the "Presentation" tab.
  231. *
  232. * @since 6.0
  233. *
  234. * @param array $field The field settings array.
  235. * @return void
  236. */
  237. function render_field_presentation_settings( $field ) {
  238. $choices = array();
  239. if ( $field['collapsed'] ) {
  240. $sub_field = acf_get_field( $field['collapsed'] );
  241. if ( $sub_field ) {
  242. $choices[ $sub_field['key'] ] = $sub_field['label'];
  243. }
  244. }
  245. acf_render_field_setting(
  246. $field,
  247. array(
  248. 'label' => __( 'Collapsed', 'acf' ),
  249. 'instructions' => __( 'Select a sub field to show when row is collapsed', 'acf' ),
  250. 'type' => 'select',
  251. 'name' => 'collapsed',
  252. 'allow_null' => 1,
  253. 'choices' => $choices,
  254. )
  255. );
  256. acf_render_field_setting(
  257. $field,
  258. array(
  259. 'label' => __( 'Button Label', 'acf' ),
  260. 'instructions' => '',
  261. 'type' => 'text',
  262. 'name' => 'button_label',
  263. 'placeholder' => __( 'Add Row', 'acf' ),
  264. )
  265. );
  266. }
  267. /**
  268. * Filters the field $value after it is loaded from the database.
  269. *
  270. * @since 3.6
  271. * @date 23/01/13
  272. *
  273. * @param mixed $value The value found in the database.
  274. * @param mixed $post_id The $post_id from which the value was loaded.
  275. * @param array $field The field array holding all the field options.
  276. * @return array $value
  277. */
  278. public function load_value( $value, $post_id, $field ) {
  279. // Bail early if we don't have enough info to load the field.
  280. if ( empty( $value ) || ! is_numeric( $value ) || empty( $field['sub_fields'] ) ) {
  281. return false;
  282. }
  283. $value = (int) $value;
  284. $rows = array();
  285. $offset = 0;
  286. // Ensure pagination is disabled inside blocks.
  287. if ( acf_get_data( 'acf_inside_rest_call' ) || doing_action( 'wp_ajax_acf/ajax/fetch-block' ) ) {
  288. $field['pagination'] = false;
  289. }
  290. if ( ! empty( $field['pagination'] ) && $this->is_rendering ) {
  291. $rows_per_page = isset( $field['rows_per_page'] ) ? (int) $field['rows_per_page'] : 20;
  292. if ( $rows_per_page < 1 ) {
  293. $rows_per_page = 20;
  294. }
  295. if ( doing_action( 'wp_ajax_acf/ajax/query_repeater' ) ) {
  296. $offset = ( intval( $_POST['paged'] ) - 1 ) * $rows_per_page; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified elsewhere.
  297. $value = min( $value, $offset + $rows_per_page );
  298. } else {
  299. $value = min( $value, $rows_per_page );
  300. }
  301. }
  302. for ( $i = $offset; $i < $value; $i++ ) {
  303. $rows[ $i ] = array();
  304. foreach ( array_keys( $field['sub_fields'] ) as $j ) {
  305. $sub_field = $field['sub_fields'][ $j ];
  306. // Bail early if no name (tab field).
  307. if ( acf_is_empty( $sub_field['name'] ) ) {
  308. continue;
  309. }
  310. // Update $sub_field name and value.
  311. $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}";
  312. $sub_value = acf_get_value( $post_id, $sub_field );
  313. $rows[ $i ][ $sub_field['key'] ] = $sub_value;
  314. }
  315. }
  316. return $rows;
  317. }
  318. /**
  319. * This filter is applied to the $value after it is loaded from the db,
  320. * and before it is returned to the template.
  321. *
  322. * @since 3.6
  323. * @date 23/01/13
  324. *
  325. * @param mixed $value The value which was loaded from the database.
  326. * @param mixed $post_id The $post_id from which the value was loaded.
  327. * @param array $field The field array holding all the field options.
  328. *
  329. * @return array $value The modified value.
  330. */
  331. function format_value( $value, $post_id, $field ) {
  332. // bail early if no value
  333. if ( empty( $value ) ) {
  334. return false;
  335. }
  336. // bail early if not array
  337. if ( ! is_array( $value ) ) {
  338. return false;
  339. }
  340. // bail early if no sub fields
  341. if ( empty( $field['sub_fields'] ) ) {
  342. return false;
  343. }
  344. // loop over rows
  345. foreach ( array_keys( $value ) as $i ) {
  346. // loop through sub fields
  347. foreach ( array_keys( $field['sub_fields'] ) as $j ) {
  348. // get sub field
  349. $sub_field = $field['sub_fields'][ $j ];
  350. // bail early if no name (tab)
  351. if ( acf_is_empty( $sub_field['name'] ) ) {
  352. continue;
  353. }
  354. // extract value
  355. $sub_value = acf_extract_var( $value[ $i ], $sub_field['key'] );
  356. // update $sub_field name
  357. $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}";
  358. // format value
  359. $sub_value = acf_format_value( $sub_value, $post_id, $sub_field );
  360. // append to $row
  361. $value[ $i ][ $sub_field['_name'] ] = $sub_value;
  362. }
  363. }
  364. return $value;
  365. }
  366. /**
  367. * Validates values for the repeater field
  368. *
  369. * @date 11/02/2014
  370. * @since 5.0.0
  371. *
  372. * @param bool $valid If the field is valid.
  373. * @param mixed $value The value to validate.
  374. * @param array $field The main field array.
  375. * @param string $input The input element's name attribute.
  376. *
  377. * @return bool
  378. */
  379. function validate_value( $valid, $value, $field, $input ) {
  380. // vars
  381. $count = 0;
  382. // check if is value (may be empty string)
  383. if ( is_array( $value ) ) {
  384. // remove acfcloneindex
  385. if ( isset( $value['acfcloneindex'] ) ) {
  386. unset( $value['acfcloneindex'] );
  387. }
  388. // count
  389. $count = count( $value );
  390. }
  391. // validate required
  392. if ( $field['required'] && ! $count ) {
  393. $valid = false;
  394. }
  395. // min
  396. $min = (int) $field['min'];
  397. if ( empty( $field['pagination'] ) && $min && $count < $min ) {
  398. // create error
  399. $error = __( 'Minimum rows reached ({min} rows)', 'acf' );
  400. $error = str_replace( '{min}', $min, $error );
  401. // return
  402. return $error;
  403. }
  404. // validate value
  405. if ( $count ) {
  406. // bail early if no sub fields
  407. if ( ! $field['sub_fields'] ) {
  408. return $valid;
  409. }
  410. // loop rows
  411. foreach ( $value as $i => $row ) {
  412. // Skip rows that were deleted in paginated repeaters.
  413. if ( false !== strpos( $i, '_deleted' ) ) {
  414. continue;
  415. }
  416. // loop sub fields
  417. foreach ( $field['sub_fields'] as $sub_field ) {
  418. // vars
  419. $k = $sub_field['key'];
  420. // test sub field exists
  421. if ( ! isset( $row[ $k ] ) ) {
  422. continue;
  423. }
  424. // validate
  425. acf_validate_value( $row[ $k ], $sub_field, "{$input}[{$i}][{$k}]" );
  426. }
  427. // end loop sub fields
  428. }
  429. // end loop rows
  430. }
  431. return $valid;
  432. }
  433. /**
  434. * This function will update a value row.
  435. *
  436. * @date 15/2/17
  437. * @since 5.5.8
  438. *
  439. * @param array $row
  440. * @param int $i
  441. * @param array $field
  442. * @param mixed $post_id
  443. * @return boolean
  444. */
  445. function update_row( $row, $i, $field, $post_id ) {
  446. // bail early if no layout reference
  447. if ( ! is_array( $row ) ) {
  448. return false;
  449. }
  450. // bail early if no layout
  451. if ( empty( $field['sub_fields'] ) ) {
  452. return false;
  453. }
  454. foreach ( $field['sub_fields'] as $sub_field ) {
  455. $value = null;
  456. if ( array_key_exists( $sub_field['key'], $row ) ) {
  457. $value = $row[ $sub_field['key'] ];
  458. } elseif ( array_key_exists( $sub_field['name'], $row ) ) {
  459. $value = $row[ $sub_field['name'] ];
  460. } else {
  461. // Value does not exist.
  462. continue;
  463. }
  464. // modify name for save
  465. $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}";
  466. // update field
  467. acf_update_value( $value, $post_id, $sub_field );
  468. }
  469. return true;
  470. }
  471. /**
  472. * This function will delete a value row.
  473. *
  474. * @date 15/2/17
  475. * @since 5.5.8
  476. *
  477. * @param int $i
  478. * @param array $field
  479. * @param mixed $post_id
  480. * @return boolean
  481. */
  482. function delete_row( $i, $field, $post_id ) {
  483. // bail early if no sub fields
  484. if ( empty( $field['sub_fields'] ) ) {
  485. return false;
  486. }
  487. foreach ( $field['sub_fields'] as $sub_field ) {
  488. // modify name for delete
  489. $sub_field['name'] = "{$field['name']}_{$i}_{$sub_field['name']}";
  490. // delete value
  491. acf_delete_value( $post_id, $sub_field );
  492. }
  493. return true;
  494. }
  495. /**
  496. * Filters the $value before it is updated in the database.
  497. *
  498. * @since 3.6
  499. * @date 23/01/13
  500. *
  501. * @param mixed $value The value which will be saved in the database.
  502. * @param array $field The field array holding all the field options.
  503. * @param mixed $post_id The $post_id of which the value will be saved.
  504. *
  505. * @return mixed $value
  506. */
  507. function update_value( $value, $post_id, $field ) {
  508. // Bail early if no sub fields.
  509. if ( empty( $field['sub_fields'] ) ) {
  510. return $value;
  511. }
  512. if ( ! is_array( $value ) ) {
  513. $value = array();
  514. }
  515. if ( isset( $value['acfcloneindex'] ) ) {
  516. unset( $value['acfcloneindex'] );
  517. }
  518. $new_value = 0;
  519. $old_value = (int) acf_get_metadata( $post_id, $field['name'] );
  520. if ( ! empty( $field['pagination'] ) && did_action( 'acf/save_post' ) && ! isset( $_POST['_acf_form'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Value not used.
  521. $old_rows = acf_get_value( $post_id, $field );
  522. $old_rows = is_array( $old_rows ) ? $old_rows : array();
  523. $edited_rows = array();
  524. $deleted_rows = array();
  525. $reordered_rows = array();
  526. $new_rows = array();
  527. // Categorize the submitted values, so we know what to do with them.
  528. foreach ( $value as $key => $row ) {
  529. if ( ! is_array( $row ) ) {
  530. continue;
  531. }
  532. // Check if this is a new row.
  533. if ( false === strpos( $key, 'row' ) ) {
  534. unset( $row['acf_changed'] );
  535. // Check if this new row was inserted before an existing row.
  536. $reordered_row_num = isset( $row['acf_reordered'] ) ? (int) $row['acf_reordered'] : false;
  537. if ( false !== $reordered_row_num && $reordered_row_num <= $old_value ) {
  538. $reordered_rows[ $key ] = $reordered_row_num;
  539. } else {
  540. $new_rows[ $key ] = $row;
  541. }
  542. continue;
  543. }
  544. $row_num = (int) str_replace( 'row-', '', $key );
  545. if ( isset( $row['acf_deleted'] ) ) {
  546. $deleted_rows[] = $row_num;
  547. } elseif ( isset( $row['acf_reordered'] ) ) {
  548. $reordered_rows[ $row_num ] = (int) $row['acf_reordered'];
  549. } else {
  550. unset( $row['acf_changed'] );
  551. $edited_rows[ $row_num ] = $row;
  552. }
  553. }
  554. // Process any row deletions first, but don't remove their keys yet.
  555. foreach ( $deleted_rows as $deleted_row ) {
  556. $this->delete_row( $deleted_row, $field, $post_id );
  557. $old_rows[ $deleted_row ] = 'acf_deleted';
  558. }
  559. // Update any existing rows that were edited.
  560. foreach ( $edited_rows as $key => $row ) {
  561. if ( array_key_exists( $key, $old_rows ) ) {
  562. $old_rows[ $key ] = $row;
  563. }
  564. }
  565. $rows_to_move = array();
  566. $new_rows_to_move = array();
  567. foreach ( $reordered_rows as $old_order => $new_order ) {
  568. if ( is_int( $old_order ) ) {
  569. $rows_to_move[ $new_order ][] = $value[ 'row-' . $old_order ];
  570. unset( $old_rows[ $old_order ] );
  571. } else {
  572. $new_rows_to_move[ $new_order ][] = $value[ $old_order ];
  573. }
  574. }
  575. // Iterate over existing moved rows first.
  576. if ( ! empty( $rows_to_move ) ) {
  577. ksort( $rows_to_move );
  578. foreach ( $rows_to_move as $key => $values ) {
  579. array_splice( $old_rows, $key, 0, $values );
  580. }
  581. }
  582. // Iterate over inserted/duplicated rows in reverse order, so they're inserted into the correct spot.
  583. if ( ! empty( $new_rows_to_move ) ) {
  584. krsort( $new_rows_to_move );
  585. foreach ( $new_rows_to_move as $key => $values ) {
  586. array_splice( $old_rows, $key, 0, $values );
  587. }
  588. }
  589. // Append any new rows.
  590. foreach ( $new_rows as $new_row ) {
  591. $old_rows[] = $new_row;
  592. }
  593. // Update the rows in the database.
  594. $new_row_num = 0;
  595. foreach ( $old_rows as $key => $row ) {
  596. if ( 'acf_deleted' === $row ) {
  597. unset( $old_rows[ $key ] );
  598. continue;
  599. }
  600. $this->update_row( $row, $new_row_num, $field, $post_id );
  601. $new_row_num++;
  602. }
  603. // Calculate the total number of rows that will be saved after this update.
  604. $new_value = count( $old_rows );
  605. } else {
  606. $i = -1;
  607. foreach ( $value as $row ) {
  608. $i++;
  609. // Bail early if no row.
  610. if ( ! is_array( $row ) ) {
  611. continue;
  612. }
  613. $this->update_row( $row, $i, $field, $post_id );
  614. $new_value++;
  615. }
  616. }
  617. // Remove old rows.
  618. if ( $old_value > $new_value ) {
  619. for ( $i = $new_value; $i < $old_value; $i++ ) {
  620. $this->delete_row( $i, $field, $post_id );
  621. }
  622. }
  623. // Save empty string for empty value.
  624. if ( empty( $new_value ) ) {
  625. $new_value = '';
  626. }
  627. return $new_value;
  628. }
  629. /**
  630. * Deletes any subfields after the field has been deleted.
  631. *
  632. * @date 4/04/2014
  633. * @since 5.0.0
  634. *
  635. * @param array $field The main field array.
  636. * @return void
  637. */
  638. function delete_field( $field ) {
  639. // Bail early if no subfields.
  640. if ( empty( $field['sub_fields'] ) ) {
  641. return;
  642. }
  643. // Delete any subfields.
  644. foreach ( $field['sub_fields'] as $sub_field ) {
  645. acf_delete_field( $sub_field['ID'] );
  646. }
  647. }
  648. /**
  649. * Deletes a value from the database.
  650. *
  651. * @date 1/07/2015
  652. * @since 5.2.3
  653. *
  654. * @param int $post_id The post ID to delete the value from.
  655. * @param string $key The meta name/key (unused).
  656. * @param array $field The main field array.
  657. * @return void
  658. */
  659. function delete_value( $post_id, $key, $field ) {
  660. // Get the old value from the database.
  661. $old_value = (int) acf_get_metadata( $post_id, $field['name'] );
  662. // Bail early if no rows or no subfields.
  663. if ( ! $old_value || empty( $field['sub_fields'] ) ) {
  664. return;
  665. }
  666. for ( $i = 0; $i < $old_value; $i++ ) {
  667. $this->delete_row( $i, $field, $post_id );
  668. }
  669. }
  670. /**
  671. * This filter is applied to the $field before it is saved to the database.
  672. *
  673. * @since 3.6
  674. * @date 23/01/13
  675. *
  676. * @param array $field The field array holding all the field options.
  677. *
  678. * @return array
  679. */
  680. function update_field( $field ) {
  681. unset( $field['sub_fields'] );
  682. return $field;
  683. }
  684. /**
  685. * This filter is applied to the $field before it is duplicated and saved to the database.
  686. *
  687. * @since 3.6
  688. * @date 23/01/13
  689. *
  690. * @param array $field The field array holding all the field options.
  691. * @return array
  692. */
  693. function duplicate_field( $field ) {
  694. // get sub fields
  695. $sub_fields = acf_extract_var( $field, 'sub_fields' );
  696. // save field to get ID
  697. $field = acf_update_field( $field );
  698. // duplicate sub fields
  699. acf_duplicate_fields( $sub_fields, $field['ID'] );
  700. return $field;
  701. }
  702. /**
  703. * This function will translate field settings.
  704. *
  705. * @date 8/03/2016
  706. * @since 5.3.2
  707. *
  708. * @param array $field The main field array.
  709. * @return array
  710. */
  711. function translate_field( $field ) {
  712. $field['button_label'] = acf_translate( $field['button_label'] );
  713. return $field;
  714. }
  715. /**
  716. * This function will add compatibility for the 'column_width' setting
  717. *
  718. * @date 30/1/17
  719. * @since 5.5.6
  720. *
  721. * @param array $field The main field array.
  722. * @return array
  723. */
  724. function validate_any_field( $field ) {
  725. // width has changed
  726. if ( isset( $field['column_width'] ) ) {
  727. $field['wrapper']['width'] = acf_extract_var( $field, 'column_width' );
  728. }
  729. return $field;
  730. }
  731. /**
  732. * Prepares the field for export.
  733. *
  734. * @date 11/03/2014
  735. * @since 5.0.0
  736. *
  737. * @param array $field The field settings.
  738. * @return array
  739. */
  740. function prepare_field_for_export( $field ) {
  741. // Check for subfields.
  742. if ( ! empty( $field['sub_fields'] ) ) {
  743. $field['sub_fields'] = acf_prepare_fields_for_export( $field['sub_fields'] );
  744. }
  745. return $field;
  746. }
  747. /**
  748. * Returns a flat array of fields containing all subfields ready for import.
  749. *
  750. * @date 11/03/2014
  751. * @since 5.0.0
  752. *
  753. * @param array $field The field settings.
  754. * @return array
  755. */
  756. function prepare_field_for_import( $field ) {
  757. // Check for sub fields.
  758. if ( ! empty( $field['sub_fields'] ) ) {
  759. $sub_fields = acf_extract_var( $field, 'sub_fields' );
  760. // Modify sub fields.
  761. foreach ( $sub_fields as $i => $sub_field ) {
  762. $sub_fields[ $i ]['parent'] = $field['key'];
  763. $sub_fields[ $i ]['menu_order'] = $i;
  764. }
  765. // Return array of [field, sub_1, sub_2, ...].
  766. return array_merge( array( $field ), $sub_fields );
  767. }
  768. return $field;
  769. }
  770. /**
  771. * Additional validation for the repeater field when submitted via REST.
  772. *
  773. * @param bool $valid
  774. * @param int $value
  775. * @param array $field
  776. *
  777. * @return bool|WP_Error
  778. */
  779. public function validate_rest_value( $valid, $value, $field ) {
  780. if ( ! is_array( $value ) && is_null( $value ) ) {
  781. $param = sprintf( '%s[%s]', $field['prefix'], $field['name'] );
  782. $data = array(
  783. 'param' => $param,
  784. 'value' => $value,
  785. );
  786. $error = sprintf( __( '%s must be of type array or null.', 'acf' ), $param );
  787. return new WP_Error( 'rest_invalid_param', $error, $param );
  788. }
  789. return $valid;
  790. }
  791. /**
  792. * Return the schema array for the REST API.
  793. *
  794. * @param array $field
  795. * @return array
  796. */
  797. public function get_rest_schema( array $field ) {
  798. $schema = array(
  799. 'type' => array( 'array', 'null' ),
  800. 'required' => ! empty( $field['required'] ),
  801. 'items' => array(
  802. 'type' => 'object',
  803. 'properties' => array(),
  804. ),
  805. );
  806. foreach ( $field['sub_fields'] as $sub_field ) {
  807. if ( $sub_field_schema = acf_get_field_rest_schema( $sub_field ) ) {
  808. $schema['items']['properties'][ $sub_field['name'] ] = $sub_field_schema;
  809. }
  810. }
  811. if ( ! empty( $field['min'] ) ) {
  812. $schema['minItems'] = (int) $field['min'];
  813. }
  814. if ( ! empty( $field['max'] ) ) {
  815. $schema['maxItems'] = (int) $field['max'];
  816. }
  817. return $schema;
  818. }
  819. /**
  820. * Apply basic formatting to prepare the value for default REST output.
  821. *
  822. * @param mixed $value
  823. * @param int|string $post_id
  824. * @param array $field
  825. * @return array|mixed
  826. */
  827. public function format_value_for_rest( $value, $post_id, array $field ) {
  828. if ( empty( $value ) || ! is_array( $value ) || empty( $field['sub_fields'] ) ) {
  829. return null;
  830. }
  831. // Loop through each row and within that, each sub field to process sub fields individually.
  832. foreach ( $value as &$row ) {
  833. foreach ( $field['sub_fields'] as $sub_field ) {
  834. // Bail early if the field has no name (tab).
  835. if ( acf_is_empty( $sub_field['name'] ) ) {
  836. continue;
  837. }
  838. // Extract the sub field 'field_key'=>'value' pair from the $row and format it.
  839. $sub_value = acf_extract_var( $row, $sub_field['key'] );
  840. $sub_value = acf_format_value_for_rest( $sub_value, $post_id, $sub_field );
  841. // Add the sub field value back to the $row but mapped to the field name instead
  842. // of the key reference.
  843. $row[ $sub_field['name'] ] = $sub_value;
  844. }
  845. }
  846. return $value;
  847. }
  848. /**
  849. * Takes the provided input name and turns it into a field name that
  850. * works with repeater fields that are subfields of other fields.
  851. *
  852. * @param string $input_name The name attribute used in the repeater.
  853. *
  854. * @return string|bool
  855. */
  856. public function get_field_name_from_input_name( $input_name ) {
  857. $parts = array();
  858. preg_match_all( '/\[([^\]]*)\]/', $input_name, $parts );
  859. if ( ! isset( $parts[1] ) ) {
  860. return false;
  861. }
  862. $field_keys = $parts[1];
  863. $name_parts = array();
  864. foreach ( $field_keys as $field_key ) {
  865. if ( ! acf_is_field_key( $field_key ) ) {
  866. if ( 'acfcloneindex' === $field_key ) {
  867. $name_parts[] = 'acfcloneindex';
  868. continue;
  869. }
  870. $row_num = str_replace( 'row-', '', $field_key );
  871. if ( is_numeric( $row_num ) ) {
  872. $name_parts[] = (int) $row_num;
  873. continue;
  874. }
  875. }
  876. $field = acf_get_field( $field_key );
  877. if ( $field ) {
  878. $name_parts[] = $field['name'];
  879. }
  880. }
  881. return implode( '_', $name_parts );
  882. }
  883. /**
  884. * Returns an array of rows used to populate the repeater table over AJAX.
  885. *
  886. * @since 6.0.0
  887. *
  888. * @return void|WP_Error
  889. */
  890. public function ajax_get_rows() {
  891. if ( ! acf_verify_ajax() ) {
  892. $error = array( 'error' => __( 'Invalid nonce.', 'acf' ) );
  893. wp_send_json_error( $error, 401 );
  894. }
  895. $args = acf_request_args(
  896. array(
  897. 'field_name' => '',
  898. 'field_key' => '',
  899. 'post_id' => 0,
  900. 'rows_per_page' => 0,
  901. 'refresh' => false,
  902. )
  903. );
  904. if ( '' === $args['field_name'] || '' === $args['field_key'] ) {
  905. $error = array( 'error' => __( 'Invalid field key or name.', 'acf' ) );
  906. wp_send_json_error( $error, 404 );
  907. }
  908. $field = acf_get_field( $args['field_key'] );
  909. $post_id = acf_get_valid_post_id( $args['post_id'] );
  910. $response = array();
  911. if ( ! $field || ! $post_id ) {
  912. $error = array( 'error' => __( 'There was an error retrieving the field.', 'acf' ) );
  913. wp_send_json_error( $error, 404 );
  914. }
  915. // Make sure we have a valid field.
  916. $field = acf_validate_field( $field );
  917. // Make sure that we only get a subset of the rows.
  918. $this->is_rendering = true;
  919. $args['rows_per_page'] = (int) $args['rows_per_page'];
  920. if ( $args['rows_per_page'] ) {
  921. $field['rows_per_page'] = $args['rows_per_page'];
  922. }
  923. /**
  924. * We have to swap out the field name with the one sent via JS,
  925. * as the repeater could be inside a subfield.
  926. */
  927. $field['name'] = $args['field_name'];
  928. $field['value'] = acf_get_value( $post_id, $field );
  929. $field = acf_prepare_field( $field );
  930. $repeater_table = new ACF_Repeater_Table( $field );
  931. $response['rows'] = $repeater_table->rows( true );
  932. if ( $args['refresh'] ) {
  933. $response['total_rows'] = (int) acf_get_metadata( $post_id, $args['field_name'] );
  934. }
  935. wp_send_json_success( $response );
  936. }
  937. }
  938. // initialize
  939. acf_register_field_type( 'acf_field_repeater' );
  940. endif; // class_exists check