shuffled = $shuffled; $this->field_state = $field_state; } /** * Registers hooks used while repeater field's values are being updated. */ public function register_hooks() { add_action( 'acf/save_post', array( $this, 'storeSynchroniseOption' ), 4, 1 ); add_action( 'acf/save_post', array( $this, 'store_state_before' ), 5, 1 ); add_action( 'acf/save_post', array( $this, 'update_translated_repeaters' ), 15, 1 ); add_action( 'acf/render_fields', array( $this, 'display_synchronisation_switcher' ), 10, 2 ); add_filter( 'wpml_custom_field_values_for_post_signature', [ $this, 'revertFieldValuesForSignature' ], 10, 2 ); } /** * Outputs HTML with checkbox to enable synchronisation for changes in order of fields. * * @param mixed $fields The ACF fields to display on the post edit screen. * @param int $element_id Current post ID. */ public function display_synchronisation_switcher( $fields, $element_id ) { if ( $this->hasRepeaterField( $fields ) && $this->should_display_synchronisation_switcher( $element_id ) ) { ?>
isSynchroniseOptionChecked( $element_id ), true, true ); ?> />
synchronisation_checkbox_displayed ) { $this->synchronisation_checkbox_displayed = true; $should = $this->shuffled->hasTranslations( $element_id ); } return $should; } /** * Load all existing translations for this post and all existing metadata for this post. * * @param int $post_id ID of the post being saved. */ public function store_state_before( $post_id = 0 ) { if ( $this->synchronise_option_selected() ) { $this->field_state->storeStateBefore( $post_id ); } } /** * @param int $post_id ID of the post being saved. */ public function update_translated_repeaters( $post_id = 0 ) { if ( $this->should_translation_update_run( $post_id ) ) { $this->meta_data_after_move = $this->field_state->getCurrentMetadata( $post_id ); foreach ( $this->meta_data_after_move as $key => $value ) { $key_change = $this->get_keys_for_meta_value_changed( $key, $value ); if ( $key_change ) { $translations = $this->shuffled->getTranslations( $post_id ); if ( $translations ) { $this->remove_deprecated_meta( $translations, $key_change['was'], $key_change['is'] ); } } } $this->readd_meta(); } } /** * Checks if post ID is given and if the state for comparision is already saved. * * @param int $post_id * * @return bool */ private function should_translation_update_run( $post_id = 0 ) { return $this->synchronise_option_selected() && $this->shuffled->isValidId( $post_id ) && $this->field_state->getStateBefore(); } /** * Returns meta key changed for given meta value. * * @param string $meta_key_after_move The new meta key. * @param mixed $meta_value_after_move Meta value. * * @return array */ private function get_keys_for_meta_value_changed( $meta_key_after_move, $meta_value_after_move ) { $changed = array(); // For given meta value after move, find related keys in data before move. $keys_before_move = $this->keys_before_move( $meta_value_after_move ); if ( $keys_before_move ) { // Now find keys for the same data but after move. $keys_after_move = $this->keys_after_move( $meta_value_after_move ); // Check if keys are different. $key_was = array_diff( $keys_before_move, $keys_after_move ); $key_is = array_diff( $keys_after_move, $keys_before_move ); $found = array_search( $meta_key_after_move, $key_is, true ); if ( false !== $found ) { $key_was = array_values( $key_was ); $key_is = array_values( $key_is ); if ( $key_was && $key_is ) { $changed = [ 'was' => array_shift( $key_was ), 'is' => array_shift( $key_is ), ]; } } } return $changed; } /** * Returns new meta keys for given meta value. * * @param mixed $meta_value_after_move * * @return array */ private function keys_after_move( $meta_value_after_move ) { return array_keys( $this->meta_data_after_move, $meta_value_after_move, true ); } /** * Returns original meta keys for given meta value. * * @param mixed $meta_value_after_move * * @return array */ private function keys_before_move( $meta_value_after_move ) { return array_keys( $this->field_state->getStateBefore(), $meta_value_after_move, true ); } /** * Re-add metas again from saved pairs. */ private function readd_meta() { if ( $this->meta_to_update ) { foreach ( $this->meta_to_update as $translated_post_id => $meta_pairs ) { foreach ( $meta_pairs as $pair ) { $this->shuffled->updateOneMeta( $translated_post_id, $pair[0], $pair[1] ); } } } } /** * Foreach existing translation remove it but keep its value in pair with new key. * * @param array $translations Post translations to update. * @param string $key_of_original_value Original meta key. * @param string $meta_key_after_move New meta key. */ private function remove_deprecated_meta( array $translations, $key_of_original_value, $meta_key_after_move ) { foreach ( $translations as $language_code => $translated_post_data ) { $translated_meta_value = $this->shuffled->getOneMeta( $translated_post_data->element_id, $key_of_original_value, true ); if ( $translated_meta_value ) { $this->shuffled->deleteOneMeta( $translated_post_data->element_id, $key_of_original_value ); $this->meta_to_update[ $translated_post_data->element_id ][] = array( $meta_key_after_move, $translated_meta_value, ); } } } /** * Checks if checkbox to synchronise is selected. * * @return bool */ private function synchronise_option_selected() { return isset( $_POST[ self::ACTION_SYNCHRONISE ] ) && wp_verify_nonce( $_POST[ self::ACTION_SYNCHRONISE ], self::ACTION_SYNCHRONISE ) && isset( $_POST['wpml_synchronise_acf_fields_translations'] ) && 'synchronise' === $_POST['wpml_synchronise_acf_fields_translations']; } /** * Checks if synchronise checkbox has been sent during the post save. * * @return bool */ private function synchroniseOptionSent() { return isset( $_POST[ self::ACTION_SYNCHRONISE ] ); } /** * Checks if post/taxonomy has repeater field associated with it. * * @param array $fields Fields belonging to the element (post or taxonomy). * * @return bool */ private function hasRepeaterField( $fields ) { foreach ( (array) $fields as $field ) { if ( isset( $field['type'] ) && 'repeater' === $field['type'] ) { return true; } } return false; } /** * Save repeater synchronisation option in wp_options table. * * @param int $elementID Processed element (post, taxonomy) ID. */ public function storeSynchroniseOption( $elementID ) { if ( $this->shuffled->hasTranslations( $elementID ) ) { $trid = $this->shuffled->getTrid( $elementID ); if ( $trid && $this->synchroniseOptionSent() ) { $synchroniseOption = get_option( self::SYNCHRONISE_WP_OPTION_NAME, [] ); if ( $this->synchronise_option_selected() ) { $synchroniseOption[ $trid ] = true; } else { $synchroniseOption[ $trid ] = false; } update_option( self::SYNCHRONISE_WP_OPTION_NAME, $synchroniseOption ); } } } /** * Get repeater synchronisation option from wp_options table. * * @param int $elementID Processed element (post, taxonomy) ID. * * @return bool */ private function isSynchroniseOptionChecked( $elementID ) { $trid = $this->shuffled->getTrid( $elementID ); if ( $trid ) { $synchroniseOption = get_option( self::SYNCHRONISE_WP_OPTION_NAME, [] ); if ( isset( $synchroniseOption[ $trid ] ) ) { return (bool) $synchroniseOption[ $trid ]; } } return true; } /** * If option to synchronise custom fields has been selected, replace repeater subfields * with values from version before meta data update. * * It runs when WPML calculates post md5 to compare with md5s of translations. * In case user shuffled field we don't want to mark post as needed to be translated. * * @param array $customFields * @param int $postId * * @return array */ public function revertFieldValuesForSignature( $customFields, $postId = 0 ) { if ( $this->synchronise_option_selected() && is_array( $customFields ) && ! empty( $customFields ) && is_numeric( $postId ) && $postId > 0 ) { foreach ( $customFields as $key => $value ) { if ( isset( $this->field_state->getStateBefore()[ $key ] ) && $this->isChildOfRepeaterField( $key, $postId ) && $this->get_keys_for_meta_value_changed( $key, $value ) ) { $customFields[ $key ] = $this->field_state->getStateBefore()[ $key ]; } } } return $customFields; } /** * Check if processed field is a child of Repeater field. * * @param string $key * @param int $postId * * @return bool */ private function isChildOfRepeaterField( $key, $postId ) { $acfFieldObject = get_field_object( $key, $postId ); if ( isset( $acfFieldObject['parent'] ) && $acfFieldObject['parent'] > 0 ) { $fieldParent = get_post( $acfFieldObject['parent'] ); $fieldParentContent = maybe_unserialize( $fieldParent->post_content ); if ( isset( $fieldParentContent['type'] ) && 'repeater' === $fieldParentContent['type'] ) { return true; } } return false; } }