diff --git a/core/modules/field/field.api.php b/core/modules/field/field.api.php index 73c4a83..aa4479c 100644 --- a/core/modules/field/field.api.php +++ b/core/modules/field/field.api.php @@ -689,13 +689,19 @@ function hook_field_is_empty($item, $field) { * Field API widgets specify how fields are displayed in edit forms. Fields of a * given @link field_types field type @endlink may be edited using more than one * widget. In this case, the Field UI module allows the site builder to choose - * which widget to use. Widget types are defined by implementing - * hook_field_widget_info(). + * which widget to use. + * + * Widgets are Plugins managed by the + * Drupal\field\Plugin\Type\Widget\WidgetPluginManager class. A widget is + * implemented by providing a class that implements + * Drupal\field\Plugin\Type\Widget\WidgetInterface (in most cases, by + * subclassing Drupal\field\Plugin\Type\Widget\WidgetBase), and provides the + * proper annotation block. * * Widgets are @link forms_api_reference.html Form API @endlink - * elements with additional processing capabilities. Widget hooks are typically - * called by the Field Attach API during the creation of the field form - * structure with field_attach_form(). + * elements with additional processing capabilities. The methods of the + * WidgetInterface object are typically called by the Field Attach API during + * the creation of the field form structure with field_attach_form(). * * @see field * @see field_types @@ -703,84 +709,11 @@ function hook_field_is_empty($item, $field) { */ /** - * Expose Field API widget types. - * - * @return - * An array describing the widget types implemented by the module. - * The keys are widget type names. To avoid name clashes, widget type - * names should be prefixed with the name of the module that exposes them. - * The values are arrays describing the widget type, with the following - * key/value pairs: - * - label: The human-readable name of the widget type. - * - description: A short description for the widget type. - * - field types: An array of field types the widget supports. - * - settings: An array whose keys are the names of the settings available - * for the widget type, and whose values are the default values for those - * settings. - * - behaviors: (optional) An array describing behaviors of the widget, with - * the following elements: - * - multiple values: One of the following constants: - * - FIELD_BEHAVIOR_DEFAULT: (default) If the widget allows the input of - * one single field value (most common case). The widget will be - * repeated for each value input. - * - FIELD_BEHAVIOR_CUSTOM: If one single copy of the widget can receive - * several field values. Examples: checkboxes, multiple select, - * comma-separated textfield. - * - default value: One of the following constants: - * - FIELD_BEHAVIOR_DEFAULT: (default) If the widget accepts default - * values. - * - FIELD_BEHAVIOR_NONE: if the widget does not support default values. - * - weight: (optional) An integer to determine the weight of this widget - * relative to other widgets in the Field UI when selecting a widget for a - * given field instance. - * - * @see hook_field_widget_info_alter() - * @see hook_field_widget_form() - * @see hook_field_widget_form_alter() - * @see hook_field_widget_WIDGET_TYPE_form_alter() - * @see hook_field_widget_error() - * @see hook_field_widget_settings_form() - */ -function hook_field_widget_info() { - return array( - 'text_textfield' => array( - 'label' => t('Text field'), - 'field types' => array('text'), - 'settings' => array('size' => 60), - 'behaviors' => array( - 'multiple values' => FIELD_BEHAVIOR_DEFAULT, - 'default value' => FIELD_BEHAVIOR_DEFAULT, - ), - ), - 'text_textarea' => array( - 'label' => t('Text area (multiple rows)'), - 'field types' => array('text_long'), - 'settings' => array('rows' => 5), - 'behaviors' => array( - 'multiple values' => FIELD_BEHAVIOR_DEFAULT, - 'default value' => FIELD_BEHAVIOR_DEFAULT, - ), - ), - 'text_textarea_with_summary' => array( - 'label' => t('Text area with a summary'), - 'field types' => array('text_with_summary'), - 'settings' => array('rows' => 9, 'summary_rows' => 3), - 'behaviors' => array( - 'multiple values' => FIELD_BEHAVIOR_DEFAULT, - 'default value' => FIELD_BEHAVIOR_DEFAULT, - ), - // As an advanced widget, force it to sink to the bottom of the choices. - 'weight' => 2, - ), - ); -} - -/** * Perform alterations on Field API widget types. * * @param $info - * Array of informations on widget types exposed by hook_field_widget_info() - * implementations. + * Array of informations on existing widget types, as collected by the + * annotation discovery mechanism. */ function hook_field_widget_info_alter(&$info) { // Add a setting to a widget type. @@ -789,93 +722,7 @@ function hook_field_widget_info_alter(&$info) { ); // Let a new field type re-use an existing widget. - $info['options_select']['field types'][] = 'my_field_type'; -} - -/** - * Return the form for a single field widget. - * - * Field widget form elements should be based on the passed-in $element, which - * contains the base form element properties derived from the field - * configuration. - * - * Field API will set the weight, field name and delta values for each form - * element. If there are multiple values for this field, the Field API will - * invoke this hook as many times as needed. - * - * Note that, depending on the context in which the widget is being included - * (regular entity form, field configuration form, advanced search form...), - * the values for $field and $instance might be different from the "official" - * 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 - * definitions they should operate on. The field_widget_field() and - * field_widget_instance() functions should be used instead to fetch the - * current working definitions from $form_state, where Field API stores them. - * - * Alternatively, hook_field_widget_form() can extract the needed specific - * properties from $field and $instance and set them as ad-hoc - * $element['#custom'] properties, for later use by its element callbacks. - * - * Other modules may alter the form element provided by this function using - * hook_field_widget_form_alter(). - * - * @param $form - * The form structure where widgets are being attached to. This might be a - * full form structure, or a sub-element of a larger form. - * @param $form_state - * An associative array containing the current state of the form. - * @param $field - * The field structure. - * @param $instance - * The field instance. - * @param $langcode - * The language associated with $items. - * @param $items - * Array of default values for this field. - * @param $delta - * The order of this item in the array of subelements (0, 1, 2, etc). - * @param $element - * A form element array containing basic properties for the widget: - * - #entity_type: The name of the entity the field is attached to. - * - #bundle: The name of the field bundle the field is contained in. - * - #field_name: The name of the field. - * - #language: The language the field is being edited in. - * - #field_parents: The 'parents' space for the field in the form. Most - * widgets can simply overlook this property. This identifies the - * location where the field values are placed within - * $form_state['values'], and is used to access processing information - * for the field through the field_form_get_state() and - * field_form_set_state() functions. - * - #columns: A list of field storage columns of the field. - * - #title: The sanitized element label for the field instance, ready for - * output. - * - #description: The sanitized element description for the field instance, - * ready for output. - * - #required: A Boolean indicating whether the element value is required; - * for required multiple value fields, only the first widget's values are - * required. - * - #delta: The order of this item in the array of subelements; see $delta - * above. - * - * @return - * The form elements for a single widget for this field. - * - * @see field_widget_field() - * @see field_widget_instance() - * @see hook_field_widget_form_alter() - * @see hook_field_widget_WIDGET_TYPE_form_alter() - */ -function hook_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { - $element += array( - '#type' => $instance['widget']['type'], - '#default_value' => isset($items[$delta]) ? $items[$delta] : '', - ); - return $element; + $info['options_select']['field_types'][] = 'my_field_type'; } /** @@ -956,53 +803,27 @@ function hook_field_widget_WIDGET_TYPE_form_alter(&$element, &$form_state, $cont * of the hook involves reading from the database, it is highly recommended to * statically cache the information. * - * @param $widget + * @param array $widget_properties * The instance's widget properties. - * @param $context + * @param array $context * An associative array containing: * - entity_type: The entity type; e.g., 'node' or 'user'. - * - entity: The entity object. + * - bundle: The bundle: e.g., 'page' or 'article'. * - field: The field that the widget belongs to. * - instance: The instance of the field. - * - default: A boolean indicating whether the form is being shown as a dummy - * form to set default values. * * @see hook_field_widget_properties_ENTITY_TYPE_alter() */ -function hook_field_widget_properties_alter(&$widget, $context) { +function hook_field_widget_properties_alter(array &$widget_properties, array $context) { // Change a widget's type according to the time of day. $field = $context['field']; if ($context['entity_type'] == 'node' && $field['field_name'] == 'field_foo') { $time = date('H'); - $widget['type'] = $time < 12 ? 'widget_am' : 'widget_pm'; + $widget_properties['type'] = $time < 12 ? 'widget_am' : 'widget_pm'; } } /** - * Flag a field-level validation error. - * - * @param $element - * An array containing the form element for the widget. The error needs to be - * flagged on the right sub-element, according to the widget's internal - * structure. - * @param $error - * An associative array with the following key-value pairs, as returned by - * hook_field_validate(): - * - error: the error code. Complex widgets might need to report different - * errors to different form elements inside the widget. - * - message: the human readable message to be displayed. - * @param $form - * The form structure where field elements are attached to. This might be a - * full form structure, or a sub-element of a larger form. - * @param $form_state - * An associative array containing the current state of the form. - */ -function hook_field_widget_error($element, $error, $form, &$form_state) { - form_error($element, $error['message']); -} - - -/** * @} End of "defgroup field_widget". */ @@ -2381,25 +2202,23 @@ function hook_field_extra_fields_display_alter(&$displays, $context) { * of the hook involves reading from the database, it is highly recommended to * statically cache the information. * - * @param $widget + * @param array $widget_properties * The instance's widget properties. - * @param $context + * @param array $context * An associative array containing: * - entity_type: The entity type; e.g., 'node' or 'user'. - * - entity: The entity object. + * - bundle: The bundle: e.g., 'page' or 'article'. * - field: The field that the widget belongs to. * - instance: The instance of the field. - * - default: A boolean indicating whether the form is being shown as a dummy - * form to set default values. * * @see hook_field_widget_properties_alter() */ -function hook_field_widget_properties_ENTITY_TYPE_alter(&$widget, $context) { +function hook_field_widget_properties_ENTITY_TYPE_alter(array &$widget_properties, array $context) { // Change a widget's type according to the time of day. $field = $context['field']; if ($field['field_name'] == 'field_foo') { $time = date('H'); - $widget['type'] = $time < 12 ? 'widget_am' : 'widget_pm'; + $widget_properties['type'] = $time < 12 ? 'widget_am' : 'widget_pm'; } } diff --git a/core/modules/field/field.attach.inc b/core/modules/field/field.attach.inc index 7c2acdb..86cdf02 100644 --- a/core/modules/field/field.attach.inc +++ b/core/modules/field/field.attach.inc @@ -108,6 +108,95 @@ const FIELD_STORAGE_INSERT = 'insert'; */ /** + * Invoke a method on all the fields of a given entity. + * + * @todo Remove _field_invoke() and friends when field types and formatters are + * turned into plugins. + * + * @param string $method + * The name of the method to invoke. + * @param Closure $target + * A closure that receives an $instance object and returns the object on + * which the method should be invoked. + * @param Drupal\Core\Entity\EntityInterface $entity + * The fully formed $entity_type entity. + * @param mixed $a + * A parameter for the invoked method. Defaults to NULL. + * @param mixed $b + * A parameter for the invoked method. Defaults to NULL. + * @param array $options + * An associative array of additional options, with the following keys: + * - field_name: The name of the field whose operation should be invoked. By + * default, the operation is invoked on all the fields in the entity's + * bundle. NOTE: This option is not compatible with the 'deleted' option; + * the 'field_id' option should be used instead. + * - field_id: The ID of the field whose operation should be invoked. By + * default, the operation is invoked on all the fields in the entity's' + * bundles. + * - deleted: If TRUE, the function will operate on deleted fields as well + * as non-deleted fields. If unset or FALSE, only non-deleted fields are + * operated on. + * - langcode: A language code or an array of language codes keyed by field + * name. It will be used to narrow down to a single value the available + * languages to act on. + */ +function field_invoke_method($method, \Closure $target_closure, EntityInterface $entity, &$a = NULL, &$b = NULL, array $options = array()) { + // Merge default options. + $default_options = array( + 'deleted' => FALSE, + 'langcode' => NULL, + ); + $options += $default_options; + + $entity_type = $entity->entityType(); + // Determine the list of instances to iterate on. + $instances = _field_invoke_get_instances($entity_type, $entity->bundle(), $options); + + // Iterate through the instances and collect results. + $return = array(); + foreach ($instances as $instance) { + + // Let the closure determine the target object on which the method should be + // called. + $target = $target_closure($instance); + + if (method_exists($target, $method)) { + $field = field_info_field_by_id($instance['field_id']); + $field_name = $field['field_name']; + + // Determine the list of languages to iterate on. + $available_langcodes = field_available_languages($entity_type, $field); + $langcodes = _field_language_suggestion($available_langcodes, $options['langcode'], $field_name); + + foreach ($langcodes as $langcode) { + $items = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array(); + + $result = $target->$method($entity, $langcode, $items, $a, $b); + + if (isset($result)) { + // For methods with array results, we merge results together. + // For methods with scalar results, we collect results in an array. + if (is_array($result)) { + $return = array_merge($return, $result); + } + else { + $return[] = $result; + } + } + + // Populate $items back in the field values, but avoid replacing missing + // fields with an empty array (those are not equivalent on update). + if ($items !== array() || isset($entity->{$field_name}[$langcode])) { + $entity->{$field_name}[$langcode] = $items; + } + } + } + } + + return $return; +} + +/** * Invoke a field hook. * * @param $op @@ -432,6 +521,20 @@ function _field_invoke_get_instances($entity_type, $bundle, $options) { } /** + * Defines a 'target closure' for field_invoke_method(). + * + * Used to invoke methods on an instance's widget. + * + * @return Closure + * A 'target closure' for field_invoke_method(). + */ +function _field_invoke_widget_target() { + return function ($instance) { + return $instance->getWidget(); + }; +} + +/** * Add form elements for all fields for an entity to a form structure. * * The form elements for the entity's fields are added by reference as direct @@ -543,7 +646,7 @@ function field_attach_form($entity_type, EntityInterface $entity, &$form, &$form // If no language is provided use the default site language. $options = array('langcode' => field_valid_language($langcode)); - $form += (array) _field_invoke_default('form', $entity_type, $entity, $form, $form_state, $options); + $form += (array) field_invoke_method('form', _field_invoke_widget_target(), $entity, $form, $form_state, $options); // Add custom weight handling. $form['#pre_render'][] = '_field_extra_fields_pre_render'; @@ -795,9 +898,6 @@ function field_attach_validate($entity_type, $entity) { * An associative array containing the current state of the form. */ function field_attach_form_validate($entity_type, EntityInterface $entity, $form, &$form_state) { - // Extract field values from submitted values. - _field_invoke_default('extract_form_values', $entity_type, $entity, $form, $form_state); - // Perform field_level validation. try { field_attach_validate($entity_type, $entity); @@ -812,7 +912,7 @@ function field_attach_form_validate($entity_type, EntityInterface $entity, $form field_form_set_state($form['#parents'], $field_name, $langcode, $form_state, $field_state); } } - _field_invoke_default('form_errors', $entity_type, $entity, $form, $form_state); + field_invoke_method('flagErrors', _field_invoke_widget_target(), $entity, $form, $form_state); } } @@ -836,9 +936,7 @@ function field_attach_form_validate($entity_type, EntityInterface $entity, $form */ function field_attach_submit($entity_type, EntityInterface $entity, $form, &$form_state) { // Extract field values from submitted values. - _field_invoke_default('extract_form_values', $entity_type, $entity, $form, $form_state); - - _field_invoke_default('submit', $entity_type, $entity, $form, $form_state); + field_invoke_method('submit', _field_invoke_widget_target(), $entity, $form, $form_state); // Let other modules act on submitting the entity. // Avoid module_invoke_all() to let $form_state be taken by reference. diff --git a/core/modules/field/field.crud.inc b/core/modules/field/field.crud.inc index f1e5481..6ac93fa 100644 --- a/core/modules/field/field.crud.inc +++ b/core/modules/field/field.crud.inc @@ -559,6 +559,14 @@ function _field_write_instance($instance, $update = FALSE) { $field = field_read_field($instance['field_name']); $field_type = field_info_field_types($field['type']); + // Temporary workaround to allow incoming $instance as arrays or classed + // objects. + // @todo remove once the external APIs have been converted to use + // FieldInstance objects. + if (is_object($instance) && get_class($instance) == 'Drupal\field\FieldInstance') { + $instance = $instance->getArray(); + } + // Set defaults. $instance += array( 'settings' => array(), diff --git a/core/modules/field/field.default.inc b/core/modules/field/field.default.inc index b4a6f50..7d1a309 100644 --- a/core/modules/field/field.default.inc +++ b/core/modules/field/field.default.inc @@ -11,39 +11,6 @@ */ /** - * Extracts field values from submitted form values. - * - * @param $entity_type - * The type of $entity. - * @param $entity - * The entity for the operation. - * @param $field - * The field structure for the operation. - * @param $instance - * The instance structure for $field on $entity's bundle. - * @param $langcode - * The language associated to $items. - * @param $items - * The field values. This parameter is altered by reference to receive the - * incoming form values. - * @param $form - * The form structure where field elements are attached to. This might be a - * full form structure, or a sub-element of a larger form. - * @param $form_state - * The form state. - */ -function field_default_extract_form_values($entity_type, $entity, $field, $instance, $langcode, &$items, $form, &$form_state) { - $path = array_merge($form['#parents'], array($field['field_name'], $langcode)); - $key_exists = NULL; - $values = drupal_array_get_nested_value($form_state['values'], $path, $key_exists); - if ($key_exists) { - // Remove the 'value' of the 'add more' button. - unset($values['add_more']); - $items = $values; - } -} - -/** * Generic field validation handler. * * Possible error codes: @@ -86,13 +53,6 @@ function field_default_validate($entity_type, $entity, $field, $instance, $langc } } -function field_default_submit($entity_type, $entity, $field, $instance, $langcode, &$items, $form, &$form_state) { - // Filter out empty values. - $items = _field_filter_items($field, $items); - // Reorder items to account for drag-n-drop reordering. - $items = _field_sort_items($field, $items); -} - /** * Default field 'insert' operation. * diff --git a/core/modules/field/field.form.inc b/core/modules/field/field.form.inc index 8e7dbaa..dae9ec9 100644 --- a/core/modules/field/field.form.inc +++ b/core/modules/field/field.form.inc @@ -6,290 +6,6 @@ */ /** - * Creates a form element for a field and can populate it with a default value. - * - * If the form element is not associated with an entity (i.e., $entity is NULL) - * field_get_default_value will be called to supply the default value for the - * field. Also allows other modules to alter the form element by implementing - * their own hooks. - * - * @param $entity_type - * The type of entity (for example 'node' or 'user') that the field belongs - * to. - * @param $entity - * The entity object that the field belongs to. This may be NULL if creating a - * form element with a default value. - * @param $field - * An array representing the field whose editing element is being created. - * @param $instance - * An array representing the structure for $field in its current context. - * @param $langcode - * The language associated with the field. - * @param $items - * An array of the field values. When creating a new entity this may be NULL - * or an empty array to use default values. - * @param $form - * An array representing the form that the editing element will be attached - * to. - * @param $form_state - * An array containing the current state of the form. - * @param $get_delta - * Used to get only a specific delta value of a multiple value field. - * - * @return - * The form element array created for this field. - */ -function field_default_form($entity_type, $entity, $field, $instance, $langcode, $items, &$form, &$form_state, $get_delta = NULL) { - // This could be called with no entity, as when a UI module creates a - // dummy form to set default values. - if ($entity) { - $id = $entity->id(); - } - - $parents = $form['#parents']; - - $addition = array(); - $field_name = $field['field_name']; - $addition[$field_name] = array(); - - // Populate widgets with default values when creating a new entity. - if (empty($items) && empty($id)) { - $items = field_get_default_value($entity_type, $entity, $field, $instance, $langcode); - } - - // Let modules alter the widget properties. - $context = array( - 'entity_type' => $entity_type, - 'entity' => $entity, - 'field' => $field, - 'instance' => $instance, - 'default' => !$entity, - ); - drupal_alter(array('field_widget_properties', 'field_widget_properties_' . $entity_type), $instance['widget'], $context); - - // Collect widget elements. - $elements = array(); - - // Store field information in $form_state. - if (!field_form_get_state($parents, $field_name, $langcode, $form_state)) { - $field_state = array( - 'field' => $field, - 'instance' => $instance, - 'items_count' => count($items), - 'array_parents' => array(), - 'errors' => array(), - ); - field_form_set_state($parents, $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) { - // Store the entity in the form. - $form['#entity'] = $entity; - $elements = field_multiple_value_form($field, $instance, $langcode, $items, $form, $form_state); - } - // If the widget is handling multiple values (e.g Options), or if we are - // displaying an individual element, just get a single form element and make - // it the $delta value. - else { - $delta = isset($get_delta) ? $get_delta : 0; - $function = $instance['widget']['module'] . '_field_widget_form'; - if (function_exists($function)) { - $element = array( - '#entity_type' => $instance['entity_type'], - '#entity' => $entity, - '#bundle' => $instance['bundle'], - '#field_name' => $field_name, - '#language' => $langcode, - '#field_parents' => $parents, - '#columns' => array_keys($field['columns']), - '#title' => check_plain($instance['label']), - '#description' => field_filter_xss($instance['description']), - // Only the first widget should be required. - '#required' => $delta == 0 && $instance['required'], - '#delta' => $delta, - ); - if ($element = $function($form, $form_state, $field, $instance, $langcode, $items, $delta, $element)) { - // Allow modules to alter the field widget form element. - $context = array( - 'form' => $form, - 'field' => $field, - 'instance' => $instance, - 'langcode' => $langcode, - 'items' => $items, - 'delta' => $delta, - 'default' => !$entity, - ); - drupal_alter(array('field_widget_form', 'field_widget_' . $instance['widget']['type'] . '_form'), $element, $form_state, $context); - - // If we're processing a specific delta value for a field where the - // field module handles multiples, set the delta in the result. - // For fields that handle their own processing, we can't make - // assumptions about how the field is structured, just merge in the - // returned element. - if (field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) { - $elements[$delta] = $element; - } - else { - $elements = $element; - } - } - } - } - - // Also aid in theming of field widgets by rendering a classified container. - $addition[$field_name] = array( - '#type' => 'container', - '#attributes' => array( - 'class' => array( - 'field-type-' . drupal_html_class($field['type']), - 'field-name-' . drupal_html_class($field_name), - 'field-widget-' . drupal_html_class($instance['widget']['type']), - ), - ), - '#weight' => $instance['widget']['weight'], - ); - - // Populate the 'array_parents' information in $form_state['field'] after - // the form is built, so that we catch changes in the form structure performed - // in alter() hooks. - $elements['#after_build'][] = 'field_form_element_after_build'; - $elements['#field_name'] = $field_name; - $elements['#language'] = $langcode; - $elements['#field_parents'] = $parents; - - $addition[$field_name] += array( - '#tree' => TRUE, - // The '#language' key can be used to access the field's form element - // when $langcode is unknown. - '#language' => $langcode, - $langcode => $elements, - '#access' => field_access('edit', $field, $entity_type, $entity), - ); - - return $addition; -} - -/** - * Special handling to create form elements for multiple values. - * - * Handles generic features for multiple fields: - * - number of widgets - * - AHAH-'add more' button - * - drag-n-drop value reordering - */ -function field_multiple_value_form($field, $instance, $langcode, $items, &$form, &$form_state) { - $field_name = $field['field_name']; - $parents = $form['#parents']; - - // Determine the number of widgets to display. - switch ($field['cardinality']) { - case FIELD_CARDINALITY_UNLIMITED: - $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state); - $max = $field_state['items_count']; - break; - - default: - $max = $field['cardinality'] - 1; - break; - } - - $title = check_plain($instance['label']); - $description = field_filter_xss($instance['description']); - - $id_prefix = implode('-', array_merge($parents, array($field_name))); - $wrapper_id = drupal_html_id($id_prefix . '-add-more-wrapper'); - - $field_elements = array(); - - $function = $instance['widget']['module'] . '_field_widget_form'; - if (function_exists($function)) { - for ($delta = 0; $delta <= $max; $delta++) { - $multiple = $field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED; - $element = array( - '#entity_type' => $instance['entity_type'], - '#entity' => $form['#entity'], - '#bundle' => $instance['bundle'], - '#field_name' => $field_name, - '#language' => $langcode, - '#field_parents' => $parents, - '#columns' => array_keys($field['columns']), - // For multiple fields, title and description are handled by the wrapping table. - '#title' => $multiple ? '' : $title, - '#description' => $multiple ? '' : $description, - // Only the first widget should be required. - '#required' => $delta == 0 && $instance['required'], - '#delta' => $delta, - '#weight' => $delta, - ); - if ($element = $function($form, $form_state, $field, $instance, $langcode, $items, $delta, $element)) { - // Input field for the delta (drag-n-drop reordering). - if ($multiple) { - // We name the element '_weight' to avoid clashing with elements - // defined by widget. - $element['_weight'] = array( - '#type' => 'weight', - '#title' => t('Weight for row @number', array('@number' => $delta + 1)), - '#title_display' => 'invisible', - // Note: this 'delta' is the FAPI 'weight' element's property. - '#delta' => $max, - '#default_value' => isset($items[$delta]['_weight']) ? $items[$delta]['_weight'] : $delta, - '#weight' => 100, - ); - } - - // Allow modules to alter the field widget form element. - $context = array( - 'form' => $form, - 'field' => $field, - 'instance' => $instance, - 'langcode' => $langcode, - 'items' => $items, - 'delta' => $delta, - 'default' => FALSE, - ); - drupal_alter(array('field_widget_form', 'field_widget_' . $instance['widget']['type'] . '_form'), $element, $form_state, $context); - - $field_elements[$delta] = $element; - } - } - - if ($field_elements) { - $field_elements += array( - '#theme' => 'field_multiple_value_form', - '#field_name' => $field['field_name'], - '#cardinality' => $field['cardinality'], - '#title' => $title, - '#required' => $instance['required'], - '#description' => $description, - '#prefix' => '
', - '#suffix' => '
', - '#max_delta' => $max, - ); - // Add 'add more' button, if not working with a programmed form. - if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED && empty($form_state['programmed'])) { - $field_elements['add_more'] = array( - '#type' => 'submit', - '#name' => strtr($id_prefix, '-', '_') . '_add_more', - '#value' => t('Add another item'), - '#attributes' => array('class' => array('field-add-more-submit')), - '#limit_validation_errors' => array(array_merge($parents, array($field_name, $langcode))), - '#submit' => array('field_add_more_submit'), - '#ajax' => array( - 'callback' => 'field_add_more_js', - 'wrapper' => $wrapper_id, - 'effect' => 'fade', - ), - ); - } - } - } - - return $field_elements; -} - -/** * Returns HTML for an individual form element. * * Combine multiple values into a table with drag-n-drop reordering. @@ -387,43 +103,6 @@ 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_state = field_form_get_state($form['#parents'], $field['field_name'], $langcode, $form_state); - - if (!empty($field_state['errors'])) { - // Locate the correct element in the form. - $element = drupal_array_get_nested_value($form_state['complete_form'], $field_state['array_parents']); - // Only set errors if the element is accessible. - if (!isset($element['#access']) || $element['#access']) { - $function = $instance['widget']['module'] . '_field_widget_error'; - $function_exists = function_exists($function); - - $multiple_widget = field_behaviors_widget('multiple values', $instance) != FIELD_BEHAVIOR_DEFAULT; - foreach ($field_state['errors'] as $delta => $delta_errors) { - // For multiple single-value widgets, pass errors by delta. - // For a multiple-value widget, pass all errors to the main widget. - $error_element = $multiple_widget ? $element : $element[$delta]; - foreach ($delta_errors as $error) { - if ($function_exists) { - $function($error_element, $error, $form, $form_state); - } - 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['message']); - } - } - } - // Reinitialize the errors list for the next submit. - $field_state['errors'] = array(); - field_form_set_state($form['#parents'], $field['field_name'], $langcode, $form_state, $field_state); - } - } -} - -/** * Submit handler for the "Add another item" button of a field form. * * This handler is run regardless of whether JS is enabled or not. It makes diff --git a/core/modules/field/field.info.inc b/core/modules/field/field.info.inc index 65c6bcc..41dd9a6 100644 --- a/core/modules/field/field.info.inc +++ b/core/modules/field/field.info.inc @@ -5,6 +5,8 @@ * Field Info API, providing information about available fields and field types. */ +use Drupal\field\FieldInstance; + /** * @defgroup field_info Field Info API * @{ @@ -88,7 +90,6 @@ function _field_info_collate_types() { else { $info = array( 'field types' => array(), - 'widget types' => array(), 'formatter types' => array(), 'storage types' => array(), ); @@ -108,21 +109,6 @@ function _field_info_collate_types() { } drupal_alter('field_info', $info['field types']); - // Populate widget types. - foreach (module_implements('field_widget_info') as $module) { - $widget_types = (array) module_invoke($module, 'field_widget_info'); - foreach ($widget_types as $name => $widget_info) { - // Provide defaults. - $widget_info += array( - 'settings' => array(), - ); - $info['widget types'][$name] = $widget_info; - $info['widget types'][$name]['module'] = $module; - } - } - drupal_alter('field_widget_info', $info['widget types']); - uasort($info['widget types'], 'drupal_sort_weight'); - // Populate formatter types. foreach (module_implements('field_formatter_info') as $module) { $formatter_types = (array) module_invoke($module, 'field_formatter_info'); @@ -227,7 +213,7 @@ function _field_info_collate_fields() { foreach ($definitions['instances'] as $instance) { $field = $info['fields'][$instance['field_id']]; $instance = _field_info_prepare_instance($instance, $field); - $info['instances'][$instance['entity_type']][$instance['bundle']][$instance['field_name']] = $instance; + $info['instances'][$instance['entity_type']][$instance['bundle']][$instance['field_name']] = new FieldInstance($instance); // Enrich field definitions with the list of bundles where they have // instances. NOTE: Deleted fields in $info['field_ids'] are not // enriched because all of their instances are deleted, too, and @@ -310,8 +296,6 @@ function _field_info_prepare_instance($instance, $field) { $instance['default_value'] = NULL; } - $instance['widget'] = _field_info_prepare_instance_widget($field, $instance['widget']); - foreach ($instance['display'] as $view_mode => $display) { $instance['display'][$view_mode] = _field_info_prepare_instance_display($field, $display); } @@ -372,37 +356,6 @@ function _field_info_prepare_instance_display($field, $display) { } /** - * Prepares widget specifications for the current run-time context. - * - * @param $field - * The field structure for the instance. - * @param $widget - * Widget specifications as found in $instance['widget']. - */ -function _field_info_prepare_instance_widget($field, $widget) { - $field_type = field_info_field_types($field['type']); - - // Fill in default values. - $widget += array( - 'type' => $field_type['default_widget'], - 'settings' => array(), - 'weight' => 0, - ); - - $widget_type = field_info_widget_types($widget['type']); - // Fallback to default formatter if formatter type is not available. - if (!$widget_type) { - $widget['type'] = $field_type['default_widget']; - $widget_type = field_info_widget_types($widget['type']); - } - $widget['module'] = $widget_type['module']; - // Fill in default settings for the widget. - $widget['settings'] += field_info_widget_settings($widget['type']); - - return $widget; -} - -/** * Prepares 'extra fields' for the current run-time context. * * @param $extra_fields @@ -496,27 +449,22 @@ function field_info_field_types($field_type = NULL) { } /** - * Returns information about field widgets from hook_field_widget_info(). + * Returns information about field widgets from AnnotatedClassDiscovery. * * @param $widget_type * (optional) A widget type name. If omitted, all widget types will be * returned. * * @return - * Either a single widget type description, as provided by - * hook_field_widget_info(), or an array of all existing widget types, keyed - * by widget type name. + * Either a single widget type description, as provided by class annotations + * or an array of all existing widget types, keyed by widget type name. */ function field_info_widget_types($widget_type = NULL) { - $info = _field_info_collate_types(); - $widget_types = $info['widget types']; if ($widget_type) { - if (isset($widget_types[$widget_type])) { - return $widget_types[$widget_type]; - } + return field_get_plugin_manager('widget')->getDefinition($widget_type); } else { - return $widget_types; + return field_get_plugin_manager('widget')->getDefinitions(); } } diff --git a/core/modules/field/field.module b/core/modules/field/field.module index 72b6d91..ce652cf 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -490,6 +490,35 @@ function field_associate_fields($module) { } /** + * Returns the PluginManager object for a given field plugin type. + * + * @param string $plugin_type + * The plugin type. One of: + * - field_type + * - widget + * - formatter + * - storage + * + * @return Drupal\Component\Plugin\PluginManagerInterface + * The PluginManager object. + */ +function field_get_plugin_manager($plugin_type) { + $plugin_types = &drupal_static(__FUNCTION__, array()); + + $classes = array( + 'widget' => 'Drupal\field\Plugin\Type\Widget\WidgetPluginManager', + ); + + if (isset($classes[$plugin_type])) { + if (!isset($plugin_types[$plugin_type])) { + $plugin_types[$plugin_type] = new $classes[$plugin_type](); + } + + return $plugin_types[$plugin_type]; + } +} + +/** * Helper function to get the default value for a field on an entity. * * @param $entity_type diff --git a/core/modules/field/lib/Drupal/field/FieldInstance.php b/core/modules/field/lib/Drupal/field/FieldInstance.php new file mode 100644 index 0000000..f75eb83 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/FieldInstance.php @@ -0,0 +1,128 @@ +definition = $definition; + } + + /** + * Returns the Widget plugin for the instance. + * + * @return Drupal\field\Plugin\Type\Widget\WidgetInterface + * The Widget plugin to be used for the instance. + */ + public function getWidget() { + if (empty($this->widget)) { + $widget_properties = $this->definition['widget']; + + // Let modules alter the widget properties. + $context = array( + 'entity_type' => $this->definition['entity_type'], + 'bundle' => $this->definition['bundle'], + 'field' => field_info_field($this->definition['field_name']), + 'instance' => $this, + ); + drupal_alter(array('field_widget_properties', 'field_widget_properties_' . $this->definition['entity_type']), $widget_properties, $context); + + $options = array( + 'instance' => $this, + 'type' => $widget_properties['type'], + 'settings' => $widget_properties['settings'], + 'weight' => $widget_properties['weight'], + ); + $this->widget = field_get_plugin_manager('widget')->getInstance($options); + } + + return $this->widget; + } + + /** + * Implements ArrayAccess::offsetExists(). + */ + public function offsetExists($offset) { + return isset($this->definition[$offset]) || array_key_exists($offset, $this->definition); + } + + /** + * Implements ArrayAccess::offsetGet(). + */ + public function &offsetGet($offset) { + return $this->definition[$offset]; + } + + /** + * Implements ArrayAccess::offsetSet(). + */ + public function offsetSet($offset, $value) { + if (!isset($offset)) { + // Do nothing; $array[] syntax is not supported by this temporary wrapper. + return; + } + $this->definition[$offset] = $value; + + // If the widget properties changed, the widget plugin needs to be + // re-instanciated. + if ($offset == 'widget') { + unset($this->widget); + } + } + + /** + * Implements ArrayAccess::offsetUnset(). + */ + public function offsetUnset($offset) { + unset($this->definition[$offset]); + + // If the widget properties changed, the widget plugin needs to be + // re-instanciated. + if ($offset == 'widget') { + unset($this->widget); + } + } + + /** + * Returns the instance definition as a regular array. + * + * This is used as a temporary BC layer. + * @todo Remove once the external APIs have been converted to use + * FieldInstance objects. + * + * @return array + * The instance definition as a regular array. + */ + public function getArray() { + return $this->definition; + } + + /** + * Handles serialization of Drupal\field\FieldInstance objects. + */ + public function __sleep() { + return array('definition'); + } + +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/PluginSettingsBase.php b/core/modules/field/lib/Drupal/field/Plugin/PluginSettingsBase.php new file mode 100644 index 0000000..be22874 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/PluginSettingsBase.php @@ -0,0 +1,89 @@ +defaultSettingsMerged) { + $this->mergeDefaults(); + } + return $this->settings; + } + + /** + * Implements Drupal\field\Plugin\PluginSettingsInterface::getSetting(). + */ + public function getSetting($key) { + // Merge defaults if we have no value for the key. + if (!$this->defaultSettingsMerged && !array_key_exists($key, $this->settings)) { + $this->mergeDefaults(); + } + return isset($this->settings[$key]) ? $this->settings[$key] : NULL; + } + + /** + * Merges default settings values into $settings. + */ + protected function mergeDefaults() { + $this->settings += $this->getDefaultSettings(); + $this->defaultSettingsMerged = TRUE; + } + + /** + * Implements Drupal\field\Plugin\PluginSettingsInterface::getDefaultSettings(). + */ + public function getDefaultSettings() { + $definition = $this->getDefinition(); + return $definition['settings']; + } + + /** + * Implements Drupal\field\Plugin\PluginSettingsInterface::setSettings(). + */ + public function setSettings(array $settings) { + $this->settings = $settings; + $this->defaultSettingsMerged = FALSE; + return $this; + } + + /** + * Implements Drupal\field\Plugin\PluginSettingsInterface::setSetting(). + */ + public function setSetting($key, $value) { + $this->settings[$key] = $value; + return $this; + } + +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/PluginSettingsInterface.php b/core/modules/field/lib/Drupal/field/Plugin/PluginSettingsInterface.php new file mode 100644 index 0000000..955e88d --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/PluginSettingsInterface.php @@ -0,0 +1,69 @@ +decorated = $decorated; + } + + /** + * Implements Drupal\Component\Plugin\Discovery\DiscoveryInterface::getDefinition(). + */ + public function getDefinition($plugin_id) { + $definitions = $this->getDefinitions(); + return isset($definitions[$plugin_id]) ? $definitions[$plugin_id] : NULL; + } + + /** + * Implements Drupal\Component\Plugin\Discovery\DiscoveryInterface::getDefinitions(). + */ + public function getDefinitions() { + $definitions = $this->decorated->getDefinitions(); + + $legacy_discovery = new HookDiscovery('field_widget_info'); + if ($legacy_definitions = $legacy_discovery->getDefinitions()) { + foreach ($legacy_definitions as $plugin_id => &$definition) { + $definition['class'] = '\Drupal\field\Plugin\field\widget\LegacyWidget'; + + // Transform properties for which the format has changed. + if (isset($definition['field types'])) { + $definition['field_types'] = $definition['field types']; + unset($definition['field types']); + } + if (isset($definition['behaviors']['multiple values'])) { + $definition['multiple_values'] = $definition['behaviors']['multiple values']; + unset($definition['behaviors']['multiple values']); + } + + $definitions[$plugin_id] = $definition; + } + } + return $definitions; + } + +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetBase.php b/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetBase.php new file mode 100644 index 0000000..6b653ff --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetBase.php @@ -0,0 +1,447 @@ +instance = $instance; + $this->field = field_info_field($instance['field_name']); + $this->settings = $settings; + $this->weight = $weight; + } + + /** + * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::form(). + */ + public function form(EntityInterface $entity, $langcode, array $items, array &$form, array &$form_state, $get_delta = NULL) { + $entity_type = $entity->entityType(); + $field = $this->field; + $instance = $this->instance; + $field_name = $field['field_name']; + + $parents = $form['#parents']; + + $addition = array( + $field_name => array(), + ); + + // Populate widgets with default values when creating a new entity. + if (empty($items) && ($entity->isNew())) { + $items = field_get_default_value($entity_type, $entity, $field, $instance, $langcode); + } + + // Store field information in $form_state. + if (!field_form_get_state($parents, $field_name, $langcode, $form_state)) { + $field_state = array( + 'field' => $field, + 'instance' => $instance, + 'items_count' => count($items), + 'array_parents' => array(), + 'errors' => array(), + ); + field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state); + } + + // Collect widget elements. + $elements = array(); + + // If the widget is handling multiple values (e.g Options), or if we are + // displaying an individual element, just get a single form element and make + // it the $delta value. + $definition = $this->getDefinition(); + if (isset($get_delta) || $definition['multiple_values']) { + $delta = isset($get_delta) ? $get_delta : 0; + $element = array( + '#title' => check_plain($instance['label']), + '#description' => field_filter_xss($instance['description']), + ); + $element = $this->formSingleElement($entity, $items, $delta, $langcode, $element, $form, $form_state); + + if ($element) { + if (isset($get_delta)) { + // If we are processing a specific delta value for a field where the + // field module handles multiples, set the delta in the result. + $elements[$delta] = $element; + } + else { + // For fields that handle their own processing, we cannot make + // assumptions about how the field is structured, just merge in the + // returned element. + $elements = $element; + } + } + } + // If the widget does not handle multiple values itself, (and we are not + // displaying an individual element), process the multiple value form. + else { + $elements = $this->formMultipleElements($entity, $items, $langcode, $form, $form_state); + } + + // Also aid in theming of field widgets by rendering a classified + // container. + $addition[$field_name] = array( + '#type' => 'container', + '#attributes' => array( + 'class' => array( + 'field-type-' . drupal_html_class($field['type']), + 'field-name-' . drupal_html_class($field_name), + 'field-widget-' . drupal_html_class($this->getPluginId()), + ), + ), + '#weight' => $this->weight, + ); + + // Populate the 'array_parents' information in $form_state['field'] after + // the form is built, so that we catch changes in the form structure performed + // in alter() hooks. + $elements['#after_build'][] = 'field_form_element_after_build'; + $elements['#field_name'] = $field_name; + $elements['#language'] = $langcode; + $elements['#field_parents'] = $parents; + + $addition[$field_name] += array( + '#tree' => TRUE, + // The '#language' key can be used to access the field's form element + // when $langcode is unknown. + '#language' => $langcode, + $langcode => $elements, + '#access' => field_access('edit', $field, $entity_type, $entity), + ); + + return $addition; + } + + /** + * Special handling to create form elements for multiple values. + * + * Handles generic features for multiple fields: + * - number of widgets + * - AHAH-'add more' button + * - table display and drag-n-drop value reordering + */ + protected function formMultipleElements(EntityInterface $entity, array $items, $langcode, array &$form, array &$form_state) { + $field = $this->field; + $instance = $this->instance; + $field_name = $field['field_name']; + + $parents = $form['#parents']; + + // Determine the number of widgets to display. + switch ($field['cardinality']) { + case FIELD_CARDINALITY_UNLIMITED: + $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state); + $max = $field_state['items_count']; + $is_multiple = TRUE; + break; + + default: + $max = $field['cardinality'] - 1; + $is_multiple = ($field['cardinality'] > 1); + break; + } + + $id_prefix = implode('-', array_merge($parents, array($field_name))); + $wrapper_id = drupal_html_id($id_prefix . '-add-more-wrapper'); + + $title = check_plain($instance['label']); + $description = field_filter_xss($instance['description']); + + $elements = array(); + + for ($delta = 0; $delta <= $max; $delta++) { + // For multiple fields, title and description are handled by the wrapping + // table. + $element = array( + '#title' => $is_multiple ? '' : $title, + '#description' => $is_multiple ? '' : $description, + ); + $element = $this->formSingleElement($entity, $items, $delta, $langcode, $element, $form, $form_state); + + if ($element) { + // Input field for the delta (drag-n-drop reordering). + if ($is_multiple) { + // We name the element '_weight' to avoid clashing with elements + // defined by widget. + $element['_weight'] = array( + '#type' => 'weight', + '#title' => t('Weight for row @number', array('@number' => $delta + 1)), + '#title_display' => 'invisible', + // Note: this 'delta' is the FAPI #type 'weight' element's property. + '#delta' => $max, + '#default_value' => isset($items[$delta]['_weight']) ? $items[$delta]['_weight'] : $delta, + '#weight' => 100, + ); + } + + $elements[$delta] = $element; + } + } + + if ($elements) { + $elements += array( + '#theme' => 'field_multiple_value_form', + '#field_name' => $field['field_name'], + '#cardinality' => $field['cardinality'], + '#required' => $instance['required'], + '#title' => $title, + '#description' => $description, + '#prefix' => '
', + '#suffix' => '
', + '#max_delta' => $max, + ); + + // Add 'add more' button, if not working with a programmed form. + if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED && empty($form_state['programmed'])) { + $elements['add_more'] = array( + '#type' => 'submit', + '#name' => strtr($id_prefix, '-', '_') . '_add_more', + '#value' => t('Add another item'), + '#attributes' => array('class' => array('field-add-more-submit')), + '#limit_validation_errors' => array(array_merge($parents, array($field_name, $langcode))), + '#submit' => array('field_add_more_submit'), + '#ajax' => array( + 'callback' => 'field_add_more_js', + 'wrapper' => $wrapper_id, + 'effect' => 'fade', + ), + ); + } + } + + return $elements; + } + + /** + * Generates the form element for a single copy of the widget. + */ + protected function formSingleElement(EntityInterface $entity, array $items, $delta, $langcode, array $element, array &$form, array &$form_state) { + $instance = $this->instance; + $field = $this->field; + + $element += array( + '#entity_type' => $entity->entityType(), + '#bundle' => $entity->bundle(), + '#entity' => $entity, + '#field_name' => $field['field_name'], + '#language' => $langcode, + '#field_parents' => $form['#parents'], + '#columns' => array_keys($field['columns']), + // Only the first widget should be required. + '#required' => $delta == 0 && $instance['required'], + '#delta' => $delta, + '#weight' => $delta, + ); + + $element = $this->formElement($items, $delta, $element, $langcode, $form, $form_state); + + if ($element) { + // Allow modules to alter the field widget form element. + $context = array( + 'form' => $form, + 'field' => $field, + 'instance' => $instance, + 'langcode' => $langcode, + 'items' => $items, + 'delta' => $delta, + 'default' => !empty($entity->field_ui_default_value), + ); + drupal_alter(array('field_widget_form', 'field_widget_' . $this->getPluginId() . '_form'), $element, $form_state, $context); + } + + return $element; + } + + /** + * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::submit(). + */ + public function submit(EntityInterface $entity, $langcode, array &$items, array $form, array &$form_state) { + $field_name = $this->field['field_name']; + + // Extract the values from $form_state['values']. + $path = array_merge($form['#parents'], array($field_name, $langcode)); + $key_exists = NULL; + $values = drupal_array_get_nested_value($form_state['values'], $path, $key_exists); + + if ($key_exists) { + // Remove the 'value' of the 'add more' button. + unset($values['add_more']); + + // Let the widget turn the submitted values into actual field values. + // Make sure the '_weight' entries are persisted in the process. + $weights = array(); + if (isset($values[0]['_weight'])) { + foreach ($values as $delta => $value) { + $weights[$delta] = $value['_weight']; + } + } + $items = $this->massageFormValues($values, $form, $form_state); + + foreach ($items as $delta => &$item) { + // Put back the weight. + if (isset($weights[$delta])) { + $item['_weight'] = $weights[$delta]; + } + // The tasks below are going to reshuffle deltas. Keep track of the + // original deltas for correct reporting of errors in flagErrors(). + $item['_original_delta'] = $delta; + } + + // Account for drag-n-drop reordering. + $this->sortItems($items); + + // Remove empty values. + $items = _field_filter_items($this->field, $items); + + // Put delta mapping in $form_state, so that flagErrors() can use it. + $field_state = field_form_get_state($form['#parents'], $field_name, $langcode, $form_state); + foreach ($items as $delta => &$item) { + $field_state['original_deltas'][$delta] = $item['_original_delta']; + unset($item['_original_delta']); + } + field_form_set_state($form['#parents'], $field_name, $langcode, $form_state, $field_state); + } + } + + /** + * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::flagErrors(). + */ + public function flagErrors(EntityInterface $entity, $langcode, array $items, array $form, array &$form_state) { + $field_name = $this->field['field_name']; + + $field_state = field_form_get_state($form['#parents'], $field_name, $langcode, $form_state); + + if (!empty($field_state['errors'])) { + // Locate the correct element in the the form. + $element = drupal_array_get_nested_value($form_state['complete_form'], $field_state['array_parents']); + + // Only set errors if the element is accessible. + if (!isset($element['#access']) || $element['#access']) { + $definition = $this->getDefinition(); + $is_multiple = $definition['multiple_values']; + + foreach ($field_state['errors'] as $delta => $delta_errors) { + // For a multiple-value widget, pass all errors to the main widget. + // For single-value widgets, pass errors by delta. + if ($is_multiple) { + $delta_element = $element; + } + else { + $original_delta = $field_state['original_deltas'][$delta]; + $delta_element = $element[$original_delta]; + } + foreach ($delta_errors as $error) { + $error_element = $this->errorElement($delta_element, $error, $form, $form_state); + form_error($error_element, $error['message']); + } + } + // Reinitialize the errors list for the next submit. + $field_state['errors'] = array(); + field_form_set_state($form['#parents'], $field_name, $langcode, $form_state, $field_state); + } + } + } + + /** + * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::settingsForm(). + */ + public function settingsForm(array $form, array &$form_state) { + return array(); + } + + /** + * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::errorElement(). + */ + public function errorElement(array $element, array $error, array $form, array &$form_state) { + return $element; + } + + /** + * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::massageFormValues() + */ + public function massageFormValues(array $values, array $form, array &$form_state) { + return $values; + } + + /** + * Sorts submitted field values according to drag-n-drop reordering. + * + * @param array $items + * The field values. + */ + protected function sortItems(array &$items) { + $is_multiple = ($this->field['cardinality'] == FIELD_CARDINALITY_UNLIMITED) || ($this->field['cardinality'] > 1); + if ($is_multiple && isset($items[0]['_weight'])) { + usort($items, function ($a, $b) { + $a_weight = (is_array($a) ? $a['_weight'] : 0); + $b_weight = (is_array($b) ? $b['_weight'] : 0); + return $a_weight - $b_weight; + }); + // Remove the '_weight' entries. + foreach ($items as $delta => &$item) { + if (is_array($item)) { + unset($item['_weight']); + } + } + } + } + +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetBaseInterface.php b/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetBaseInterface.php new file mode 100644 index 0000000..8fdd785 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetBaseInterface.php @@ -0,0 +1,85 @@ +isNew() is + * TRUE), the 'default value', if any, is pre-populated. Also allows other + * modules to alter the form element by implementing their own hooks. + * + * @param Drupal\Core\Entity\EntityInterface $entity + * The entity for which the widget is being built. + * @param string $langcode + * The language associated with the field. + * @param array $items + * An array of the field values. When creating a new entity this may be NULL + * or an empty array to use default values. + * @param array $form + * An array representing the form that the editing element will be attached + * to. + * @param array $form_state + * An array containing the current state of the form. + * @param int $get_delta + * Used to get only a specific delta value of a multiple value field. + * + * @return array + * The form element array created for this field. + */ + public function form(EntityInterface $entity, $langcode, array $items, array &$form, array &$form_state, $get_delta = NULL); + + /** + * Extracts field values from submitted form values. + * + * @param Drupal\Core\Entity\EntityInterface $entity + * The entity for which the widget is being submitted. + * @param string $langcode + * The language associated to $items. + * @param array $items + * The field values. This parameter is altered by reference to receive the + * incoming form values. + * @param array $form + * The form structure where field elements are attached to. This might be a + * full form structure, or a sub-element of a larger form. + * @param array $form_state + * The form state. + */ + public function submit(EntityInterface $entity, $langcode, array &$items, array $form, array &$form_state); + + /** + * Reports field-level validation errors against actual form elements. + * + * @param Drupal\Core\Entity\EntityInterface $entity + * The entity for which the widget is being submitted. + * @param string $langcode + * The language associated to $items. + * @param array $items + * The field values. + * @param array $form + * The form structure where field elements are attached to. This might be a + * full form structure, or a sub-element of a larger form. + * @param array $form_state + * The form state. + */ + public function flagErrors(EntityInterface $entity, $langcode, array $items, array $form, array &$form_state); + +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetFactory.php b/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetFactory.php new file mode 100644 index 0000000..851cbea --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetFactory.php @@ -0,0 +1,24 @@ +getPluginClass($plugin_id); + return new $plugin_class($plugin_id, $this->discovery, $configuration['instance'], $configuration['settings'], $configuration['weight']); + } +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetInterface.php b/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetInterface.php new file mode 100644 index 0000000..c0ffc95 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/Type/Widget/WidgetInterface.php @@ -0,0 +1,164 @@ + array(), + 'multiple_values' => FALSE, + ); + + protected $cache_bin = 'field'; + protected $cache_key = 'field_widget_types'; + protected $hook = 'field_widget_info'; + + /** + * Constructs a WidgetPluginManager object. + */ + public function __construct() { + $this->baseDiscovery = new LegacyDiscoveryDecorator(new AnnotatedClassDiscovery('field', 'widget')); + $this->discovery = new CacheDecorator($this->baseDiscovery, $this->cache_key, $this->cache_bin); + + $this->factory = new WidgetFactory($this); + } + + /** + * Clear cached definitions. + * + * @todo Remove when http://drupal.org/node/1764232 is fixed. + */ + public function clearDefinitions() { + // Clear 'static' data by creating a new object. + $this->discovery = new CacheDecorator($this->baseDiscovery, $this->cache_key, $this->cache_bin); + cache($this->cache_bin)->delete($this->cache_key); + } + + /** + * Overrides Drupal\Component\Plugin\PluginManagerBase::getDefinition(). + * + * @todo Remove when http://drupal.org/node/1778942 is fixed. + */ + public function getDefinition($plugin_id) { + $definition = $this->discovery->getDefinition($plugin_id); + if (!empty($definition)) { + $this->processDefinition($definition, $plugin_id); + return $definition;; + } + } + + /** + * Overrides Drupal\Component\Plugin\PluginManagerBase::getInstance(). + */ + public function getInstance(array $options) { + $instance = $options['instance']; + $type = $options['type']; + + $definition = $this->getDefinition($type); + $field = field_info_field($instance['field_name']); + + // Switch back to default widget if either: + // - $type_info doesn't exist (the widget type is unknown), + // - the field type is not allowed for the widget. + if (!isset($definition['class']) || !in_array($field['type'], $definition['field_types'])) { + // Grab the default widget for the field type. + $field_type_definition = field_info_field_types($field['type']); + $type = $field_type_definition['default_widget']; + } + + $configuration = array( + 'instance' => $instance, + 'settings' => $options['settings'], + 'weight' => $options['weight'], + ); + return $this->createInstance($type, $configuration); + } + +} diff --git a/core/modules/field/lib/Drupal/field/Plugin/field/widget/LegacyWidget.php b/core/modules/field/lib/Drupal/field/Plugin/field/widget/LegacyWidget.php new file mode 100644 index 0000000..166bd87 --- /dev/null +++ b/core/modules/field/lib/Drupal/field/Plugin/field/widget/LegacyWidget.php @@ -0,0 +1,106 @@ +getDefinition(); + $function = $definition['module'] . '_field_widget_settings_form'; + if (function_exists($function)) { + return $function($this->field, $this->instance); + } + return array(); + } + + /** + * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::formElement(). + */ + public function formElement(array $items, $delta, array $element, $langcode, array &$form, array &$form_state) { + $definition = $this->getDefinition(); + $function = $definition['module'] . '_field_widget_form'; + + if (function_exists($function)) { + // hook_field_widget_form() implementations read widget properties directly + // from $instance. Put the actual properties we use here, which might have + // been altered by hook_field_widget_property(). + $instance = clone $this->instance; + $instance['widget']['type'] = $this->getPluginId(); + $instance['widget']['settings'] = $this->getSettings(); + + return $function($form, $form_state, $this->field, $instance, $langcode, $items, $delta, $element); + } + return array(); + } + + /** + * Overrides Drupal\field\Plugin\Type\Widget\WidgetBase::flagErrors(). + * + * In D7, hook_field_widget_error() was supposed to call form_error() itself, + * whereas the new errorElement() method simply returns the element to flag. + * So we override the flagError() method to be more similar to the previous + * code in field_default_form_errors(). + */ + public function flagErrors(EntityInterface $entity, $langcode, array $items, array $form, array &$form_state) { + $field_name = $this->field['field_name']; + + $field_state = field_form_get_state($form['#parents'], $field_name, $langcode, $form_state); + + if (!empty($field_state['errors'])) { + // Locate the correct element in the form. + $element = drupal_array_get_nested_value($form_state['complete_form'], $field_state['array_parents']); + // Only set errors if the element is accessible. + if (!isset($element['#access']) || $element['#access']) { + $definition = $this->getDefinition(); + $is_multiple = $definition['multiple_values']; + $function = $definition['module'] . '_field_widget_error'; + $function_exists = function_exists($function); + + foreach ($field_state['errors'] as $delta => $delta_errors) { + // For multiple single-value widgets, pass errors by delta. + // For a multiple-value widget, pass all errors to the main widget. + $error_element = $is_multiple ? $element : $element[$delta]; + foreach ($delta_errors as $error) { + if ($function_exists) { + $function($error_element, $error, $form, $form_state); + } + 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['message']); + } + } + } + // Reinitialize the errors list for the next submit. + $field_state['errors'] = array(); + field_form_set_state($form['#parents'], $field_name, $langcode, $form_state, $field_state); + } + } + } + +} diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldInfoTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldInfoTest.php index 7155af9..28112d2 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FieldInfoTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldInfoTest.php @@ -52,15 +52,6 @@ class FieldInfoTest extends FieldTestBase { $this->assertEqual($info[$f_key]['module'], 'field_test', t("Formatter type field_test module appears")); } - $widget_info = field_test_field_widget_info(); - $info = field_info_widget_types(); - foreach ($widget_info as $w_key => $widget) { - foreach ($widget as $key => $val) { - $this->assertEqual($info[$w_key][$key], $val, t("Widget type $w_key key $key is $val")); - } - $this->assertEqual($info[$w_key]['module'], 'field_test', t("Widget type field_test module appears")); - } - $storage_info = field_test_field_storage_info(); $info = field_info_storage_types(); foreach ($storage_info as $s_key => $storage) { @@ -208,9 +199,10 @@ class FieldInfoTest extends FieldTestBase { $this->assertIdentical($instance['settings'], $field_type['instance_settings'] , t('All expected instance settings are present.')); // Check that the default widget is used and expected settings are in place. - $this->assertIdentical($instance['widget']['type'], $field_type['default_widget'], t('Unavailable widget replaced with default widget.')); - $widget_type = field_info_widget_types($instance['widget']['type']); - $this->assertIdentical($instance['widget']['settings'], $widget_type['settings'] , t('All expected widget settings are present.')); + $widget = $instance->getWidget(); + $this->assertIdentical($widget->getPluginId(), $field_type['default_widget'], t('Unavailable widget replaced with default widget.')); + $widget_type = $widget->getDefinition(); + $this->assertIdentical($widget->getSettings(), $widget_type['settings'] , t('All expected widget settings are present.')); // Check that display settings are set for the 'default' mode. $display = $instance['display']['default']; @@ -256,9 +248,9 @@ class FieldInfoTest extends FieldTestBase { $this->assertIdentical(field_info_instance_settings($type), $data['instance_settings'], "field_info_field_settings returns {$type}'s field instance settings"); } - $info = field_test_field_widget_info(); - foreach ($info as $type => $data) { - $this->assertIdentical(field_info_widget_settings($type), $data['settings'], "field_info_widget_settings returns {$type}'s widget settings"); + foreach (array('test_field_widget', 'test_field_widget_multiple') as $type) { + $info = field_info_widget_types($type); + $this->assertIdentical(field_info_widget_settings($type), $info['settings'], "field_info_widget_settings returns {$type}'s widget settings"); } $info = field_test_field_formatter_info(); diff --git a/core/modules/field/lib/Drupal/field/Tests/FormTest.php b/core/modules/field/lib/Drupal/field/Tests/FormTest.php index dea9dc5..2e6121e 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FormTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/FormTest.php @@ -67,9 +67,8 @@ class FormTest extends FieldTestBase { $this->assertNoField("{$this->field_name}[$langcode][1][value]", 'No extraneous widget is displayed'); // TODO : check that the widget is populated with default value ? - // Check that neither hook_field_widget_properties_alter nor - // hook_field_widget_form_alter() believe this is the default value form. - $this->assertNoText('From hook_field_widget_properties_alter(): Default form is true.', 'Not default value form in hook_field_widget_properties_alter().'); + // Check that hook_field_widget_form_alter() does not believe this is the + // default value form. $this->assertNoText('From hook_field_widget_form_alter(): Default form is true.', 'Not default value form in hook_field_widget_form_alter().'); // Submit with invalid value (field-level validation). diff --git a/core/modules/field/modules/number/lib/Drupal/number/Plugin/field/widget/NumberWidget.php b/core/modules/field/modules/number/lib/Drupal/number/Plugin/field/widget/NumberWidget.php new file mode 100644 index 0000000..ecaf204 --- /dev/null +++ b/core/modules/field/modules/number/lib/Drupal/number/Plugin/field/widget/NumberWidget.php @@ -0,0 +1,83 @@ +field; + $instance = $this->instance; + + $value = isset($items[$delta]['value']) ? $items[$delta]['value'] : NULL; + + $element += array( + '#type' => 'number', + '#default_value' => $value, + ); + + // Set the step for floating point and decimal numbers. + switch ($field['type']) { + case 'number_decimal': + $element['#step'] = pow(0.1, $field['settings']['scale']); + break; + + case 'number_float': + $element['#step'] = 'any'; + break; + } + + // Set minimum and maximum. + if (is_numeric($instance['settings']['min'])) { + $element['#min'] = $instance['settings']['min']; + } + if (is_numeric($instance['settings']['max'])) { + $element['#max'] = $instance['settings']['max']; + } + + // Add prefix and suffix. + if (!empty($instance['settings']['prefix'])) { + $prefixes = explode('|', $instance['settings']['prefix']); + $element['#field_prefix'] = field_filter_xss(array_pop($prefixes)); + } + if (!empty($instance['settings']['suffix'])) { + $suffixes = explode('|', $instance['settings']['suffix']); + $element['#field_suffix'] = field_filter_xss(array_pop($suffixes)); + } + + return array('value' => $element); + } + + /** + * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::errorElement(). + */ + public function errorElement(array $element, array $error, array $form, array &$form_state) { + return $element['value']; + } + +} diff --git a/core/modules/field/modules/number/number.module b/core/modules/field/modules/number/number.module index 9985984..1a7024b 100644 --- a/core/modules/field/modules/number/number.module +++ b/core/modules/field/modules/number/number.module @@ -304,65 +304,3 @@ function number_field_formatter_view($entity_type, $entity, $field, $instance, $ return $element; } - -/** - * Implements hook_field_widget_info(). - */ -function number_field_widget_info() { - return array( - 'number' => array( - 'label' => t('Text field'), - 'field types' => array('number_integer', 'number_decimal', 'number_float'), - ), - ); -} - -/** - * Implements hook_field_widget_form(). - */ -function number_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { - $value = isset($items[$delta]['value']) ? $items[$delta]['value'] : ''; - - $element += array( - '#type' => 'number', - '#default_value' => $value, - ); - - // Set the step for floating point and decimal numbers. - switch ($field['type']) { - case 'number_decimal': - $element['#step'] = pow(0.1, $field['settings']['scale']); - break; - - case 'number_float': - $element['#step'] = 'any'; - break; - } - - // Set minimum and maximum. - if (is_numeric($instance['settings']['min'])) { - $element['#min'] = $instance['settings']['min']; - } - if (is_numeric($instance['settings']['max'])) { - $element['#max'] = $instance['settings']['max']; - } - - // Add prefix and suffix. - if (!empty($instance['settings']['prefix'])) { - $prefixes = explode('|', $instance['settings']['prefix']); - $element['#field_prefix'] = field_filter_xss(array_pop($prefixes)); - } - if (!empty($instance['settings']['suffix'])) { - $suffixes = explode('|', $instance['settings']['suffix']); - $element['#field_suffix'] = field_filter_xss(array_pop($suffixes)); - } - - return array('value' => $element); -} - -/** - * Implements hook_field_widget_error(). - */ -function number_field_widget_error($element, $error, $form, &$form_state) { - form_error($element['value'], $error['message']); -} diff --git a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/widget/TextareaWidget.php b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/widget/TextareaWidget.php new file mode 100644 index 0000000..17f80a0 --- /dev/null +++ b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/widget/TextareaWidget.php @@ -0,0 +1,69 @@ + 'number', + '#title' => t('Rows'), + '#default_value' => $this->getSetting('rows'), + '#required' => TRUE, + '#min' => 1, + ); + return $element; + } + + /** + * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::formElement(). + */ + public function formElement(array $items, $delta, array $element, $langcode, array &$form, array &$form_state) { + $main_widget = $element + array( + '#type' => 'textarea', + '#default_value' => isset($items[$delta]['value']) ? $items[$delta]['value'] : NULL, + '#rows' => $this->getSetting('rows'), + '#attributes' => array('class' => array('text-full')), + ); + + if ($this->instance['settings']['text_processing']) { + $element = $main_widget; + $element['#type'] = 'text_format'; + $element['#format'] = isset($items[$delta]['format']) ? $items[$delta]['format'] : NULL; + $element['#base_type'] = $main_widget['#type']; + } + else { + $element['value'] = $main_widget; + } + + return $element; + } + +} diff --git a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/widget/TextareaWithSummaryWidget.php b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/widget/TextareaWithSummaryWidget.php new file mode 100644 index 0000000..373f1ed --- /dev/null +++ b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/widget/TextareaWithSummaryWidget.php @@ -0,0 +1,73 @@ +instance['settings']['display_summary']; + $element['summary'] = array( + '#type' => $display_summary ? 'textarea' : 'value', + '#default_value' => isset($items[$delta]['summary']) ? $items[$delta]['summary'] : NULL, + '#title' => t('Summary'), + '#rows' => $this->getSetting('summary_rows'), + '#description' => t('Leave blank to use trimmed value of full text as the summary.'), + '#attached' => array( + 'js' => array(drupal_get_path('module', 'text') . '/text.js'), + ), + '#attributes' => array('class' => array('text-summary')), + '#prefix' => '
', + '#suffix' => '
', + '#weight' => -10, + ); + + return $element; + } + + /** + * Overrides TextareaWidget::errorElement(). + */ + public function errorElement(array $element, array $error, array $form, array &$form_state) { + switch ($error['error']) { + case 'text_summary_max_length': + $error_element = $element['summary']; + break; + + default: + $error_element = $element; + break; + } + + return $error_element; + } + +} diff --git a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/widget/TextfieldWidget.php b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/widget/TextfieldWidget.php new file mode 100644 index 0000000..e6b87b1 --- /dev/null +++ b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/widget/TextfieldWidget.php @@ -0,0 +1,70 @@ + 'number', + '#title' => t('Size of textfield'), + '#default_value' => $this->getSetting('size'), + '#required' => TRUE, + '#min' => 1, + ); + return $element; + } + + /** + * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::formElement(). + */ + public function formElement(array $items, $delta, array $element, $langcode, array &$form, array &$form_state) { + $main_widget = $element + array( + '#type' => 'textfield', + '#default_value' => isset($items[$delta]['value']) ? $items[$delta]['value'] : NULL, + '#size' => $this->getSetting('size'), + '#maxlength' => $this->field['settings']['max_length'], + '#attributes' => array('class' => array('text-full')), + ); + + if ($this->instance['settings']['text_processing']) { + $element = $main_widget; + $element['#type'] = 'text_format'; + $element['#format'] = isset($items[$delta]['format']) ? $items[$delta]['format'] : NULL; + $element['#base_type'] = $main_widget['#type']; + } + else { + $element['value'] = $main_widget; + } + + return $element; + } + +} diff --git a/core/modules/field/modules/text/text.module b/core/modules/field/modules/text/text.module index dcf4d1e..bbd209e 100644 --- a/core/modules/field/modules/text/text.module +++ b/core/modules/field/modules/text/text.module @@ -444,140 +444,6 @@ function text_summary($text, $format = NULL, $size = NULL) { } /** - * Implements hook_field_widget_info(). - */ -function text_field_widget_info() { - return array( - 'text_textfield' => array( - 'label' => t('Text field'), - 'field types' => array('text'), - 'settings' => array('size' => 60), - ), - 'text_textarea' => array( - 'label' => t('Text area (multiple rows)'), - 'field types' => array('text_long'), - 'settings' => array('rows' => 5), - ), - 'text_textarea_with_summary' => array( - 'label' => t('Text area with a summary'), - 'field types' => array('text_with_summary'), - 'settings' => array('rows' => 9, 'summary_rows' => 3), - ), - ); -} - -/** - * Implements hook_field_widget_settings_form(). - */ -function text_field_widget_settings_form($field, $instance) { - $widget = $instance['widget']; - $settings = $widget['settings']; - - if ($widget['type'] == 'text_textfield') { - $form['size'] = array( - '#type' => 'number', - '#title' => t('Size of textfield'), - '#default_value' => $settings['size'], - '#required' => TRUE, - '#min' => 1, - ); - } - else { - $form['rows'] = array( - '#type' => 'number', - '#title' => t('Rows'), - '#default_value' => $settings['rows'], - '#required' => TRUE, - '#min' => 1, - ); - } - - return $form; -} - -/** - * Implements hook_field_widget_form(). - */ -function text_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { - $summary_widget = array(); - $main_widget = array(); - - switch ($instance['widget']['type']) { - case 'text_textfield': - $main_widget = $element + array( - '#type' => 'textfield', - '#default_value' => isset($items[$delta]['value']) ? $items[$delta]['value'] : NULL, - '#size' => $instance['widget']['settings']['size'], - '#maxlength' => $field['settings']['max_length'], - '#attributes' => array('class' => array('text-full')), - ); - break; - - case 'text_textarea_with_summary': - $display = !empty($items[$delta]['summary']) || !empty($instance['settings']['display_summary']); - $summary_widget = array( - '#type' => $display ? 'textarea' : 'value', - '#default_value' => isset($items[$delta]['summary']) ? $items[$delta]['summary'] : NULL, - '#title' => t('Summary'), - '#rows' => $instance['widget']['settings']['summary_rows'], - '#description' => t('Leave blank to use trimmed value of full text as the summary.'), - '#attached' => array( - 'js' => array(drupal_get_path('module', 'text') . '/text.js'), - ), - '#attributes' => array('class' => array('text-summary')), - '#prefix' => '
', - '#suffix' => '
', - '#weight' => -10, - ); - // Fall through to the next case. - - case 'text_textarea': - $main_widget = $element + array( - '#type' => 'textarea', - '#default_value' => isset($items[$delta]['value']) ? $items[$delta]['value'] : NULL, - '#rows' => $instance['widget']['settings']['rows'], - '#attributes' => array('class' => array('text-full')), - ); - break; - } - - if ($main_widget) { - // Conditionally alter the form element's type if text processing is enabled. - if ($instance['settings']['text_processing']) { - $element = $main_widget; - $element['#type'] = 'text_format'; - $element['#format'] = isset($items[$delta]['format']) ? $items[$delta]['format'] : NULL; - $element['#base_type'] = $main_widget['#type']; - } - else { - $element['value'] = $main_widget; - } - } - if ($summary_widget) { - $element['summary'] = $summary_widget; - } - - return $element; -} - -/** - * Implements hook_field_widget_error(). - */ -function text_field_widget_error($element, $error, $form, &$form_state) { - switch ($error['error']) { - case 'text_summary_max_length': - $error_element = $element[$element['#columns'][1]]; - break; - - default: - $error_element = $element[$element['#columns'][0]]; - break; - } - - form_error($error_element, $error['message']); -} - -/** * Implements hook_field_prepare_translation(). */ function text_field_prepare_translation($entity_type, $entity, $field, $instance, $langcode, &$items, $source_entity, $source_langcode) { diff --git a/core/modules/field/tests/modules/field_test/field_test.entity.inc b/core/modules/field/tests/modules/field_test/field_test.entity.inc index 4819b3f..7acb4bb 100644 --- a/core/modules/field/tests/modules/field_test/field_test.entity.inc +++ b/core/modules/field/tests/modules/field_test/field_test.entity.inc @@ -337,9 +337,11 @@ function field_test_entity_nested_form($form, &$form_state, $entity_1, $entity_2 */ function field_test_entity_nested_form_validate($form, &$form_state) { $entity_1 = entity_create('test_entity', $form_state['values']); + field_attach_submit('test_entity', $entity_1, $form, $form_state); field_attach_form_validate('test_entity', $entity_1, $form, $form_state); $entity_2 = entity_create('test_entity', $form_state['values']['entity_2']); + field_attach_submit('test_entity', $entity_2, $form['entity_2'], $form_state); field_attach_form_validate('test_entity', $entity_2, $form['entity_2'], $form_state); } diff --git a/core/modules/field/tests/modules/field_test/field_test.field.inc b/core/modules/field/tests/modules/field_test/field_test.field.inc index 8933d8b..b2ed35c 100644 --- a/core/modules/field/tests/modules/field_test/field_test.field.inc +++ b/core/modules/field/tests/modules/field_test/field_test.field.inc @@ -163,53 +163,6 @@ function field_test_field_instance_settings_form($field, $instance) { } /** - * Implements hook_field_widget_info(). - */ -function field_test_field_widget_info() { - return array( - 'test_field_widget' => array( - 'label' => t('Test field'), - 'field types' => array('test_field', 'hidden_test_field'), - 'settings' => array('test_widget_setting' => 'dummy test string'), - ), - 'test_field_widget_multiple' => array( - 'label' => t('Test field 1'), - 'field types' => array('test_field'), - 'settings' => array('test_widget_setting_multiple' => 'dummy test string'), - 'behaviors' => array( - 'multiple values' => FIELD_BEHAVIOR_CUSTOM, - ), - ), - ); -} - -/** - * Implements hook_field_widget_form(). - */ -function field_test_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { - switch ($instance['widget']['type']) { - case 'test_field_widget': - $element += array( - '#type' => 'textfield', - '#default_value' => isset($items[$delta]['value']) ? $items[$delta]['value'] : '', - ); - return array('value' => $element); - - case 'test_field_widget_multiple': - $values = array(); - foreach ($items as $delta => $value) { - $values[] = $value['value']; - } - $element += array( - '#type' => 'textfield', - '#default_value' => implode(', ', $values), - '#element_validate' => array('field_test_widget_multiple_validate'), - ); - return $element; - } -} - -/** * Form element validation handler for 'test_field_widget_multiple' widget. */ function field_test_widget_multiple_validate($element, &$form_state) { @@ -222,42 +175,6 @@ function field_test_widget_multiple_validate($element, &$form_state) { } /** - * Implements hook_field_widget_error(). - */ -function field_test_field_widget_error($element, $error, $form, &$form_state) { - // @todo No easy way to differenciate widget types, we should receive it as a - // parameter. - if (isset($element['value'])) { - // Widget is test_field_widget. - $error_element = $element['value']; - } - else { - // Widget is test_field_widget_multiple. - $error_element = $element; - } - - form_error($error_element, $error['message']); -} - -/** - * Implements hook_field_widget_settings_form(). - */ -function field_test_field_widget_settings_form($field, $instance) { - $widget = $instance['widget']; - $settings = $widget['settings']; - - $form['test_widget_setting'] = array( - '#type' => 'textfield', - '#title' => t('Field test field widget setting'), - '#default_value' => $settings['test_widget_setting'], - '#required' => FALSE, - '#description' => t('A dummy form element to simulate field widget setting.'), - ); - - return $form; -} - -/** * Implements hook_field_formatter_info(). */ function field_test_field_formatter_info() { diff --git a/core/modules/field/tests/modules/field_test/field_test.module b/core/modules/field/tests/modules/field_test/field_test.module index 6dfbb15..f4bb287 100644 --- a/core/modules/field/tests/modules/field_test/field_test.module +++ b/core/modules/field/tests/modules/field_test/field_test.module @@ -238,11 +238,6 @@ function field_test_field_widget_properties_alter(&$widget, $context) { if (in_array($context['entity_type'], array('node', 'comment')) && ($context['field']['field_name'] == 'alter_test_text')) { $widget['settings']['size'] = 42; } - // Set a message if this is for the form displayed to set default value for - // the field instance. - if ($context['default']) { - drupal_set_message('From hook_field_widget_properties_alter(): Default form is true.'); - } } /** @@ -261,11 +256,11 @@ function field_test_field_widget_properties_user_alter(&$widget, $context) { function field_test_field_widget_form_alter(&$element, &$form_state, $context) { switch ($context['field']['field_name']) { case 'alter_test_text': - drupal_set_message('Field size: ' . $context['instance']['widget']['settings']['size']); + drupal_set_message('Field size: ' . $context['instance']->getWidget()->getSetting('size')); break; case 'alter_test_options': - drupal_set_message('Widget type: ' . $context['instance']['widget']['type']); + drupal_set_message('Widget type: ' . $context['instance']->getWidget()->getPluginId()); break; } // Set a message if this is for the form displayed to set default value for diff --git a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/field/widget/TestFieldWidget.php b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/field/widget/TestFieldWidget.php new file mode 100644 index 0000000..55471ba --- /dev/null +++ b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/field/widget/TestFieldWidget.php @@ -0,0 +1,64 @@ + 'textfield', + '#title' => t('Field test field widget setting'), + '#description' => t('A dummy form element to simulate field widget setting.'), + '#default_value' => $this->getSetting('test_widget_setting'), + '#required' => FALSE, + ); + return $element; + } + + /** + * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::formElement(). + */ + public function formElement(array $items, $delta, array $element, $langcode, array &$form, array &$form_state) { + $element += array( + '#type' => 'textfield', + '#default_value' => isset($items[$delta]['value']) ? $items[$delta]['value'] : '', + ); + return array('value' => $element); + } + + /** + * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::errorElement(). + */ + public function errorElement(array $element, array $error, array $form, array &$form_state) { + return $element['value']; + } + +} diff --git a/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/field/widget/TestFieldWidgetMultiple.php b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/field/widget/TestFieldWidgetMultiple.php new file mode 100644 index 0000000..ead51bd --- /dev/null +++ b/core/modules/field/tests/modules/field_test/lib/Drupal/field_test/Plugin/field/widget/TestFieldWidgetMultiple.php @@ -0,0 +1,69 @@ + 'textfield', + '#title' => t('Field test field widget setting'), + '#description' => t('A dummy form element to simulate field widget setting.'), + '#default_value' => $this->getSetting('test_widget_setting'), + '#required' => FALSE, + ); + return $element; + } + + /** + * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::formElement(). + */ + public function formElement(array $items, $delta, array $element, $langcode, array &$form, array &$form_state) { + $values = array(); + foreach ($items as $delta => $value) { + $values[] = $value['value']; + } + $element += array( + '#type' => 'textfield', + '#default_value' => implode(', ', $values), + '#element_validate' => array('field_test_widget_multiple_validate'), + ); + return $element; + } + + /** + * Implements Drupal\field\Plugin\Type\Widget\WidgetInterface::errorElement(). + */ + public function errorElement(array $element, array $error, array $form, array &$form_state) { + return $element; + } + +} diff --git a/core/modules/field_ui/field_ui.admin.inc b/core/modules/field_ui/field_ui.admin.inc index bb99234..54dc192 100644 --- a/core/modules/field_ui/field_ui.admin.inc +++ b/core/modules/field_ui/field_ui.admin.inc @@ -5,6 +5,8 @@ * Administrative interface for custom field type creation. */ +use Drupal\field\FieldInstance; + /** * Page callback: Lists all defined fields for quick reference. * @@ -307,7 +309,6 @@ function field_ui_field_overview_form($form, &$form_state, $entity_type, $bundle // Gather bundle information. $instances = field_info_instances($entity_type, $bundle); $field_types = field_info_field_types(); - $widget_types = field_info_widget_types(); $extra_fields = field_info_extra_fields($entity_type, $bundle, 'form'); @@ -345,6 +346,8 @@ function field_ui_field_overview_form($form, &$form_state, $entity_type, $bundle foreach ($instances as $name => $instance) { $field = field_info_field($instance['field_name']); $admin_field_path = $admin_path . '/fields/' . $instance['field_name']; + $widget_definition = $instance->getWidget()->getDefinition(); + $table[$name] = array( '#attributes' => array('class' => array('draggable', 'tabledrag-leaf')), '#row_type' => 'field', @@ -387,7 +390,7 @@ function field_ui_field_overview_form($form, &$form_state, $entity_type, $bundle ), 'widget_type' => array( '#type' => 'link', - '#title' => t($widget_types[$instance['widget']['type']]['label']), + '#title' => $widget_definition['label'], '#href' => $admin_field_path . '/widget-type', '#options' => array('attributes' => array('title' => t('Change widget type.'))), ), @@ -1475,8 +1478,10 @@ function field_ui_widget_type_options($field_type = NULL, $by_label = FALSE) { if (!isset($options)) { $options = array(); $field_types = field_info_field_types(); - foreach (field_info_widget_types() as $name => $widget_type) { - foreach ($widget_type['field types'] as $widget_field_type) { + $widget_types = field_info_widget_types(); + uasort($widget_types, 'drupal_sort_weight'); + foreach ($widget_types as $name => $widget_type) { + foreach ($widget_type['field_types'] as $widget_field_type) { // Check that the field type exists. if (isset($field_types[$widget_field_type])) { $options[$widget_field_type][$name] = $widget_type['label']; @@ -1657,7 +1662,7 @@ function field_ui_field_settings_form_submit($form, &$form_state) { * @see field_ui_widget_type_form_submit() * @ingroup forms */ -function field_ui_widget_type_form($form, &$form_state, $instance) { +function field_ui_widget_type_form($form, &$form_state, FieldInstance $instance) { drupal_set_title($instance['label']); $bundle = $instance['bundle']; @@ -1666,7 +1671,7 @@ function field_ui_widget_type_form($form, &$form_state, $instance) { $field = field_info_field($field_name); $field_type = field_info_field_types($field['type']); - $widget_type = field_info_widget_types($instance['widget']['type']); + $widget_definition = $instance->getWidget()->getDefinition(); $bundles = field_info_bundles(); $bundle_label = $bundles[$entity_type][$bundle]['label']; @@ -1685,7 +1690,7 @@ function field_ui_widget_type_form($form, &$form_state, $instance) { '#title' => t('Widget type'), '#required' => TRUE, '#options' => field_ui_widget_type_options($field['type']), - '#default_value' => $instance['widget']['type'], + '#default_value' => $instance->getWidget()->getPluginId(), '#description' => t('The type of form element you would like to present to the user when creating this field in the %type type.', array('%type' => $bundle_label)), ); @@ -1817,6 +1822,10 @@ function field_ui_field_edit_form($form, &$form_state, $instance) { $form['#field'] = $field; $form['#instance'] = $instance; + // Create an arbitrary entity object (used by the 'default value' widget). + $ids = (object) array('entity_type' => $instance['entity_type'], 'bundle' => $instance['bundle'], 'entity_id' => NULL); + $form['#entity'] = _field_create_entity_from_ids($ids); + $form['#entity']->field_ui_default_value = TRUE; if (!empty($field['locked'])) { $form['locked'] = array( @@ -1906,12 +1915,9 @@ function field_ui_field_edit_form($form, &$form_state, $instance) { $form['instance']['settings'] = $additions; } - // Add additional widget settings from the widget module. - $additions = module_invoke($widget_type['module'], 'field_widget_settings_form', $field, $instance); - if (is_array($additions)) { - $form['instance']['widget']['settings'] = $additions; - $form['instance']['widget']['active']['#value'] = 1; - } + // Add widget settings for the widget type. + $additions = $instance->getWidget()->settingsForm($form, $form_state); + $form['instance']['widget']['settings'] = $additions ? $additions : array('#type' => 'value', '#value' => array()); // Add handling for default value if not provided by any other module. if (field_behaviors_widget('default value', $instance) == FIELD_BEHAVIOR_DEFAULT && empty($instance['default_value_function'])) { @@ -2018,6 +2024,7 @@ function field_ui_field_edit_instance_pre_render($element) { */ function field_ui_default_value_widget($field, $instance, &$form, &$form_state) { $field_name = $field['field_name']; + $entity = $form['#entity']; $element = array( '#type' => 'fieldset', @@ -2031,13 +2038,15 @@ function field_ui_default_value_widget($field, $instance, &$form, &$form_state) '#parents' => array(), ); - // Insert the widget. - $items = $instance['default_value']; + // Adjust the instance definition used for the form element. We want a + // non-required input and no description. $instance['required'] = FALSE; $instance['description'] = ''; - // @todo Allow multiple values (requires more work on 'add more' JS handler). - $element += field_default_form($instance['entity_type'], NULL, $field, $instance, LANGUAGE_NOT_SPECIFIED, $items, $element, $form_state, 0); + // Insert the widget. Since we do not use the "official" instance definition, + // the whole flow cannot use field_invoke_method(). + $items = (array) $instance['default_value']; + $element += $instance->getWidget()->form($entity, LANGUAGE_NOT_SPECIFIED, $items, $element, $form_state); return $element; } @@ -2050,31 +2059,36 @@ function field_ui_default_value_widget($field, $instance, &$form, &$form_state) function field_ui_field_edit_form_validate($form, &$form_state) { // Take the incoming values as the $instance definition, so that the 'default // value' gets validated using the instance settings being submitted. - $instance = $form_state['values']['instance']; + $instance = $form['#instance']; $field_name = $instance['field_name']; + $entity = $form['#entity']; if (isset($form['instance']['default_value_widget'])) { $element = $form['instance']['default_value_widget']; - $field_state = field_form_get_state($element['#parents'], $field_name, LANGUAGE_NOT_SPECIFIED, $form_state); - $field = $field_state['field']; - // Extract the 'default value'. $items = array(); - field_default_extract_form_values(NULL, NULL, $field, $instance, LANGUAGE_NOT_SPECIFIED, $items, $element, $form_state); + $instance->getWidget()->submit($entity, LANGUAGE_NOT_SPECIFIED, $items, $element, $form_state); - // Validate the value and report errors. + // Grab the field definition from $form_state. + $field_state = field_form_get_state($element['#parents'], $field_name, LANGUAGE_NOT_SPECIFIED, $form_state); + $field = $field_state['field']; + + // Validate the value. $errors = array(); $function = $field['module'] . '_field_validate'; if (function_exists($function)) { $function(NULL, NULL, $field, $instance, LANGUAGE_NOT_SPECIFIED, $items, $errors); } + + // Report errors. if (isset($errors[$field_name][LANGUAGE_NOT_SPECIFIED])) { // Store reported errors in $form_state. $field_state['errors'] = $errors[$field_name][LANGUAGE_NOT_SPECIFIED]; field_form_set_state($element['#parents'], $field_name, LANGUAGE_NOT_SPECIFIED, $form_state, $field_state); + // Assign reported errors to the correct form element. - field_default_form_errors(NULL, NULL, $field, $instance, LANGUAGE_NOT_SPECIFIED, $items, $element, $form_state); + $instance->getWidget()->flagErrors($entity, LANGUAGE_NOT_SPECIFIED, $items, $element, $form_state); } } } @@ -2085,12 +2099,12 @@ function field_ui_field_edit_form_validate($form, &$form_state) { * @see field_ui_field_edit_form_validate(). */ function field_ui_field_edit_form_submit($form, &$form_state) { - $instance = $form_state['values']['instance']; - $field = $form_state['values']['field']; + $instance = $form['#instance']; + $field = $form['#field']; + $entity = $form['#entity']; - // Update any field settings that have changed. - $field_source = field_info_field($instance['field_name']); - $field = array_merge($field_source, $field); + // Merge incoming values into the field. + $field = array_merge($field, $form_state['values']['field']); try { field_update_field($field); } @@ -2105,15 +2119,15 @@ function field_ui_field_edit_form_submit($form, &$form_state) { // Extract field values. $items = array(); - field_default_extract_form_values(NULL, NULL, $field, $instance, LANGUAGE_NOT_SPECIFIED, $items, $element, $form_state); - field_default_submit(NULL, NULL, $field, $instance, LANGUAGE_NOT_SPECIFIED, $items, $element, $form_state); + $instance->getWidget()->submit($entity, LANGUAGE_NOT_SPECIFIED, $items, $element, $form_state); $instance['default_value'] = $items ? $items : NULL; } - // Retrieve the stored instance settings to merge with the incoming values. - $instance_source = field_read_instance($instance['entity_type'], $instance['field_name'], $instance['bundle']); - $instance = array_merge($instance_source, $instance); + // Merge incoming values into the instance. + foreach ($form_state['values']['instance'] as $key => $value) { + $instance[$key] = $value; + } field_update_instance($instance); drupal_set_message(t('Saved %label configuration.', array('%label' => $instance['label']))); diff --git a/core/modules/field_ui/field_ui.api.php b/core/modules/field_ui/field_ui.api.php index 80cbe78..e33fbdd 100644 --- a/core/modules/field_ui/field_ui.api.php +++ b/core/modules/field_ui/field_ui.api.php @@ -91,47 +91,6 @@ function hook_field_instance_settings_form($field, $instance) { } /** - * Add settings to a widget settings form. - * - * Invoked from field_ui_field_edit_form() to allow the module defining the - * widget to add settings for a widget instance. - * - * @param $field - * The field structure being configured. - * @param $instance - * The instance structure being configured. - * - * @return - * The form definition for the widget settings. - */ -function hook_field_widget_settings_form($field, $instance) { - $widget = $instance['widget']; - $settings = $widget['settings']; - - if ($widget['type'] == 'text_textfield') { - $form['size'] = array( - '#type' => 'number', - '#title' => t('Size of textfield'), - '#default_value' => $settings['size'], - '#min' => 1, - '#required' => TRUE, - ); - } - else { - $form['rows'] = array( - '#type' => 'number', - '#title' => t('Rows'), - '#default_value' => $settings['rows'], - '#min' => 1, - '#required' => TRUE, - ); - } - - return $form; -} - - -/** * Specify the form elements for a formatter's settings. * * @param $field diff --git a/core/modules/field_ui/lib/Drupal/field_ui/Tests/AlterTest.php b/core/modules/field_ui/lib/Drupal/field_ui/Tests/AlterTest.php index 687aa2e..7ee9501 100644 --- a/core/modules/field_ui/lib/Drupal/field_ui/Tests/AlterTest.php +++ b/core/modules/field_ui/lib/Drupal/field_ui/Tests/AlterTest.php @@ -69,10 +69,8 @@ class AlterTest extends WebTestBase { // size when the form is displayed. $this->drupalGet('admin/structure/types/manage/article/fields/alter_test_text'); $this->assertText('Field size: 42', 'Altered field size is found in hook_field_widget_form_alter().'); - // Test that hook_field_widget_properties_alter() and - // hook_field_widget_form_alter() registers this is the default value form - // and sets a message. - $this->assertText('From hook_field_widget_properties_alter(): Default form is true.', 'Default value form detected in hook_field_widget_properties_alter().'); + // Test that hook_field_widget_form_alter() registers this is the default + // value form and sets a message. $this->assertText('From hook_field_widget_form_alter(): Default form is true.', 'Default value form detected in hook_field_widget_form_alter().'); // Create the alter_test_options field. diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module index 61b018a..8e5a18c 100644 --- a/core/modules/forum/forum.module +++ b/core/modules/forum/forum.module @@ -255,9 +255,8 @@ function forum_uri($forum) { */ function _forum_node_check_node_type(Node $node) { // Fetch information about the forum field. - $field = field_info_instance('node', 'taxonomy_forums', $node->type); - - return is_array($field); + $instance = field_info_instance('node', 'taxonomy_forums', $node->type); + return !empty($instance); } /** diff --git a/core/modules/image/lib/Drupal/image/Tests/ImageFieldDefaultImagesTest.php b/core/modules/image/lib/Drupal/image/Tests/ImageFieldDefaultImagesTest.php index a72e052..f7670fe 100644 --- a/core/modules/image/lib/Drupal/image/Tests/ImageFieldDefaultImagesTest.php +++ b/core/modules/image/lib/Drupal/image/Tests/ImageFieldDefaultImagesTest.php @@ -56,12 +56,17 @@ class ImageFieldDefaultImagesTest extends ImageFieldTestBase { $instance = field_info_instance('node', $field_name, 'article'); // Add another instance with another default image to the page content type. - $instance2 = array_merge($instance, array( + $instance2 = array( + 'field_name' => $field['field_name'], + 'entity_type' => 'node', 'bundle' => 'page', + 'label' => $instance['label'], + 'required' => $instance['required'], 'settings' => array( 'default_image' => $default_images['instance2']->fid, ), - )); + 'widget' => $instance['widget'], + ); field_create_instance($instance2); $instance2 = field_info_instance('node', $field_name, 'page');