public = false; parent::__construct(); } /* * __construct * * This function will setup the field type data * * @type function * @date 5/03/2014 * @since 5.0.0 * * @param n/a * @return n/a */ function initialize() { // vars $this->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 ealry 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 ealry 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 ealry 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 '
| > |
|---|
');
acf_render_field_setting(
$field,
array(
'label' => __('Prefix Field Labels', 'acf'),
'message' => $instructions,
// 'instructions_placement' => 'field',
'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'),
'message' => $instructions,
// 'instructions_placement' => 'field',
'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 '';
}
// ajax_fields
if (isset($_POST['fields'][$selector])) {
return $this->get_clone_setting_field_choice($_POST['fields'][$selector]);
}
// 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 ealry 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 ealry 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
?>