diff --git a/core/modules/entity_reference/entity_reference.module b/core/modules/entity_reference/entity_reference.module index 6ad51f7dd80..7125faeb816 100644 --- a/core/modules/entity_reference/entity_reference.module +++ b/core/modules/entity_reference/entity_reference.module @@ -63,6 +63,18 @@ function entity_reference_entity_field_info_alter(&$info, $entity_type) { } } +/** + * Implements hook_field_widget_info_alter(). + */ +function entity_reference_field_widget_info_alter(&$info) { + if (isset($info['options_select'])) { + $info['options_select']['field_types'][] = 'entity_reference'; + } + if (isset($info['options_buttons'])) { + $info['options_buttons']['field_types'][] = 'entity_reference'; + } +} + /** * Gets the selection handler for a given entity_reference field. * diff --git a/core/modules/options/lib/Drupal/options/Plugin/field/widget/ButtonsWidget.php b/core/modules/options/lib/Drupal/options/Plugin/field/widget/ButtonsWidget.php new file mode 100644 index 00000000000..163f46c6773 --- /dev/null +++ b/core/modules/options/lib/Drupal/options/Plugin/field/widget/ButtonsWidget.php @@ -0,0 +1,76 @@ +getOptions(); + $selected = $this->getSelectedOptions($items); + + // If required and there is one single option, preselect it. + if ($this->required && count($options) == 1) { + reset($options); + $selected = array(key($options)); + } + + if ($this->multiple) { + $element += array( + '#type' => 'checkboxes', + '#default_value' => $selected, + '#options' => $options, + ); + } + else { + $element += array( + '#type' => 'radios', + // Radio buttons need a scalar value. Take the first default value, or + // default to NULL so that the form element is properly recognized as + // not having a default value. + '#default_value' => $selected ? reset($selected) : NULL, + '#options' => $options, + ); + } + + return $element; + } + + /** + * {@inheritdoc} + */ + protected function getEmptyOption() { + if (!$this->required && !$this->multiple) { + return static::OPTIONS_EMPTY_NONE; + } + } + +} diff --git a/core/modules/options/lib/Drupal/options/Plugin/field/widget/OnOffWidget.php b/core/modules/options/lib/Drupal/options/Plugin/field/widget/OnOffWidget.php new file mode 100644 index 00000000000..7e6b4a599f2 --- /dev/null +++ b/core/modules/options/lib/Drupal/options/Plugin/field/widget/OnOffWidget.php @@ -0,0 +1,69 @@ + 'checkbox', + '#title' => t('Use field label instead of the "On value" as label'), + '#default_value' => $this->getSetting('display_label'), + '#weight' => -1, + ); + return $element; + } + + /** + * {@inheritdoc} + */ + public function formElement(array $items, $delta, array $element, $langcode, array &$form, array &$form_state) { + $element = parent::formElement($items, $delta, $element, $langcode, $form, $form_state); + + $options = $this->getOptions(); + $selected = $this->getSelectedOptions($items); + + $element += array( + '#type' => 'checkbox', + '#default_value' => !empty($selected[0]), + ); + + // Override the title from the incoming $element. + if ($this->getSetting('display_label')) { + $element['#title'] = $this->instance['label']; + } + else { + $element['#title'] = isset($options[1]) ? $options[1] : ''; + } + + return $element; + } + +} diff --git a/core/modules/options/lib/Drupal/options/Plugin/field/widget/OptionsWidgetBase.php b/core/modules/options/lib/Drupal/options/Plugin/field/widget/OptionsWidgetBase.php new file mode 100644 index 00000000000..40f24dfd948 --- /dev/null +++ b/core/modules/options/lib/Drupal/options/Plugin/field/widget/OptionsWidgetBase.php @@ -0,0 +1,206 @@ +field['columns']); + $this->column = key($this->field['columns']); + } + + /** + * {@inheritdoc} + */ + public function formElement(array $items, $delta, array $element, $langcode, array &$form, array &$form_state) { + // Prepare some properties for the child methods to build the actual form + // element. + $this->entity = $element['#entity']; + $this->required = $element['#required']; + $this->multiple = ($this->field['cardinality'] == FIELD_CARDINALITY_UNLIMITED) || ($this->field['cardinality'] > 1); + $this->has_value = isset($items[0][$this->column]); + + // Add our custom validator. + $element['#element_validate'][] = array(get_class($this), 'validateElement'); + $element['#key_column'] = $this->column; + + // The rest of the $element is built by child method implementations. + + return $element; + } + + /** + * Form validation handler for widget elements. + * + * @param array $element + * The form element. + * @param array $form_state + * The form state. + */ + public static function validateElement(array $element, array &$form_state) { + if ($element['#required'] && $element['#value'] == '_none') { + form_error($element, t('!name field is required.', array('!name' => $element['#title']))); + } + + // Massage submitted form values. + // Drupal\field\Plugin\Type\Widget\WidgetBase::submit() expects values as + // an array of values keyed by delta first, then by column, while our + // widgets return the opposite. + + if (is_array($element['#value'])) { + $values = array_values($element['#value']); + } + else { + $values = array($element['#value']); + } + + // Filter out the 'none' option. Use a strict comparison, because + // 0 == 'any string'. + $index = array_search('_none', $values, TRUE); + if ($index !== FALSE) { + unset($values[$index]); + } + + // Transpose selections from field => delta to delta => field. + $items = array(); + foreach ($values as $value) { + $items[] = array($element['#key_column'] => $value); + } + form_set_value($element, $items, $form_state); + } + + /** + * Returns the array of options for the widget. + * + * @return array + * The array of options for the widget. + */ + protected function getOptions() { + if (!isset($this->options)) { + // Get the list of options from the field type module, and sanitize them. + $options = (array) module_invoke($this->field['module'], 'options_list', $this->field, $this->instance, $this->entity); + array_walk_recursive($options, array($this, 'sanitizeLabel')); + + // Options might be nested ("optgroups"). If the widget does not support + // nested options, flatten the list. + if (!$this->supportsGroups()) { + $options = $this->flattenOptions($options); + } + + // Add an empty option if the widget needs one. + if ($empty_option = $this->getEmptyOption()) { + $label = theme('options_none', array('option' => $empty_option, 'widget' => $this, 'instance' => $this->instance)); + $options = array('_none' => $label) + $options; + } + + $this->options = $options; + } + return $this->options; + } + + /** + * Determines selected options from the incoming field values. + * + * @param array $items + * The field values. + * + * @return array + * The array of corresponding selected options. + */ + protected function getSelectedOptions(array $items) { + // We need to check against a flat list of options. + $flat_options = $this->flattenOptions($this->getOptions()); + + $selected_options = array(); + foreach ($items as $item) { + $value = $item[$this->column]; + // Keep the value if it actually is in the list of options (needs to be + // checked against the flat list). + if (isset($flat_options[$value])) { + $selected_options[] = $value; + } + } + + return $selected_options; + } + + /** + * Flattens an array of allowed values. + * + * @param array $array + * A single or multidimensional array. + * + * @return array + * The flattened array. + */ + protected function flattenOptions(array $array) { + $result = array(); + array_walk_recursive($array, function($a, $b) use (&$result) { $result[$b] = $a; }); + return $result; + } + + /** + * Indicates whether the widgets support optgroups. + * + * @return bool + * TRUE if the widget supports optgroups, FALSE otherwise. + */ + protected function supportsGroups() { + return FALSE; + } + + /** + * Sanitizes a string label to display as an option. + * + * @param string $label + * The label to sanitize. + */ + static protected function sanitizeLabel(&$label) { + // Allow a limited set of HTML tags. + $label = field_filter_xss($label); + } + + /** + * Returns the empty option to add to the list of options, if any. + * + * @return string|null + * Either static::OPTIONS_EMPTY_NONE, static::OPTIONS_EMPTY_SELECT, or NULL. + */ + protected function getEmptyOption() { } + +} diff --git a/core/modules/options/lib/Drupal/options/Plugin/field/widget/SelectWidget.php b/core/modules/options/lib/Drupal/options/Plugin/field/widget/SelectWidget.php new file mode 100644 index 00000000000..b17a448925c --- /dev/null +++ b/core/modules/options/lib/Drupal/options/Plugin/field/widget/SelectWidget.php @@ -0,0 +1,85 @@ + 'select', + '#options' => $this->getOptions(), + '#default_value' => $this->getSelectedOptions($items), + // Do not display a 'multiple' select box if there is only one option. + '#multiple' => $this->multiple && count($this->options) > 1, + ); + + return $element; + } + + /** + * {@inheritdoc} + */ + static protected function sanitizeLabel(&$label) { + // Select form inputs allow unencoded HTML entities, but no HTML tags. + $label = strip_tags($label); + } + + /** + * {@inheritdoc} + */ + protected function supportsGroups() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + protected function getEmptyOption() { + if ($this->multiple) { + // Multiple select: add a 'none' option for non-required fields. + if (!$this->required) { + return static::OPTIONS_EMPTY_NONE; + } + } + else { + // Single select: add a 'none' option for non-required fields, + // and a 'select a value' option for required fields that do not come + // with a value selected. + if (!$this->required) { + return static::OPTIONS_EMPTY_NONE; + } + if (!$this->has_value) { + return static::OPTIONS_EMPTY_SELECT; + } + } + } + +} diff --git a/core/modules/options/lib/Drupal/options/Tests/OptionsWidgetsTest.php b/core/modules/options/lib/Drupal/options/Tests/OptionsWidgetsTest.php index 023f01298cc..78efa3e320f 100644 --- a/core/modules/options/lib/Drupal/options/Tests/OptionsWidgetsTest.php +++ b/core/modules/options/lib/Drupal/options/Tests/OptionsWidgetsTest.php @@ -62,8 +62,7 @@ class OptionsWidgetsTest extends FieldTestBase { 'type' => 'list_boolean', 'cardinality' => 1, 'settings' => array( - // Make sure that 0 works as a 'on' value'. - 'allowed_values' => array(1 => 'Zero', 0 => 'Some & unescaped markup'), + 'allowed_values' => array(0 => 'Zero', 1 => 'Some & unescaped markup'), ), ); $this->bool = field_create_field($this->bool); @@ -458,7 +457,7 @@ class OptionsWidgetsTest extends FieldTestBase { // Submit form: check the option. $edit = array("bool[$langcode]" => TRUE); $this->drupalPost(NULL, $edit, t('Save')); - $this->assertFieldValues($entity_init, 'bool', $langcode, array(0)); + $this->assertFieldValues($entity_init, 'bool', $langcode, array(1)); // Display form: check that the right options are selected. $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); @@ -467,7 +466,7 @@ class OptionsWidgetsTest extends FieldTestBase { // Submit form: uncheck the option. $edit = array("bool[$langcode]" => FALSE); $this->drupalPost(NULL, $edit, t('Save')); - $this->assertFieldValues($entity_init, 'bool', $langcode, array(1)); + $this->assertFieldValues($entity_init, 'bool', $langcode, array(0)); // Display form: with 'off' value, option is unchecked. $this->drupalGet('test-entity/manage/' . $entity->ftid . '/edit'); diff --git a/core/modules/options/options.api.php b/core/modules/options/options.api.php index 2a303d5c7b2..e92768ff78d 100644 --- a/core/modules/options/options.api.php +++ b/core/modules/options/options.api.php @@ -20,8 +20,7 @@ * properties to filter out values from a list defined by field level * properties. * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity object the field is attached to, or NULL if no entity - * exists (e.g. in field settings page). + * The entity object the field is attached to. * * @return * The array of options for the field. Array keys are the values to be diff --git a/core/modules/options/options.module b/core/modules/options/options.module index 3789af54ffd..1062f7fc3cc 100644 --- a/core/modules/options/options.module +++ b/core/modules/options/options.module @@ -8,6 +8,7 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\EntityInterface; use Drupal\field\FieldUpdateForbiddenException; +use Drupal\options\Plugin\field\widget\OptionsWidgetBase; /** * Implements hook_help(). @@ -436,400 +437,6 @@ function options_field_is_empty($item, $field) { return FALSE; } -/** - * Implements hook_field_widget_info(). - * - * Field type modules willing to use those widgets should: - * - Use hook_field_widget_info_alter() to append their field own types to the - * list of types supported by the widgets, - * - Implement hook_options_list() to provide the list of options. - */ -function options_field_widget_info() { - return array( - 'options_select' => array( - 'label' => t('Select list'), - 'field types' => array('list_integer', 'list_float', 'list_text', 'entity_reference'), - 'behaviors' => array( - 'multiple values' => FIELD_BEHAVIOR_CUSTOM, - ), - ), - 'options_buttons' => array( - 'label' => t('Check boxes/radio buttons'), - 'field types' => array('list_integer', 'list_float', 'list_text', 'list_boolean', 'entity_reference'), - 'behaviors' => array( - 'multiple values' => FIELD_BEHAVIOR_CUSTOM, - ), - ), - 'options_onoff' => array( - 'label' => t('Single on/off checkbox'), - 'field types' => array('list_boolean'), - 'behaviors' => array( - 'multiple values' => FIELD_BEHAVIOR_CUSTOM, - ), - 'settings' => array('display_label' => 0), - ), - ); -} - -/** - * Implements hook_field_widget_form(). - */ -function options_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { - // Abstract over the actual field columns, to allow different field types to - // reuse those widgets. - - // Reset internal pointer since we're dealing with objects now. - reset($field['columns']); - $value_key = key($field['columns']); - - $type = str_replace('options_', '', $instance['widget']['type']); - $multiple = $field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED; - $required = $element['#required']; - $has_value = isset($items[0][$value_key]); - $properties = _options_properties($type, $multiple, $required, $has_value); - - $entity_type = $element['#entity_type']; - $entity = $element['#entity']; - - // Prepare the list of options. - $options = _options_get_options($field, $instance, $properties, $entity); - - // Put current field values in shape. - $default_value = _options_storage_to_form($items, $options, $value_key, $properties); - - switch ($type) { - case 'select': - $element += array( - '#type' => 'select', - '#default_value' => $default_value, - // Do not display a 'multiple' select box if there is only one option. - '#multiple' => $multiple && count($options) > 1, - '#options' => $options, - ); - break; - - case 'buttons': - // If required and there is one single option, preselect it. - if ($required && count($options) == 1) { - reset($options); - $default_value = array(key($options)); - } - - // If this is a single-value field, take the first default value, or - // default to NULL so that the form element is properly recognized as - // not having a default value. - if (!$multiple) { - $default_value = $default_value ? reset($default_value) : NULL; - } - - $element += array( - '#type' => $multiple ? 'checkboxes' : 'radios', - // Radio buttons need a scalar value. - '#default_value' => $default_value, - '#options' => $options, - ); - break; - - case 'onoff': - $keys = array_keys($options); - $off_value = array_shift($keys); - $on_value = array_shift($keys); - $element += array( - '#type' => 'checkbox', - '#default_value' => (isset($default_value[0]) && $default_value[0] == $on_value) ? 1 : 0, - '#on_value' => $on_value, - '#off_value' => $off_value, - ); - // Override the title from the incoming $element. - $element['#title'] = isset($options[$on_value]) ? $options[$on_value] : ''; - - if ($instance['widget']['settings']['display_label']) { - $element['#title'] = $instance['label']; - } - break; - } - - $element += array( - '#value_key' => $value_key, - '#element_validate' => array('options_field_widget_validate'), - '#properties' => $properties, - ); - - return $element; -} - -/** - * Implements hook_field_widget_settings_form(). - */ -function options_field_widget_settings_form($field, $instance) { - $form = array(); - if ($instance['widget']['type'] == 'options_onoff') { - $form['display_label'] = array( - '#type' => 'checkbox', - '#title' => t('Use field label instead of the "On value" as label'), - '#default_value' => $instance['widget']['settings']['display_label'], - '#weight' => -1, - ); - } - return $form; -} - -/** - * Form element validation handler for options elements. - */ -function options_field_widget_validate($element, &$form_state) { - if ($element['#required'] && $element['#value'] == '_none') { - form_error($element, t('!name field is required.', array('!name' => $element['#title']))); - } - // Transpose selections from field => delta to delta => field, turning - // multiple selected options into multiple parent elements. - $items = _options_form_to_storage($element); - form_set_value($element, $items, $form_state); -} - -/** - * Describes the preparation steps required by each widget. - * - * @param $type - * The type of widget: select, buttons, or onoff. - * @param $multiple - * TRUE if the field allows multiple values; FALSE otherwise. - * @param $required - * TRUE if a value is required for the field; FALSE otherwise. - * @param $has_value - * TRUE if a value is selected. - * - * @return - * An array of properties for the widget. - */ -function _options_properties($type, $multiple, $required, $has_value) { - $base = array( - 'filter_xss' => FALSE, - 'strip_tags' => FALSE, - 'empty_option' => FALSE, - 'optgroups' => FALSE, - ); - - $properties = array(); - - switch ($type) { - case 'select': - $properties = array( - // Select boxes do not support any HTML tag. - 'strip_tags' => TRUE, - 'optgroups' => TRUE, - ); - if ($multiple) { - // Multiple select: add a 'none' option for non-required fields. - if (!$required) { - $properties['empty_option'] = 'option_none'; - } - } - else { - // Single select: add a 'none' option for non-required fields, - // and a 'select a value' option for required fields that do not come - // with a value selected. - if (!$required) { - $properties['empty_option'] = 'option_none'; - } - elseif (!$has_value) { - $properties['empty_option'] = 'option_select'; - } - } - break; - - case 'buttons': - $properties = array( - 'filter_xss' => TRUE, - ); - // Add a 'none' option for non-required radio buttons. - if (!$required && !$multiple) { - $properties['empty_option'] = 'option_none'; - } - break; - - case 'onoff': - $properties = array( - 'filter_xss' => TRUE, - ); - break; - } - - return $properties + $base; -} - -/** - * Collects the options for a field. - */ -function _options_get_options($field, $instance, $properties, EntityInterface $entity) { - // Get the list of options. - $options = (array) module_invoke($field['module'], 'options_list', $field, $instance, $entity); - - // Sanitize the options. - _options_prepare_options($options, $properties); - - if (!$properties['optgroups']) { - $options = options_array_flatten($options); - } - - if ($properties['empty_option']) { - $label = theme('options_none', array('instance' => $instance, 'option' => $properties['empty_option'])); - $options = array('_none' => $label) + $options; - } - - return $options; -} - -/** - * Sanitizes the options recursively to support optgroups. - * - * @param $options - * The option array. - * @param $properties - * An array containing the properties of the widget. - */ -function _options_prepare_options(&$options, $properties) { - foreach ($options as $value => $label) { - // Recurse for optgroups. - if (is_array($label)) { - _options_prepare_options($options[$value], $properties); - } - else { - if ($properties['strip_tags']) { - $options[$value] = strip_tags($label); - } - if ($properties['filter_xss']) { - $options[$value] = field_filter_xss($label); - } - } - } -} - -/** - * Transforms stored field values into the format the widgets need. - * - * @param $items - * An array of stored field values. - * @param $options - * The options array. - * @param $column - * The field storage column of the field. - * @param $properties - * An array containing the properties of the widget. - * - * @return - * An array of values in the format used by widgets. - */ -function _options_storage_to_form($items, $options, $column, $properties) { - $items_transposed = options_array_transpose($items); - $values = (isset($items_transposed[$column]) && is_array($items_transposed[$column])) ? $items_transposed[$column] : array(); - - // Discard values that are not in the current list of options. Flatten the - // array if needed. - if ($properties['optgroups']) { - $options = options_array_flatten($options); - } - $values = array_values(array_intersect($values, array_keys($options))); - return $values; -} - -/** - * Transforms submitted form values into field storage format. - * - * @param $element - * The form element. - * - * @return - * An array of field values in field storage format. - */ -function _options_form_to_storage($element) { - $values = array_values((array) $element['#value']); - $properties = $element['#properties']; - - // On/off checkbox: transform '0 / 1' into the 'on / off' values. - if ($element['#type'] == 'checkbox') { - $values = array($values[0] ? $element['#on_value'] : $element['#off_value']); - } - - // Filter out the 'none' option. Use a strict comparison, because - // 0 == 'any string'. - if ($properties['empty_option']) { - $index = array_search('_none', $values, TRUE); - if ($index !== FALSE) { - unset($values[$index]); - } - } - - // Make sure we populate at least an empty value. - if (empty($values)) { - $values = array(NULL); - } - - $result = options_array_transpose(array($element['#value_key'] => $values)); - return $result; -} - -/** - * Manipulates a 2D array to reverse rows and columns. - * - * The default data storage for fields is delta first, column names second. This - * is sometimes inconvenient for field modules, so this function can be used to - * present the data in an alternate format. - * - * @param $array - * The array to be transposed. It must be at least two-dimensional, and the - * subarrays must all have the same keys or behavior is undefined. - * - * @return - * The transposed array. - */ -function options_array_transpose($array) { - $result = array(); - if (is_array($array)) { - foreach ($array as $key1 => $value1) { - if (is_array($value1)) { - foreach ($value1 as $key2 => $value2) { - if (!isset($result[$key2])) { - $result[$key2] = array(); - } - $result[$key2][$key1] = $value2; - } - } - } - } - return $result; -} - -/** - * Flattens an array of allowed values. - * - * @param $array - * A single or multidimensional array. - * @return - * A flattened array. - */ -function options_array_flatten($array) { - $result = array(); - if (is_array($array)) { - foreach ($array as $key => $value) { - if (is_array($value)) { - $result += options_array_flatten($value); - } - else { - $result[$key] = $value; - } - } - } - return $result; -} - -/** - * Implements hook_field_widget_error(). - */ -function options_field_widget_error($element, $error, $form, &$form_state) { - form_error($element, $error['message']); -} - /** * Implements hook_options_list(). */ @@ -840,27 +447,32 @@ function options_options_list($field, $instance, $entity) { /** * Returns HTML for the label for the empty value for non-required options. * - * The default theme will display N/A for a radio list and '- None -' for a - * select. + * The default theme will display 'N/A' for a radio list and '- None -' or + * 'Select a value' for a select. * * @param $variables * An associative array containing: - * - instance: An array representing the widget requesting the options. + * - option: A string representing the option that should be displayed. Either + * \Drupal\options\Plugin\field\widget\OptionsWidgetBase::OPTIONS_EMPTY_NONE + * or + * \Drupal\options\Plugin\field\widget\OptionsWidgetBase::OPTIONS_EMPTY_SELECT. + * - widget: The widget object requesting the option. + * - instance: The instance definition. * * @ingroup themeable */ function theme_options_none($variables) { - $instance = $variables['instance']; $option = $variables['option']; + $widget = $variables['widget']; $output = ''; - switch ($instance['widget']['type']) { + switch ($widget->getPluginId()) { case 'options_buttons': $output = t('N/A'); break; case 'options_select': - $output = ($option == 'option_none' ? t('- None -') : t('- Select a value -')); + $output = ($option == OptionsWidgetBase::OPTIONS_EMPTY_NONE ? t('- None -') : t('- Select a value -')); break; } diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module index 43d2ac84381..483d0355a77 100644 --- a/core/modules/taxonomy/taxonomy.module +++ b/core/modules/taxonomy/taxonomy.module @@ -1155,8 +1155,7 @@ function taxonomy_field_formatter_view(EntityInterface $entity, $field, $instanc * properties to filter out values from a list defined by field level * properties. * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity object the field is attached to, or NULL if no entity - * exists (e.g. in field settings page). + * The entity object the field is attached to. * * @return * The array of valid terms for this field, keyed by term id.