name = 'clone'; $this->label = _x( 'Clone', 'noun', 'acf' ); $this->category = 'layout'; $this->defaults = array( 'clone' => '', 'prefix_label' => 0, 'prefix_name' => 0, 'display' => 'seamless', 'layout' => 'block', ); $this->cloning = array(); $this->have_rows = 'single'; // register filter acf_enable_filter( 'clone' ); // ajax add_action( 'wp_ajax_acf/fields/clone/query', array( $this, 'ajax_query' ) ); // filters add_filter( 'acf/get_fields', array( $this, 'acf_get_fields' ), 5, 2 ); add_filter( 'acf/prepare_field', array( $this, 'acf_prepare_field' ), 10, 1 ); add_filter( 'acf/clone_field', array( $this, 'acf_clone_field' ), 10, 2 ); } /* * is_enabled * * This function will return true if acf_local functionality is enabled * * @type function * @date 14/07/2016 * @since 5.4.0 * * @param n/a * @return n/a */ function is_enabled() { return acf_is_filter_enabled( 'clone' ); } /* * load_field() * * This filter is appied to the $field after it is loaded from the database * * @type filter * @since 3.6 * @date 23/01/13 * * @param $field - the field array holding all the field options * * @return $field - the field array holding all the field options */ function load_field( $field ) { // bail early if not enabled if ( ! $this->is_enabled() ) { return $field; } // load sub fields // - sub field name's will be modified to include prefix_name settings $field['sub_fields'] = $this->get_cloned_fields( $field ); // return return $field; } /* * acf_get_fields * * This function will hook into the 'acf/get_fields' filter and inject/replace seamless clones fields * * @type function * @date 17/06/2016 * @since 5.3.8 * * @param $fields (array) * @param $parent (array) * @return $fields */ function acf_get_fields( $fields, $parent ) { // bail early if empty if ( empty( $fields ) ) { return $fields; } // bail early if not enabled if ( ! $this->is_enabled() ) { return $fields; } // vars $i = 0; // loop while ( $i < count( $fields ) ) { // vars $field = $fields[ $i ]; // $i $i++; // bail early if not a clone field if ( $field['type'] != 'clone' ) { continue; } // bail early if not seamless if ( $field['display'] != 'seamless' ) { continue; } // bail early if sub_fields isn't set or not an array if ( ! isset( $field['sub_fields'] ) || ! is_array( $field['sub_fields'] ) ) { continue; } // replace this clone field with sub fields $i--; array_splice( $fields, $i, 1, $field['sub_fields'] ); } // return return $fields; } /* * get_cloned_fields * * This function will return an array of fields for a given clone field * * @type function * @date 28/06/2016 * @since 5.3.8 * * @param $field (array) * @param $parent (array) * @return (array) */ function get_cloned_fields( $field ) { // vars $fields = array(); // bail early if no clone setting if ( empty( $field['clone'] ) ) { return $fields; } // bail early if already cloning this field (avoid infinite looping) if ( isset( $this->cloning[ $field['key'] ] ) ) { return $fields; } // update local ref $this->cloning[ $field['key'] ] = 1; // Loop over selectors and load fields. foreach ( $field['clone'] as $selector ) { // Field Group selector. if ( acf_is_field_group_key( $selector ) ) { $field_group = acf_get_field_group( $selector ); if ( ! $field_group ) { continue; } $field_group_fields = acf_get_fields( $field_group ); if ( ! $field_group_fields ) { continue; } $fields = array_merge( $fields, $field_group_fields ); // Field selector. } elseif ( acf_is_field_key( $selector ) ) { $fields[] = acf_get_field( $selector ); } } // field has ve been loaded for this $parent, time to remove cloning ref unset( $this->cloning[ $field['key'] ] ); // clear false values (fields that don't exist) $fields = array_filter( $fields ); // bail early if no sub fields if ( empty( $fields ) ) { return array(); } // loop // run acf_clone_field() on each cloned field to modify name, key, etc foreach ( array_keys( $fields ) as $i ) { $fields[ $i ] = acf_clone_field( $fields[ $i ], $field ); } // return return $fields; } /* * acf_clone_field * * This function is run when cloning a clone field * Important to run the acf_clone_field function on sub fields to pass on settings such as 'parent_layout' * * @type function * @date 28/06/2016 * @since 5.3.8 * * @param $field (array) * @param $clone_field (array) * @return $field */ function acf_clone_field( $field, $clone_field ) { // bail early if this field is being cloned by some other kind of field (future proof) if ( $clone_field['type'] != 'clone' ) { return $field; } // backup (used later) // - backup only once (cloned clone fields can cause issues) if ( ! isset( $field['__key'] ) ) { $field['__key'] = $field['key']; $field['__name'] = $field['_name']; $field['__label'] = $field['label']; } // seamless if ( $clone_field['display'] == 'seamless' ) { // modify key // - this will allow sub clone fields to correctly load values for the same cloned field // - the original key will later be restored by acf/prepare_field allowing conditional logic JS to work $field['key'] = $clone_field['key'] . '_' . $field['key']; // modify prefix allowing clone field to save sub fields // - only used for parent seamless fields. Block or sub field's prefix will be overriden which also works $field['prefix'] = $clone_field['prefix'] . '[' . $clone_field['key'] . ']'; // modify parent $field['parent'] = $clone_field['parent']; // label_format if ( $clone_field['prefix_label'] ) { $field['label'] = $clone_field['label'] . ' ' . $field['label']; } } // prefix_name if ( $clone_field['prefix_name'] ) { // modify the field name // - this will allow field to load / save correctly $field['name'] = $clone_field['name'] . '_' . $field['_name']; // modify the field _name (orig name) // - this will allow fields to correctly understand the modified field if ( $clone_field['display'] == 'seamless' ) { $field['_name'] = $clone_field['_name'] . '_' . $field['_name']; } } // required if ( $clone_field['required'] ) { $field['required'] = 1; } // type specific // note: seamless clone fields will not be triggered if ( $field['type'] == 'clone' ) { $field = $this->acf_clone_clone_field( $field, $clone_field ); } // return return $field; } /* * acf_clone_clone_field * * This function is run when cloning a clone field * Important to run the acf_clone_field function on sub fields to pass on settings such as 'parent_layout' * Do not delete! Removing this logic causes major issues with cloned clone fields within a flexible content layout. * * @type function * @date 28/06/2016 * @since 5.3.8 * * @param $field (array) * @param $clone_field (array) * @return $field */ function acf_clone_clone_field( $field, $clone_field ) { // modify the $clone_field name // This seems odd, however, the $clone_field is later passed into the acf_clone_field() function // Do not delete! // when cloning a clone field, it is important to also change the _name too // this allows sub clone fields to appear correctly in get_row() row array if ( $field['prefix_name'] ) { $clone_field['name'] = $field['_name']; $clone_field['_name'] = $field['_name']; } // bail early if no sub fields if ( empty( $field['sub_fields'] ) ) { return $field; } // loop foreach ( $field['sub_fields'] as &$sub_field ) { // clone $sub_field = acf_clone_field( $sub_field, $clone_field ); } // return return $field; } /* * prepare_field_for_db * * description * * @type function * @date 4/11/16 * @since 5.5.0 * * @param $post_id (int) * @return $post_id (int) */ function prepare_field_for_db( $field ) { // bail early if no sub fields if ( empty( $field['sub_fields'] ) ) { return $field; } // bail early if name == _name // this is a parent clone field and does not require any modification to sub field names if ( $field['name'] == $field['_name'] ) { return $field; } // this is a sub field // _name = 'my_field' // name = 'rep_0_my_field' // modify all sub fields to add 'rep_0_' name prefix (prefix_name setting has already been applied) $length = strlen( $field['_name'] ); $prefix = substr( $field['name'], 0, -$length ); // bail early if _name is not found at the end of name (unknown potential error) if ( $prefix . $field['_name'] !== $field['name'] ) { return $field; } // acf_log('== prepare_field_for_db =='); // acf_log('- clone name:', $field['name']); // acf_log('- clone _name:', $field['_name']); // loop foreach ( $field['sub_fields'] as &$sub_field ) { $sub_field['name'] = $prefix . $sub_field['name']; } // return return $field; } /* * load_value() * * This filter is applied to the $value after it is loaded from the db * * @type filter * @since 3.6 * @date 23/01/13 * * @param $value (mixed) the value found in the database * @param $post_id (mixed) the $post_id from which the value was loaded * @param $field (array) the field array holding all the field options * @return $value */ function load_value( $value, $post_id, $field ) { // bail early if no sub fields if ( empty( $field['sub_fields'] ) ) { return $value; } // modify names $field = $this->prepare_field_for_db( $field ); // load sub fields $value = array(); // loop foreach ( $field['sub_fields'] as $sub_field ) { // add value $value[ $sub_field['key'] ] = acf_get_value( $post_id, $sub_field ); } // return return $value; } /* * format_value() * * This filter is appied to the $value after it is loaded from the db and before it is returned to the template * * @type filter * @since 3.6 * @date 23/01/13 * * @param $value (mixed) the value which was loaded from the database * @param $post_id (mixed) the $post_id from which the value was loaded * @param $field (array) the field array holding all the field options * * @return $value (mixed) the modified value */ function format_value( $value, $post_id, $field ) { // bail early if no value if ( empty( $value ) ) { return false; } // modify names $field = $this->prepare_field_for_db( $field ); // loop foreach ( $field['sub_fields'] as $sub_field ) { // extract value $sub_value = acf_extract_var( $value, $sub_field['key'] ); // format value $sub_value = acf_format_value( $sub_value, $post_id, $sub_field ); // append to $row $value[ $sub_field['__name'] ] = $sub_value; } // return return $value; } /** * Apply basic formatting to prepare the value for default REST output. * * @param mixed $value * @param string|int $post_id * @param array $field * @return mixed */ public function format_value_for_rest( $value, $post_id, array $field ) { if ( empty( $value ) || ! is_array( $value ) ) { return $value; } if ( ! is_array( $field ) || ! isset( $field['sub_fields'] ) || ! is_array( $field['sub_fields'] ) ) { return $value; } // Loop through each row and within that, each sub field to process sub fields individually. foreach ( $field['sub_fields'] as $sub_field ) { // Extract the sub field 'field_key'=>'value' pair from the $value and format it. $sub_value = acf_extract_var( $value, $sub_field['key'] ); $sub_value = acf_format_value_for_rest( $sub_value, $post_id, $sub_field ); // Add the sub field value back to the $value but mapped to the field name instead // of the key reference. $value[ $sub_field['name'] ] = $sub_value; } return $value; } /* * update_value() * * This filter is appied to the $value before it is updated in the db * * @type filter * @since 3.6 * @date 23/01/13 * * @param $value - the value which will be saved in the database * @param $field - the field array holding all the field options * @param $post_id - the $post_id of which the value will be saved * * @return $value - the modified value */ function update_value( $value, $post_id, $field ) { // bail early if no value if ( ! acf_is_array( $value ) ) { return null; } // bail early if no sub fields if ( empty( $field['sub_fields'] ) ) { return null; } // modify names $field = $this->prepare_field_for_db( $field ); // loop foreach ( $field['sub_fields'] as $sub_field ) { // vars $v = false; // key (backend) if ( isset( $value[ $sub_field['key'] ] ) ) { $v = $value[ $sub_field['key'] ]; // name (frontend) } elseif ( isset( $value[ $sub_field['_name'] ] ) ) { $v = $value[ $sub_field['_name'] ]; // empty } else { // input is not set (hidden by conditioanl logic) continue; } // restore original field key $sub_field = $this->acf_prepare_field( $sub_field ); // update value acf_update_value( $v, $post_id, $sub_field ); } // return return ''; } /* * render_field() * * Create the HTML interface for your field * * @param $field - an array holding all the field's data * * @type action * @since 3.6 * @date 23/01/13 */ function render_field( $field ) { // bail early if no sub fields if ( empty( $field['sub_fields'] ) ) { return; } // load values foreach ( $field['sub_fields'] as &$sub_field ) { // add value if ( isset( $field['value'][ $sub_field['key'] ] ) ) { // this is a normal value $sub_field['value'] = $field['value'][ $sub_field['key'] ]; } elseif ( isset( $sub_field['default_value'] ) ) { // no value, but this sub field has a default value $sub_field['value'] = $sub_field['default_value']; } // update prefix to allow for nested values $sub_field['prefix'] = $field['name']; // restore label $sub_field['label'] = $sub_field['__label']; // restore required if ( $field['required'] ) { $sub_field['required'] = 0; } } // render if ( $field['layout'] == 'table' ) { $this->render_field_table( $field ); } else { $this->render_field_block( $field ); } } /* * render_field_block * * description * * @type function * @date 12/07/2016 * @since 5.4.0 * * @param $post_id (int) * @return $post_id (int) */ function render_field_block( $field ) { // vars $label_placement = $field['layout'] == 'block' ? 'top' : 'left'; // html echo '
'; foreach ( $field['sub_fields'] as $sub_field ) { acf_render_field_wrap( $sub_field ); } echo '
'; } /* * render_field_table * * description * * @type function * @date 12/07/2016 * @since 5.4.0 * * @param $post_id (int) * @return $post_id (int) */ function render_field_table( $field ) { ?>
>
__( 'Fields', 'acf' ), 'instructions' => __( 'Select one or more fields you wish to clone', 'acf' ), 'type' => 'select', 'name' => 'clone', 'multiple' => 1, 'allow_null' => 1, 'choices' => $this->get_clone_setting_choices( $field['clone'] ), 'ui' => 1, 'ajax' => 1, 'ajax_action' => 'acf/fields/clone/query', 'placeholder' => '', ) ); acf_disable_filter( 'local' ); // display acf_render_field_setting( $field, array( 'label' => __( 'Display', 'acf' ), 'instructions' => __( 'Specify the style used to render the clone field', 'acf' ), 'type' => 'select', 'name' => 'display', 'class' => 'setting-display', 'choices' => array( 'group' => __( 'Group (displays selected fields in a group within this field)', 'acf' ), 'seamless' => __( 'Seamless (replaces this field with selected fields)', 'acf' ), ), ) ); // layout acf_render_field_setting( $field, array( 'label' => __( 'Layout', 'acf' ), 'instructions' => __( 'Specify the style used to render the selected fields', 'acf' ), 'type' => 'radio', 'name' => 'layout', 'layout' => 'horizontal', 'choices' => array( 'block' => __( 'Block', 'acf' ), 'table' => __( 'Table', 'acf' ), 'row' => __( 'Row', 'acf' ), ), ) ); // prefix_label $instructions = __( 'Labels will be displayed as %s', 'acf' ); $instructions = sprintf( $instructions, '' ); acf_render_field_setting( $field, array( 'label' => __( 'Prefix Field Labels', 'acf' ), 'instructions' => $instructions, 'name' => 'prefix_label', 'class' => 'setting-prefix-label', 'type' => 'true_false', 'ui' => 1, ) ); // prefix_name $instructions = __( 'Values will be saved as %s', 'acf' ); $instructions = sprintf( $instructions, '' ); acf_render_field_setting( $field, array( 'label' => __( 'Prefix Field Names', 'acf' ), 'instructions' => $instructions, 'name' => 'prefix_name', 'class' => 'setting-prefix-name', 'type' => 'true_false', 'ui' => 1, ) ); } /* * get_clone_setting_choices * * This function will return an array of choices data for Select2 * * @type function * @date 17/06/2016 * @since 5.3.8 * * @param $value (mixed) * @return (array) */ function get_clone_setting_choices( $value ) { // vars $choices = array(); // bail early if no $value if ( empty( $value ) ) { return $choices; } // force value to array $value = acf_get_array( $value ); // loop foreach ( $value as $v ) { $choices[ $v ] = $this->get_clone_setting_choice( $v ); } // return return $choices; } /* * get_clone_setting_choice * * This function will return the label for a given clone choice * * @type function * @date 17/06/2016 * @since 5.3.8 * * @param $selector (mixed) * @return (string) */ function get_clone_setting_choice( $selector = '' ) { // bail early no selector if ( ! $selector ) { return ''; } // phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified elsewhere. // ajax_fields if ( isset( $_POST['fields'][ $selector ] ) ) { return $this->get_clone_setting_field_choice( acf_sanitize_request_args( $_POST['fields'][ $selector ] ) ); } // phpcs:enable WordPress.Security.NonceVerification.Missing // field if ( acf_is_field_key( $selector ) ) { return $this->get_clone_setting_field_choice( acf_get_field( $selector ) ); } // group if ( acf_is_field_group_key( $selector ) ) { return $this->get_clone_setting_group_choice( acf_get_field_group( $selector ) ); } // return return $selector; } /* * get_clone_setting_field_choice * * This function will return the text for a field choice * * @type function * @date 20/07/2016 * @since 5.4.0 * * @param $field (array) * @return (string) */ function get_clone_setting_field_choice( $field ) { // bail early if no field if ( ! $field ) { return __( 'Unknown field', 'acf' ); } // title $title = $field['label'] ? $field['label'] : __( '(no title)', 'acf' ); // append type $title .= ' (' . $field['type'] . ')'; // ancestors // - allow for AJAX to send through ancestors count $ancestors = isset( $field['ancestors'] ) ? $field['ancestors'] : count( acf_get_field_ancestors( $field ) ); $title = str_repeat( '- ', $ancestors ) . $title; // return return $title; } /* * get_clone_setting_group_choice * * This function will return the text for a group choice * * @type function * @date 20/07/2016 * @since 5.4.0 * * @param $field_group (array) * @return (string) */ function get_clone_setting_group_choice( $field_group ) { // bail early if no field group if ( ! $field_group ) { return __( 'Unknown field group', 'acf' ); } // return return sprintf( __( 'All fields from %s field group', 'acf' ), $field_group['title'] ); } /* * ajax_query * * description * * @type function * @date 17/06/2016 * @since 5.3.8 * * @param $post_id (int) * @return $post_id (int) */ function ajax_query() { // validate if ( ! acf_verify_ajax() ) { die(); } // disable field to allow clone fields to appear selectable acf_disable_filter( 'clone' ); // options $options = acf_parse_args( $_POST, array( 'post_id' => 0, 'paged' => 0, 's' => '', 'title' => '', 'fields' => array(), ) ); // vars $results = array(); $s = false; $i = -1; $limit = 20; $range_start = $limit * ( $options['paged'] - 1 ); // 0, 20, 40 $range_end = $range_start + ( $limit - 1 ); // 19, 39, 59 // search if ( $options['s'] !== '' ) { // strip slashes (search may be integer) $s = wp_unslash( strval( $options['s'] ) ); } // load groups $field_groups = acf_get_field_groups(); $field_group = false; // bail early if no field groups if ( empty( $field_groups ) ) { die(); } // move current field group to start foreach ( array_keys( $field_groups ) as $j ) { // check ID if ( $field_groups[ $j ]['ID'] !== $options['post_id'] ) { continue; } // extract field group and move to start $field_group = acf_extract_var( $field_groups, $j ); // field group found, stop looking break; } // if field group was not found, this is a new field group (not yet saved) if ( ! $field_group ) { $field_group = array( 'ID' => $options['post_id'], 'title' => $options['title'], 'key' => '', ); } // move current field group to start of list array_unshift( $field_groups, $field_group ); // loop foreach ( $field_groups as $field_group ) { // vars $fields = false; $ignore_s = false; $data = array( 'text' => $field_group['title'], 'children' => array(), ); // get fields if ( $field_group['ID'] == $options['post_id'] ) { $fields = $options['fields']; } else { $fields = acf_get_fields( $field_group ); $fields = acf_prepare_fields_for_import( $fields ); } // bail early if no fields if ( ! $fields ) { continue; } // show all children for field group search match if ( $s !== false && stripos( $data['text'], $s ) !== false ) { $ignore_s = true; } // populate children $children = array(); $children[] = $field_group['key']; foreach ( $fields as $field ) { $children[] = $field['key']; } // loop foreach ( $children as $child ) { // bail early if no key (fake field group or corrupt field) if ( ! $child ) { continue; } // vars $text = false; // bail early if is search, and $text does not contain $s if ( $s !== false && ! $ignore_s ) { // get early $text = $this->get_clone_setting_choice( $child ); // search if ( stripos( $text, $s ) === false ) { continue; } } // $i $i++; // bail early if $i is out of bounds if ( $i < $range_start || $i > $range_end ) { continue; } // load text if ( $text === false ) { $text = $this->get_clone_setting_choice( $child ); } // append $data['children'][] = array( 'id' => $child, 'text' => $text, ); } // bail early if no children // - this group contained fields, but none shown on this page if ( empty( $data['children'] ) ) { continue; } // append $results[] = $data; // end loop if $i is out of bounds // - no need to look further if ( $i > $range_end ) { break; } } // return acf_send_ajax_results( array( 'results' => $results, 'limit' => $limit, ) ); } /* * acf_prepare_field * * This function will restore a field's key ready for input * * @type function * @date 6/09/2016 * @since 5.4.0 * * @param $field (array) * @return $field */ function acf_prepare_field( $field ) { // bail early if not cloned if ( empty( $field['_clone'] ) ) { return $field; } // restore key if ( isset( $field['__key'] ) ) { $field['key'] = $field['__key']; } // return return $field; } /* * validate_value * * description * * @type function * @date 11/02/2014 * @since 5.0.0 * * @param $post_id (int) * @return $post_id (int) */ function validate_value( $valid, $value, $field, $input ) { // bail early if no $value if ( empty( $value ) ) { return $valid; } // bail early if no sub fields if ( empty( $field['sub_fields'] ) ) { return $valid; } // loop foreach ( array_keys( $field['sub_fields'] ) as $i ) { // get sub field $sub_field = $field['sub_fields'][ $i ]; $k = $sub_field['key']; // bail early if valu enot set (conditional logic?) if ( ! isset( $value[ $k ] ) ) { continue; } // validate acf_validate_value( $value[ $k ], $sub_field, "{$input}[{$k}]" ); } // return return $valid; } /** * Return the schema array for the REST API. * * @param array $field * @return array */ public function get_rest_schema( array $field ) { $schema = array( 'type' => array( 'object', 'null' ), 'required' => ! empty( $field['required'] ) ? array() : false, 'items' => array( 'type' => 'object', 'properties' => array(), ), ); foreach ( $field['sub_fields'] as $sub_field ) { /** @var acf_field $type */ $type = acf_get_field_type( $sub_field['type'] ); if ( ! $type ) { continue; } $sub_field_schema = $type->get_rest_schema( $sub_field ); // Passing null to nested fields has no effect. Remove this as a possible type to prevent // confusion in the schema. $null_type_index = array_search( 'null', $sub_field_schema['type'] ); if ( $null_type_index !== false ) { unset( $sub_field_schema['type'][ $null_type_index ] ); } $schema['items']['properties'][ $sub_field['name'] ] = $sub_field_schema; /** * If the clone field itself is marked as required, all subfields are required, * regardless of the status of the original fields. */ if ( is_array( $schema['required'] ) ) { $schema['required'][] = $sub_field['name']; } } return $schema; } } // initialize acf_register_field_type( 'acf_field_clone' ); endif; // class_exists check ?>