diff --git includes/form.inc includes/form.inc index a13a43c..b492bc8 100644 --- includes/form.inc +++ includes/form.inc @@ -163,7 +163,7 @@ * Any additional arguments are passed on to the functions called by * drupal_get_form(), including the unique form constructor function. For * example, the node_edit form requires that a node object is passed in here - * when it is called. These are available to implementations of + * when it is called. These are available to implementations of * hook_form_alter() and hook_form_FORM_ID_alter() as the array * $form_state['build_info']['args']. * @@ -1428,7 +1428,8 @@ function form_set_error($name = NULL, $message = '', $limit_validation_errors = // section, then the element's values are within the part of // $form_state['values'] that the clicked button requires to be valid, // so errors for this element must be recorded. - if (array_slice(explode('][', $name), 0, count($section)) === $section) { + $section_string = implode('][', $section); + if (strpos($name, $section_string) === 0) { $record = TRUE; break; } diff --git modules/field/field.api.php modules/field/field.api.php index a701923..f8f82b9 100644 --- modules/field/field.api.php +++ modules/field/field.api.php @@ -764,7 +764,7 @@ function hook_field_widget_info_alter(&$info) { * definitions returned by field_info_field() and field_info_instance(). * Examples: mono-value widget even if the field is multi-valued, non-required * widget even if the field is 'required'... - + * * Therefore, the FAPI element callbacks (such as #process, #element_validate, * #value_callback...) used by the widget cannot use the field_info_field() * or field_info_instance() functions to retrieve the $field or $instance diff --git modules/field/field.attach.inc modules/field/field.attach.inc index 5a496e7..eea140b 100644 --- modules/field/field.attach.inc +++ modules/field/field.attach.inc @@ -456,11 +456,11 @@ function _field_invoke_get_instances($entity_type, $bundle, $options) { * // to access the field sub-array when $langcode is unknown. * 'field_foo' => array( * '#tree' => TRUE, + * '#parents' => @todo, * '#field_name' => the name of the field, * '#language' => $langcode, * $langcode => array( * '#field_name' => the name of the field, - * '#tree' => TRUE, * '#required' => whether or not the field is required, * '#title' => the label of the field instance, * '#description' => the description text for the field instance, @@ -497,7 +497,7 @@ function _field_invoke_get_instances($entity_type, $bundle, $options) { * ) * @endcode * - * Sample structure for $form_state['field']: + * @todo Sample structure for $form_state['field']: * @code * array( * // One sub-array per field appearing in the form, keyed by field name. @@ -536,7 +536,9 @@ function _field_invoke_get_instances($entity_type, $bundle, $options) { * parameter. Processing information is added by reference in * $form_state['field']. */ -function field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode = NULL) { +function field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode = NULL, $parents = array()) { + field_form_set_parents($parents, $form_state); + // If no language is provided use the default site language. $options = array('language' => field_valid_language($langcode)); $form += (array) _field_invoke_default('form', $entity_type, $entity, $form, $form_state, $options); @@ -553,6 +555,8 @@ function field_attach_form($entity_type, $entity, &$form, &$form_state, $langcod $function = $module . '_field_attach_form'; $function($entity_type, $entity, $form, $form_state, $langcode); } + + field_form_unset_parents($form_state); } /** @@ -786,7 +790,9 @@ function field_attach_validate($entity_type, $entity) { * @param $form_state * An associative array containing the current state of the form. */ -function field_attach_form_validate($entity_type, $entity, $form, &$form_state) { +function field_attach_form_validate($entity_type, $entity, $form, &$form_state, $parents = array()) { + field_form_set_parents($parents, $form_state); + // Extract field values from submitted values. _field_invoke_default('extract_form_values', $entity_type, $entity, $form, $form_state); @@ -798,12 +804,16 @@ function field_attach_form_validate($entity_type, $entity, $form, &$form_state) // Pass field-level validation errors back to widgets for accurate error // flagging. foreach ($e->errors as $field_name => $field_errors) { - foreach ($field_errors as $langcode => $language_errors) { - $form_state['field'][$field_name][$langcode]['errors'] = $language_errors; + foreach ($field_errors as $langcode => $errors) { + $field_state = field_form_get_state(NULL, $field_name, $langcode, $form_state); + $field_state['errors'] = $errors; + field_form_set_state(NULL, $field_name, $langcode, $form_state, $field_state); } } _field_invoke_default('form_errors', $entity_type, $entity, $form, $form_state); } + + field_form_unset_parents($form_state); } /** @@ -823,7 +833,9 @@ function field_attach_form_validate($entity_type, $entity, $form, &$form_state) * @param $form_state * An associative array containing the current state of the form. */ -function field_attach_submit($entity_type, $entity, $form, &$form_state) { +function field_attach_submit($entity_type, $entity, $form, &$form_state, $parents = array()) { + field_form_set_parents($parents, $form_state); + // Extract field values from submitted values. _field_invoke_default('extract_form_values', $entity_type, $entity, $form, $form_state); @@ -835,6 +847,8 @@ function field_attach_submit($entity_type, $entity, $form, &$form_state) { $function = $module . '_field_attach_submit'; $function($entity_type, $entity, $form, $form_state); } + + field_form_unset_parents($form_state); } /** diff --git modules/field/field.default.inc modules/field/field.default.inc index 1353bd6..da46a8d 100644 --- modules/field/field.default.inc +++ modules/field/field.default.inc @@ -12,12 +12,15 @@ */ function field_default_extract_form_values($entity_type, $entity, $field, $instance, $langcode, &$items, $form, &$form_state) { - $field_name = $field['field_name']; + $parents = field_form_get_parents($form_state); - if (isset($form_state['values'][$field_name][$langcode])) { - $items = $form_state['values'][$field_name][$langcode]; + $path = array_merge($parents, array($field['field_name'], $langcode)); + $path_exists = FALSE; + $values = drupal_array_get_nested_value($form_state['values'], $path, $path_exists); + if ($path_exists) { // Remove the 'value' of the 'add more' button. - unset($items['add_more']); + unset($values['add_more']); + $items = $values; } } diff --git modules/field/field.form.inc modules/field/field.form.inc index c3ac156..6808c9d 100644 --- modules/field/field.form.inc +++ modules/field/field.form.inc @@ -16,6 +16,8 @@ function field_default_form($entity_type, $entity, $field, $instance, $langcode, list($id, , ) = entity_extract_ids($entity_type, $entity); } + $parents = field_form_get_parents($form_state); + $addition = array(); $field_name = $field['field_name']; $addition[$field_name] = array(); @@ -37,6 +39,20 @@ function field_default_form($entity_type, $entity, $field, $instance, $langcode, // Collect widget elements. $elements = array(); if (field_access('edit', $field, $entity_type, $entity)) { + // Store field information in $form_state. + if (!field_form_get_state(NULL, $field_name, $langcode, $form_state)) { + $field_state = array( + 'field' => $field, + 'instance' => $instance, + 'items_count' => count($items), + // This entry will be populated at form build time. + 'array_parents' => array(), + // This entry will be populated at form validation time. + 'errors' => array(), + ); + field_form_set_state(NULL, $field_name, $langcode, $form_state, $field_state); + } + // If field module handles multiple values for this form element, and we // are displaying an individual element, process the multiple value form. if (!isset($get_delta) && field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) { @@ -54,6 +70,8 @@ function field_default_form($entity_type, $entity, $field, $instance, $langcode, '#bundle' => $instance['bundle'], '#field_name' => $field_name, '#language' => $langcode, + // Provide $parents so that widgets can navigate in $form_state['field']. + '#field_parents' => $parents, '#columns' => array_keys($field['columns']), '#title' => check_plain(t($instance['label'])), '#description' => field_filter_xss($instance['description']), @@ -79,16 +97,6 @@ function field_default_form($entity_type, $entity, $field, $instance, $langcode, } if ($elements) { - // Store field information in $form_state. - $form_state['field'][$field_name][$langcode] = array( - 'field' => $field, - 'instance' => $instance, - // This entry will be populated at form build time. - 'array_parents' => array(), - // This entry will be populated at form validation time. - 'errors' => array(), - ); - // Also aid in theming of field widgets by rendering a classified // container. $addition[$field_name] = array( @@ -110,8 +118,10 @@ function field_default_form($entity_type, $entity, $field, $instance, $langcode, $elements['#after_build'][] = 'field_form_element_after_build'; $elements['#field_name'] = $field_name; $elements['#language'] = $langcode; + $elements['#field_parents'] = $parents; $addition[$field_name] += array( + '#parents' => array_merge($parents, array($field_name)), '#tree' => TRUE, // The '#language' key can be used to access the field's form element // when $langcode is unknown. @@ -136,16 +146,10 @@ function field_multiple_value_form($field, $instance, $langcode, $items, &$form, // Determine the number of widgets to display. switch ($field['cardinality']) { case FIELD_CARDINALITY_UNLIMITED: - $filled_items = _field_filter_items($field, $items); - $current_item_count = isset($form_state['field_item_count'][$field_name]) - ? $form_state['field_item_count'][$field_name] - : count($items); - // We always want at least one empty icon for the user to fill in. - $max = ($current_item_count > count($filled_items)) - ? $current_item_count - 1 - : $current_item_count; - + $field_state = field_form_get_state(NULL, $field_name, $langcode, $form_state); + $max = $field_state['items_count']; break; + default: $max = $field['cardinality'] - 1; break; @@ -153,7 +157,11 @@ function field_multiple_value_form($field, $instance, $langcode, $items, &$form, $title = check_plain(t($instance['label'])); $description = field_filter_xss(t($instance['description'])); - $wrapper_id = drupal_html_id($field_name . '-add-more-wrapper'); + + $parents = field_form_get_parents($form_state); + $wrapper_id_prefix = implode('-', $parents); + $wrapper_id = drupal_html_id($wrapper_id_prefix . '-' . $field_name . '-add-more-wrapper'); + $field_elements = array(); $function = $instance['widget']['module'] . '_field_widget_form'; @@ -165,6 +173,8 @@ function field_multiple_value_form($field, $instance, $langcode, $items, &$form, '#bundle' => $instance['bundle'], '#field_name' => $field_name, '#language' => $langcode, + // Provide $parents so that widgets can navigate in $form_state['field']. + '#field_parents' => $parents, '#columns' => array_keys($field['columns']), // For multiple fields, title and description are handled by the wrapping table. '#title' => $multiple ? '' : $title, @@ -209,10 +219,10 @@ function field_multiple_value_form($field, $instance, $langcode, $items, &$form, if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED && empty($form_state['programmed'])) { $field_elements['add_more'] = array( '#type' => 'submit', - '#name' => $field_name . '_add_more', + '#name' => strtr($wrapper_id_prefix, '-', '_') . '_' . $field_name . '_add_more', '#value' => t('Add another item'), '#attributes' => array('class' => array('field-add-more-submit')), - '#limit_validation_errors' => array(array($field_name, $langcode)), + '#limit_validation_errors' => array(array_merge($parents, array($field_name, $langcode))), '#submit' => array('field_add_more_submit'), '#ajax' => array( 'callback' => 'field_add_more_js', @@ -223,6 +233,7 @@ function field_multiple_value_form($field, $instance, $langcode, $items, &$form, // find the relevant field using those entries. '#field_name' => $field_name, '#language' => $langcode, + '#field_parents' => $parents, ); } } @@ -319,7 +330,11 @@ function theme_field_multiple_value_form($variables) { function field_form_element_after_build($element, &$form_state) { $field_name = $element['#field_name']; $langcode = $element['#language']; - $form_state['field'][$field_name][$langcode]['array_parents'] = $element['#array_parents']; + $parents = $element['#field_parents']; + + $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state); + $field_state['array_parents'] = $element['#array_parents']; + field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state); return $element; } @@ -328,21 +343,17 @@ function field_form_element_after_build($element, &$form_state) { * Transfer field-level validation errors to widgets. */ function field_default_form_errors($entity_type, $entity, $field, $instance, $langcode, $items, $form, &$form_state) { - $field_name = $field['field_name']; - $field_info = $form_state['field'][$field_name][$langcode]; + $field_state = field_form_get_state(NULL, $field['field_name'], $langcode, $form_state); - if (!empty($field_info['errors'])) { + if (!empty($field_state['errors'])) { $function = $instance['widget']['module'] . '_field_widget_error'; $function_exists = function_exists($function); - // Walk the form down to where the widget lives. - $element = $form; - foreach ($field_info['array_parents'] as $key) { - $element = $element[$key]; - } + // Locate the widget in the form. + $element = drupal_array_get_nested_value($form_state['complete form'], $field_state['array_parents']); $multiple_widget = field_behaviors_widget('multiple values', $instance) != FIELD_BEHAVIOR_DEFAULT; - foreach ($field_info['errors'] as $delta => $delta_errors) { + foreach ($field_state['errors'] as $delta => $delta_errors) { // For multiple single-value widgets, pass errors by delta. // For a multiple-value widget, all errors are passed to the main widget. $error_element = $multiple_widget ? $element : $element[$delta]; @@ -353,10 +364,13 @@ function field_default_form_errors($entity_type, $entity, $field, $instance, $la else { // Make sure that errors are reported (even incorrectly flagged) if // the widget module fails to implement hook_field_widget_error(). - form_error($error_element, $error['error'], $form, $form_state); + form_error($error_element, $error['error']); } } } + // Reinitialize the errors list for the next submit. + $field_state['errors'] = array(); + field_form_set_state(NULL, $field['field_name'], $langcode, $form_state, $field_state); } } @@ -372,9 +386,12 @@ function field_default_form_errors($entity_type, $entity, $field, $instance, $la function field_add_more_submit($form, &$form_state) { $field_name = $form_state['clicked_button']['#field_name']; $langcode = $form_state['clicked_button']['#language']; - if ($form_state['values'][$field_name . '_add_more']) { - $form_state['field_item_count'][$field_name] = count($form_state['values'][$field_name][$langcode]); - } + $parents = $form_state['clicked_button']['#field_parents']; + + $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state); + $field_state['items_count']++; + field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state); + $form_state['rebuild'] = TRUE; } @@ -390,21 +407,19 @@ function field_add_more_js($form, $form_state) { // Retrieve field information. $field_name = $form_state['clicked_button']['#field_name']; $langcode = $form_state['clicked_button']['#language']; - $field_info = $form_state['field'][$field_name][$langcode]; - $field = $field_info['field']; + $parents = $form_state['clicked_button']['#field_parents']; + $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state); + + $field = $field_state['field']; if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED) { return; } - // Navigate to the right element in the the form. - $element = $form; - - foreach ($field_info['array_parents'] as $key) { - $element = $element[$key]; - } + // Locate the widget element. + $element = drupal_array_get_nested_value($form, $field_state['array_parents']); - // Add a DIV around the new field to receive the AJAX effect. + // Add a DIV around the delta receiving the AJAX effect. $delta = $element['#max_delta']; $element[$delta]['#prefix'] = '
' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : ''); $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '
'; @@ -412,6 +427,57 @@ function field_add_more_js($form, $form_state) { return $element; } +function field_form_set_parents($parents, &$form_state) { + if (!isset($form_state['field']['parents_stack'])) { + $form_state['field']['parents_stack'] = array(); + } + array_push($form_state['field']['parents_stack'], $parents); +} + +function field_form_unset_parents(&$form_state) { + array_pop($form_state['field']['parents_stack']); +} + +function field_form_get_parents(&$form_state) { + return end($form_state['field']['parents_stack']); +} + +function field_form_set_state($parents, $field_name, $language, &$form_state, $field_state) { + $path = _field_form_state_path($parents, $field_name, $language, $form_state); + drupal_array_set_nested_value($form_state['field'], $path, $field_state); +} + +function field_form_get_state($parents, $field_name, $language, &$form_state) { + $path = _field_form_state_path($parents, $field_name, $language, $form_state); + return drupal_array_get_nested_value($form_state['field'], $path); +} + +function _field_form_state_path($parents, $field_name, $language, $form_state) { + // If no explicit $parents, use the current parents. + if ($parents === NULL) { + $parents = field_form_get_parents($form_state); + } + + // To ensure backwards compatibility on regular entity fotms for widgets that + // still access $form_state['field'][$field_name] directly, + // - top-level fields (empty $parents) are placed directly under + // $form_state['fields'][$field_name]. + // - Other fields are placed under + // $form_state['field']['#parents'][... $parents ...]['#fields'][$field_name] + // to avoid clashes between field names and $parents parts. + // @todo Remove backwards compatibility in Drupal 8, and use a unique + // $form_state['field'][... $parents ...]['#fields'][$field_name] structure. + if (!empty($parents)) { + $path = array_merge(array('#parents'), $parents, array('#fields')); + } + else { + $path = array(); + } + $path = array_merge($path, array($field_name, $language)); + + return $path; +} + /** * Retrieves the field definition for a widget's helper callbacks. * @@ -432,8 +498,8 @@ function field_add_more_js($form, $form_state) { * The $field definition array for the current widget. */ function field_widget_field($element, $form_state) { - $info = $form_state['field'][$element['#field_name']][$element['#language']]; - return $info['field']; + $field_state = field_form_get_state($element['#field_parents'], $element['#field_name'], $element['#language'], $form_state); + return $field_state['field']; } /** @@ -456,6 +522,6 @@ function field_widget_field($element, $form_state) { * The $instance definition array for the current widget. */ function field_widget_instance($element, $form_state) { - $info = $form_state['field'][$element['#field_name']][$element['#language']]; - return $info['instance']; + $field_state = field_form_get_state($element['#field_parents'], $element['#field_name'], $element['#language'], $form_state); + return $field_state['instance']; }