Initial Drupal 11 with DDEV setup
This commit is contained in:
		
							
								
								
									
										151
									
								
								web/core/modules/options/config/schema/options.schema.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								web/core/modules/options/config/schema/options.schema.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,151 @@
 | 
			
		||||
# Schema for the configuration files of the Options module.
 | 
			
		||||
 | 
			
		||||
# This field type has no field instance settings, so no specific config schema type.
 | 
			
		||||
# @see `type: field.field_settings.*`
 | 
			
		||||
field.storage_settings.list_integer:
 | 
			
		||||
  type: mapping
 | 
			
		||||
  label: 'List (integer) settings'
 | 
			
		||||
  mapping:
 | 
			
		||||
    allowed_values:
 | 
			
		||||
      type: sequence
 | 
			
		||||
      label: 'Allowed values list'
 | 
			
		||||
      sequence:
 | 
			
		||||
        type: mapping
 | 
			
		||||
        label: 'Allowed value with label'
 | 
			
		||||
        mapping:
 | 
			
		||||
          value:
 | 
			
		||||
            type: integer
 | 
			
		||||
            label: 'Value'
 | 
			
		||||
          label:
 | 
			
		||||
            type: label
 | 
			
		||||
            label: 'Label'
 | 
			
		||||
    allowed_values_function:
 | 
			
		||||
      type: string
 | 
			
		||||
      label: 'Allowed values function'
 | 
			
		||||
 | 
			
		||||
field.value.list_integer:
 | 
			
		||||
  type: mapping
 | 
			
		||||
  label: 'Default value'
 | 
			
		||||
  mapping:
 | 
			
		||||
    value:
 | 
			
		||||
      type: integer
 | 
			
		||||
      label: 'Value'
 | 
			
		||||
 | 
			
		||||
# This field type has no field instance settings, so no specific config schema type.
 | 
			
		||||
# @see `type: field.field_settings.*`
 | 
			
		||||
field.storage_settings.list_float:
 | 
			
		||||
  type: mapping
 | 
			
		||||
  label: 'List (float) settings'
 | 
			
		||||
  mapping:
 | 
			
		||||
    allowed_values:
 | 
			
		||||
      type: sequence
 | 
			
		||||
      label: 'Allowed values list'
 | 
			
		||||
      sequence:
 | 
			
		||||
        type: mapping
 | 
			
		||||
        label: 'Allowed value with label'
 | 
			
		||||
        mapping:
 | 
			
		||||
          value:
 | 
			
		||||
            type: float
 | 
			
		||||
            label: 'Value'
 | 
			
		||||
          label:
 | 
			
		||||
            type: label
 | 
			
		||||
            label: 'Label'
 | 
			
		||||
    allowed_values_function:
 | 
			
		||||
      type: string
 | 
			
		||||
      label: 'Allowed values function'
 | 
			
		||||
 | 
			
		||||
field.value.list_float:
 | 
			
		||||
  type: mapping
 | 
			
		||||
  label: 'Default value'
 | 
			
		||||
  mapping:
 | 
			
		||||
    value:
 | 
			
		||||
      type: string
 | 
			
		||||
      label: 'Value'
 | 
			
		||||
 | 
			
		||||
# This field type has no field instance settings, so no specific config schema type.
 | 
			
		||||
# @see `type: field.field_settings.*`
 | 
			
		||||
field.storage_settings.list_string:
 | 
			
		||||
  type: mapping
 | 
			
		||||
  label: 'List (text) settings'
 | 
			
		||||
  mapping:
 | 
			
		||||
    allowed_values:
 | 
			
		||||
      type: sequence
 | 
			
		||||
      label: 'Allowed values list'
 | 
			
		||||
      sequence:
 | 
			
		||||
        type: mapping
 | 
			
		||||
        label: 'Allowed value with label'
 | 
			
		||||
        mapping:
 | 
			
		||||
          value:
 | 
			
		||||
            type: string
 | 
			
		||||
            label: 'Value'
 | 
			
		||||
          label:
 | 
			
		||||
            type: label
 | 
			
		||||
            label: 'Label'
 | 
			
		||||
    allowed_values_function:
 | 
			
		||||
      type: string
 | 
			
		||||
      label: 'Allowed values function'
 | 
			
		||||
 | 
			
		||||
field.value.list_string:
 | 
			
		||||
  type: mapping
 | 
			
		||||
  label: 'Default value'
 | 
			
		||||
  mapping:
 | 
			
		||||
    value:
 | 
			
		||||
      type: string
 | 
			
		||||
      label: 'Value'
 | 
			
		||||
 | 
			
		||||
field.formatter.settings.list_default:
 | 
			
		||||
  type: mapping
 | 
			
		||||
  label: 'Options list default display settings'
 | 
			
		||||
 | 
			
		||||
field.formatter.settings.list_key:
 | 
			
		||||
  type: mapping
 | 
			
		||||
  label: 'Key format settings'
 | 
			
		||||
 | 
			
		||||
field.widget.settings.options_buttons:
 | 
			
		||||
  type: mapping
 | 
			
		||||
  label: 'Check boxes/radio buttons format settings'
 | 
			
		||||
 | 
			
		||||
field.widget.settings.options_select:
 | 
			
		||||
  type: mapping
 | 
			
		||||
  label: 'Select list format settings'
 | 
			
		||||
 | 
			
		||||
views.argument.number_list_field:
 | 
			
		||||
  type: views.argument.numeric
 | 
			
		||||
  mapping:
 | 
			
		||||
    summary:
 | 
			
		||||
      type: mapping
 | 
			
		||||
      label: 'Display a summary'
 | 
			
		||||
      mapping:
 | 
			
		||||
        sort_order:
 | 
			
		||||
          type: string
 | 
			
		||||
          label: 'Sort order'
 | 
			
		||||
        number_of_records:
 | 
			
		||||
          type: integer
 | 
			
		||||
          label: 'Sort by'
 | 
			
		||||
        format:
 | 
			
		||||
          type: string
 | 
			
		||||
          label: 'Format'
 | 
			
		||||
        human:
 | 
			
		||||
          type: boolean
 | 
			
		||||
 | 
			
		||||
views.argument.string_list_field:
 | 
			
		||||
  type: views.argument.string
 | 
			
		||||
  mapping:
 | 
			
		||||
    summary:
 | 
			
		||||
      type: mapping
 | 
			
		||||
      label: 'Display a summary'
 | 
			
		||||
      mapping:
 | 
			
		||||
        sort_order:
 | 
			
		||||
          type: string
 | 
			
		||||
          label: 'Sort order'
 | 
			
		||||
        number_of_records:
 | 
			
		||||
          type: integer
 | 
			
		||||
          label: 'Sort by'
 | 
			
		||||
        format:
 | 
			
		||||
          type: string
 | 
			
		||||
          label: 'Format'
 | 
			
		||||
        human:
 | 
			
		||||
          type: boolean
 | 
			
		||||
 | 
			
		||||
views.filter.list_field:
 | 
			
		||||
  type: views.filter.many_to_one
 | 
			
		||||
							
								
								
									
										9
									
								
								web/core/modules/options/css/options.icon.theme.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								web/core/modules/options/css/options.icon.theme.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
/*
 | 
			
		||||
 * DO NOT EDIT THIS FILE.
 | 
			
		||||
 * See the following change record for more information,
 | 
			
		||||
 * https://www.drupal.org/node/3084859
 | 
			
		||||
 * @preserve
 | 
			
		||||
 */
 | 
			
		||||
.field-icon-selection_list {
 | 
			
		||||
  background-image: url("data:image/svg+xml,%3csvg height='36' viewBox='0 0 36 36' width='36' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='m4.98 9.51v2.49h5.04v-4.98h-5.04zm7.02 0v1.47h19.98v-2.94h-19.98zm-7.02 9v2.49h5.04v-4.98h-5.04zm7.02-.03v1.5h19.98v-3h-19.98zm-7.02 9.03v2.49h5.04v-4.98h-5.04zm7.02 0v1.47h19.98v-2.94h-19.98z' fill='%2355565b'/%3e%3c/svg%3e");
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								web/core/modules/options/css/options.icon.theme.pcss.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								web/core/modules/options/css/options.icon.theme.pcss.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
.field-icon-selection_list {
 | 
			
		||||
  background-image: url(../../../misc/icons/55565b/selection_list.svg);
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
# cspell:ignore optionwidgets
 | 
			
		||||
finished:
 | 
			
		||||
  6:
 | 
			
		||||
    optionwidgets: options
 | 
			
		||||
  7:
 | 
			
		||||
    options: options
 | 
			
		||||
    list: options
 | 
			
		||||
							
								
								
									
										103
									
								
								web/core/modules/options/options.api.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								web/core/modules/options/options.api.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,103 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @file
 | 
			
		||||
 * Hooks provided by the Options module.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\FieldableEntityInterface;
 | 
			
		||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Alters the list of options to be displayed for a field.
 | 
			
		||||
 *
 | 
			
		||||
 * This hook can notably be used to change the label of the empty option.
 | 
			
		||||
 *
 | 
			
		||||
 * @param array $options
 | 
			
		||||
 *   The array of options for the field, as returned by
 | 
			
		||||
 *   \Drupal\Core\TypedData\OptionsProviderInterface::getSettableOptions(). An
 | 
			
		||||
 *   empty option (_none) might have been added, depending on the field
 | 
			
		||||
 *   properties.
 | 
			
		||||
 * @param array $context
 | 
			
		||||
 *   An associative array containing:
 | 
			
		||||
 *   - fieldDefinition: The field definition
 | 
			
		||||
 *     (\Drupal\Core\Field\FieldDefinitionInterface).
 | 
			
		||||
 *   - entity: The entity object the field is attached to
 | 
			
		||||
 *     (\Drupal\Core\Entity\EntityInterface).
 | 
			
		||||
 *   - widget: The widget object (\Drupal\Core\Field\WidgetInterface).
 | 
			
		||||
 *
 | 
			
		||||
 * @ingroup hooks
 | 
			
		||||
 * @see hook_options_list()
 | 
			
		||||
 */
 | 
			
		||||
function hook_options_list_alter(array &$options, array $context) {
 | 
			
		||||
  // Check if this is the field we want to change.
 | 
			
		||||
  if ($context['fieldDefinition']->getName() == 'field_option') {
 | 
			
		||||
    // Change the label of the empty option.
 | 
			
		||||
    $options['_none'] = t('== Empty ==');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provide the allowed values for a 'list_*' field.
 | 
			
		||||
 *
 | 
			
		||||
 * Callback for options_allowed_values().
 | 
			
		||||
 *
 | 
			
		||||
 * 'list_*' fields can specify a callback to define the set of their allowed
 | 
			
		||||
 * values using the 'allowed_values_function' storage setting.
 | 
			
		||||
 *
 | 
			
		||||
 * That function will be called:
 | 
			
		||||
 *  - either in the context of a specific entity, which is then provided as the
 | 
			
		||||
 *    $entity parameter,
 | 
			
		||||
 *  - or for the field generally without the context of any specific entity or
 | 
			
		||||
 *    entity bundle (typically, Views needing a list of values for an exposed
 | 
			
		||||
 *    filter), in which case the $entity parameter is NULL.
 | 
			
		||||
 * This lets the callback restrict the set of allowed values or adjust the
 | 
			
		||||
 * labels depending on some conditions on the containing entity.
 | 
			
		||||
 *
 | 
			
		||||
 * For consistency, the set of values returned when an $entity is provided
 | 
			
		||||
 * should be a subset of the values returned when no $entity is provided.
 | 
			
		||||
 *
 | 
			
		||||
 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $definition
 | 
			
		||||
 *   The field storage definition.
 | 
			
		||||
 * @param \Drupal\Core\Entity\FieldableEntityInterface|null $entity
 | 
			
		||||
 *   (optional) The entity context if known, or NULL if the allowed values are
 | 
			
		||||
 *   being collected without the context of a specific entity.
 | 
			
		||||
 * @param bool &$cacheable
 | 
			
		||||
 *   (optional) If an $entity is provided, the $cacheable parameter should be
 | 
			
		||||
 *   modified by reference and set to FALSE if the set of allowed values
 | 
			
		||||
 *   returned was specifically adjusted for that entity and cannot not be reused
 | 
			
		||||
 *   for other entities. Defaults to TRUE.
 | 
			
		||||
 *
 | 
			
		||||
 * @return array
 | 
			
		||||
 *   The array of allowed values. Keys of the array are the raw stored values
 | 
			
		||||
 *   (number or text), values of the array are the display labels. If $entity
 | 
			
		||||
 *   is NULL, you should return the list of all the possible allowed values in
 | 
			
		||||
 *   any context so that other code (e.g. Views filters) can support the allowed
 | 
			
		||||
 *   values for all possible entities and bundles.
 | 
			
		||||
 *
 | 
			
		||||
 * @ingroup callbacks
 | 
			
		||||
 * @see options_allowed_values()
 | 
			
		||||
 * @see \Drupal\options_test\OptionsAllowedValues::simpleValues()
 | 
			
		||||
 * @see \Drupal\options_test\OptionsAllowedValues::dynamicValues()
 | 
			
		||||
 */
 | 
			
		||||
function callback_allowed_values_function(FieldStorageDefinitionInterface $definition, ?FieldableEntityInterface $entity = NULL, &$cacheable = TRUE) {
 | 
			
		||||
  if (isset($entity) && ($entity->bundle() == 'not_a_programmer')) {
 | 
			
		||||
    $values = [
 | 
			
		||||
      1 => 'One',
 | 
			
		||||
      2 => 'Two',
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
  else {
 | 
			
		||||
    $values = [
 | 
			
		||||
      'Group 1' => [
 | 
			
		||||
        0 => 'Zero',
 | 
			
		||||
        1 => 'One',
 | 
			
		||||
      ],
 | 
			
		||||
      'Group 2' => [
 | 
			
		||||
        2 => 'Two',
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return $values;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,6 @@
 | 
			
		||||
selection_list:
 | 
			
		||||
  label: 'Selection list'
 | 
			
		||||
  description: 'Field to select from predefined options.'
 | 
			
		||||
  weight: -15
 | 
			
		||||
  libraries:
 | 
			
		||||
    - options/drupal.options-icon
 | 
			
		||||
							
								
								
									
										8
									
								
								web/core/modules/options/options.info.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								web/core/modules/options/options.info.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
name: Options
 | 
			
		||||
type: module
 | 
			
		||||
description: 'Defines field types with select lists, checkboxes, and radio buttons to select values from fixed lists of options.'
 | 
			
		||||
package: Field types
 | 
			
		||||
version: VERSION
 | 
			
		||||
dependencies:
 | 
			
		||||
  - drupal:field
 | 
			
		||||
  - drupal:text
 | 
			
		||||
							
								
								
									
										7
									
								
								web/core/modules/options/options.libraries.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								web/core/modules/options/options.libraries.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
drupal.options-icon:
 | 
			
		||||
  version: VERSION
 | 
			
		||||
  css:
 | 
			
		||||
    theme:
 | 
			
		||||
      css/options.icon.theme.css: {}
 | 
			
		||||
  dependencies:
 | 
			
		||||
    - field_ui/drupal.field_ui.manage_fields
 | 
			
		||||
							
								
								
									
										82
									
								
								web/core/modules/options/options.module
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								web/core/modules/options/options.module
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,82 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @file
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\FieldableEntityInterface;
 | 
			
		||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Returns the array of allowed values for a list field.
 | 
			
		||||
 *
 | 
			
		||||
 * The strings are not safe for output. Keys and values of the array should be
 | 
			
		||||
 * sanitized through \Drupal\Core\Field\FieldFilteredMarkup before being
 | 
			
		||||
 * displayed.
 | 
			
		||||
 *
 | 
			
		||||
 * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $definition
 | 
			
		||||
 *   The field storage definition.
 | 
			
		||||
 * @param \Drupal\Core\Entity\FieldableEntityInterface|null $entity
 | 
			
		||||
 *   (optional) The specific entity when this function is called from the
 | 
			
		||||
 *   context of a specific field on a specific entity. This allows custom
 | 
			
		||||
 *   'allowed_values_function' callbacks to either restrict the values or
 | 
			
		||||
 *   customize the labels for particular bundles and entities. NULL when
 | 
			
		||||
 *   there is not a specific entity available, such as for Views filters.
 | 
			
		||||
 *
 | 
			
		||||
 * @return array
 | 
			
		||||
 *   The array of allowed values. Keys of the array are the raw stored values
 | 
			
		||||
 *   (number or text), values of the array are the display labels.
 | 
			
		||||
 *
 | 
			
		||||
 * @see callback_allowed_values_function()
 | 
			
		||||
 */
 | 
			
		||||
function options_allowed_values(FieldStorageDefinitionInterface $definition, ?FieldableEntityInterface $entity = NULL) {
 | 
			
		||||
  $allowed_values = &drupal_static(__FUNCTION__, []);
 | 
			
		||||
 | 
			
		||||
  $cache_keys = [$definition->getTargetEntityTypeId(), $definition->getName()];
 | 
			
		||||
  if ($entity) {
 | 
			
		||||
    $cache_keys[] = 'entity';
 | 
			
		||||
  }
 | 
			
		||||
  $cache_id = implode(':', $cache_keys);
 | 
			
		||||
 | 
			
		||||
  if (!isset($allowed_values[$cache_id])) {
 | 
			
		||||
    $function = $definition->getSetting('allowed_values_function');
 | 
			
		||||
    // If $cacheable is FALSE, then the allowed values are not statically
 | 
			
		||||
    // cached. See OptionsAllowedValues::dynamicValues() for an example of
 | 
			
		||||
    // generating dynamic and uncached values.
 | 
			
		||||
    $cacheable = TRUE;
 | 
			
		||||
    if (!empty($function)) {
 | 
			
		||||
      $values = $function($definition, $entity, $cacheable);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $values = $definition->getSetting('allowed_values');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ($cacheable) {
 | 
			
		||||
      $allowed_values[$cache_id] = $values;
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      return $values;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return $allowed_values[$cache_id];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Checks if a list of values are being used in actual field values.
 | 
			
		||||
 */
 | 
			
		||||
function _options_values_in_use($entity_type, $field_name, $values) {
 | 
			
		||||
  if ($values) {
 | 
			
		||||
    $result = \Drupal::entityQuery($entity_type)
 | 
			
		||||
      ->condition($field_name . '.value', $values, 'IN')
 | 
			
		||||
      ->count()
 | 
			
		||||
      ->accessCheck(FALSE)
 | 
			
		||||
      ->range(0, 1)
 | 
			
		||||
      ->execute();
 | 
			
		||||
    if ($result) {
 | 
			
		||||
      return TRUE;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return FALSE;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								web/core/modules/options/options.services.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								web/core/modules/options/options.services.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
parameters:
 | 
			
		||||
  options.skip_procedural_hook_scan: true
 | 
			
		||||
							
								
								
									
										87
									
								
								web/core/modules/options/src/Hook/OptionsHooks.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								web/core/modules/options/src/Hook/OptionsHooks.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,87 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\options\Hook;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 | 
			
		||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
 | 
			
		||||
use Drupal\field\FieldStorageConfigInterface;
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Drupal\Core\Routing\RouteMatchInterface;
 | 
			
		||||
use Drupal\Core\Hook\Attribute\Hook;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Hook implementations for options.
 | 
			
		||||
 */
 | 
			
		||||
class OptionsHooks {
 | 
			
		||||
 | 
			
		||||
  use StringTranslationTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Implements hook_help().
 | 
			
		||||
   */
 | 
			
		||||
  #[Hook('help')]
 | 
			
		||||
  public function help($route_name, RouteMatchInterface $route_match): ?string {
 | 
			
		||||
    switch ($route_name) {
 | 
			
		||||
      case 'help.page.options':
 | 
			
		||||
        $output = '';
 | 
			
		||||
        $output .= '<h2>' . $this->t('About') . '</h2>';
 | 
			
		||||
        $output .= '<p>' . $this->t('The Options module allows you to create fields where data values are selected from a fixed list of options. Usually these items are entered through a select list, checkboxes, or radio buttons. See the <a href=":field">Field module help</a> and the <a href=":field_ui">Field UI help</a> pages for general information on fields and how to create and manage them. For more information, see the <a href=":options_do">online documentation for the Options module</a>.', [
 | 
			
		||||
          ':field' => Url::fromRoute('help.page', [
 | 
			
		||||
            'name' => 'field',
 | 
			
		||||
          ])->toString(),
 | 
			
		||||
          ':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
 | 
			
		||||
            'name' => 'field_ui',
 | 
			
		||||
          ])->toString() : '#',
 | 
			
		||||
          ':options_do' => 'https://www.drupal.org/documentation/modules/options',
 | 
			
		||||
        ]) . '</p>';
 | 
			
		||||
        $output .= '<h2>' . $this->t('Uses') . '</h2>';
 | 
			
		||||
        $output .= '<dl>';
 | 
			
		||||
        $output .= '<dt>' . $this->t('Managing and displaying list fields') . '</dt>';
 | 
			
		||||
        $output .= '<dd>' . $this->t('The <em>settings</em> and the <em>display</em> of the list fields can be configured separately. See the <a href=":field_ui">Field UI help</a> for more information on how to manage fields and their display.', [
 | 
			
		||||
          ':field_ui' => \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', [
 | 
			
		||||
            'name' => 'field_ui',
 | 
			
		||||
          ])->toString() : '#',
 | 
			
		||||
        ]) . '</dd>';
 | 
			
		||||
        $output .= '<dt>' . $this->t('Defining option keys and labels') . '</dt>';
 | 
			
		||||
        $output .= '<dd>' . $this->t('When you define the list options you can define a key and a label for each option in the list. The label will be shown to the users while the key gets stored in the database.') . '</dd>';
 | 
			
		||||
        $output .= '<dt>' . $this->t('Choosing list field type') . '</dt>';
 | 
			
		||||
        $output .= '<dd>' . $this->t('There are three types of list fields, which store different types of data: <em>float</em>, <em>integer</em> or, <em>text</em>. The <em>float</em> type allows storing approximate decimal values. The <em>integer</em> type allows storing whole numbers, such as years (for example, 2012) or values (for example, 1, 2, 5, 305). The <em>text</em> list field type allows storing text values. No matter which type of list field you choose, you can define whatever labels you wish for data entry.') . '</dd>';
 | 
			
		||||
        $output .= '</dl>';
 | 
			
		||||
        return $output;
 | 
			
		||||
    }
 | 
			
		||||
    return NULL;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Implements hook_ENTITY_TYPE_update() for 'field_storage_config'.
 | 
			
		||||
   */
 | 
			
		||||
  #[Hook('field_storage_config_update')]
 | 
			
		||||
  public function fieldStorageConfigUpdate(FieldStorageConfigInterface $field_storage): void {
 | 
			
		||||
    drupal_static_reset('options_allowed_values');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Implements hook_ENTITY_TYPE_delete() for 'field_storage_config'.
 | 
			
		||||
   */
 | 
			
		||||
  #[Hook('field_storage_config_delete')]
 | 
			
		||||
  public function fieldStorageConfigDelete(FieldStorageConfigInterface $field_storage): void {
 | 
			
		||||
    drupal_static_reset('options_allowed_values');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Implements hook_field_storage_config_update_forbid().
 | 
			
		||||
   */
 | 
			
		||||
  #[Hook('field_storage_config_update_forbid')]
 | 
			
		||||
  public function fieldStorageConfigUpdateForbid(FieldStorageConfigInterface $field_storage, FieldStorageConfigInterface $prior_field_storage): void {
 | 
			
		||||
    if ($field_storage->getTypeProvider() == 'options' && $field_storage->hasData()) {
 | 
			
		||||
      // Forbid any update that removes allowed values with actual data.
 | 
			
		||||
      $allowed_values = $field_storage->getSetting('allowed_values');
 | 
			
		||||
      $prior_allowed_values = $prior_field_storage->getSetting('allowed_values');
 | 
			
		||||
      $lost_keys = array_keys(array_diff_key($prior_allowed_values, $allowed_values));
 | 
			
		||||
      if (_options_values_in_use($field_storage->getTargetEntityTypeId(), $field_storage->getName(), $lost_keys)) {
 | 
			
		||||
        throw new FieldStorageDefinitionUpdateForbiddenException("A list field '{$field_storage->getName()}' with existing data cannot have its keys changed.");
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								web/core/modules/options/src/Hook/OptionsViewsHooks.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								web/core/modules/options/src/Hook/OptionsViewsHooks.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\options\Hook;
 | 
			
		||||
 | 
			
		||||
use Drupal\field\FieldStorageConfigInterface;
 | 
			
		||||
use Drupal\Core\Hook\Attribute\Hook;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Hook implementations for options.
 | 
			
		||||
 */
 | 
			
		||||
class OptionsViewsHooks {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Implements hook_field_views_data().
 | 
			
		||||
   *
 | 
			
		||||
   * Views integration for list fields. Have a different filter handler and
 | 
			
		||||
   * argument handlers for list fields. This should allow to select values of
 | 
			
		||||
   * the list.
 | 
			
		||||
   */
 | 
			
		||||
  #[Hook('field_views_data')]
 | 
			
		||||
  public function fieldViewsData(FieldStorageConfigInterface $field): array {
 | 
			
		||||
    $data = \Drupal::service('views.field_data_provider')->defaultFieldImplementation($field);
 | 
			
		||||
    foreach ($data as $table_name => $table_data) {
 | 
			
		||||
      foreach ($table_data as $field_name => $field_data) {
 | 
			
		||||
        if (isset($field_data['filter']) && $field_name != 'delta') {
 | 
			
		||||
          $data[$table_name][$field_name]['filter']['id'] = 'list_field';
 | 
			
		||||
        }
 | 
			
		||||
        if (isset($field_data['argument']) && $field_name != 'delta') {
 | 
			
		||||
          if ($field->getType() == 'list_string') {
 | 
			
		||||
            $data[$table_name][$field_name]['argument']['id'] = 'string_list_field';
 | 
			
		||||
          }
 | 
			
		||||
          else {
 | 
			
		||||
            $data[$table_name][$field_name]['argument']['id'] = 'number_list_field';
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return $data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,55 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\options\Plugin\Field\FieldFormatter;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Field\Attribute\FieldFormatter;
 | 
			
		||||
use Drupal\Core\Field\FieldFilteredMarkup;
 | 
			
		||||
use Drupal\Core\Field\FormatterBase;
 | 
			
		||||
use Drupal\Core\Field\FieldItemListInterface;
 | 
			
		||||
use Drupal\Core\Form\OptGroup;
 | 
			
		||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Plugin implementation of the 'list_default' formatter.
 | 
			
		||||
 */
 | 
			
		||||
#[FieldFormatter(
 | 
			
		||||
  id: 'list_default',
 | 
			
		||||
  label: new TranslatableMarkup('Default'),
 | 
			
		||||
  field_types: [
 | 
			
		||||
    'list_integer',
 | 
			
		||||
    'list_float',
 | 
			
		||||
    'list_string',
 | 
			
		||||
  ],
 | 
			
		||||
)]
 | 
			
		||||
class OptionsDefaultFormatter extends FormatterBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function viewElements(FieldItemListInterface $items, $langcode) {
 | 
			
		||||
    $elements = [];
 | 
			
		||||
 | 
			
		||||
    // Only collect allowed options if there are actually items to display.
 | 
			
		||||
    if ($items->count()) {
 | 
			
		||||
      $provider = $items->getFieldDefinition()
 | 
			
		||||
        ->getFieldStorageDefinition()
 | 
			
		||||
        ->getOptionsProvider('value', $items->getEntity());
 | 
			
		||||
      // Flatten the possible options, to support opt groups.
 | 
			
		||||
      $options = OptGroup::flattenOptions($provider->getPossibleOptions());
 | 
			
		||||
 | 
			
		||||
      foreach ($items as $delta => $item) {
 | 
			
		||||
        $value = $item->value;
 | 
			
		||||
        // If the stored value is in the current set of allowed values, display
 | 
			
		||||
        // the associated label, otherwise just display the raw value.
 | 
			
		||||
        $output = $options[$value] ?? $value;
 | 
			
		||||
        $elements[$delta] = [
 | 
			
		||||
          '#markup' => $output,
 | 
			
		||||
          '#allowed_tags' => FieldFilteredMarkup::allowedTags(),
 | 
			
		||||
        ];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return $elements;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,41 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\options\Plugin\Field\FieldFormatter;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Field\Attribute\FieldFormatter;
 | 
			
		||||
use Drupal\Core\Field\FieldFilteredMarkup;
 | 
			
		||||
use Drupal\Core\Field\FormatterBase;
 | 
			
		||||
use Drupal\Core\Field\FieldItemListInterface;
 | 
			
		||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Plugin implementation of the 'list_key' formatter.
 | 
			
		||||
 */
 | 
			
		||||
#[FieldFormatter(
 | 
			
		||||
  id: 'list_key',
 | 
			
		||||
  label: new TranslatableMarkup('Key'),
 | 
			
		||||
  field_types: [
 | 
			
		||||
    'list_integer',
 | 
			
		||||
    'list_float',
 | 
			
		||||
    'list_string',
 | 
			
		||||
  ],
 | 
			
		||||
)]
 | 
			
		||||
class OptionsKeyFormatter extends FormatterBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function viewElements(FieldItemListInterface $items, $langcode) {
 | 
			
		||||
    $elements = [];
 | 
			
		||||
 | 
			
		||||
    foreach ($items as $delta => $item) {
 | 
			
		||||
      $elements[$delta] = [
 | 
			
		||||
        '#markup' => $item->value,
 | 
			
		||||
        '#allowed_tags' => FieldFilteredMarkup::allowedTags(),
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return $elements;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,138 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\options\Plugin\Field\FieldType;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Field\Attribute\FieldType;
 | 
			
		||||
use Drupal\Core\Field\FieldFilteredMarkup;
 | 
			
		||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
 | 
			
		||||
use Drupal\Core\Form\FormStateInterface;
 | 
			
		||||
use Drupal\Core\Render\Element;
 | 
			
		||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
 | 
			
		||||
use Drupal\Core\TypedData\DataDefinition;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Plugin implementation of the 'list_float' field type.
 | 
			
		||||
 */
 | 
			
		||||
#[FieldType(
 | 
			
		||||
  id: "list_float",
 | 
			
		||||
  label: new TranslatableMarkup("List (float)"),
 | 
			
		||||
  description: [
 | 
			
		||||
    new TranslatableMarkup("Values stored are floating-point numbers"),
 | 
			
		||||
    new TranslatableMarkup("For example, 'Fraction': 0 => 0, .25 => 1/4, .75 => 3/4, 1 => 1"),
 | 
			
		||||
  ],
 | 
			
		||||
  category: "selection_list",
 | 
			
		||||
  weight: -10,
 | 
			
		||||
  no_ui: TRUE,
 | 
			
		||||
  default_widget: "options_select",
 | 
			
		||||
  default_formatter: "list_default",
 | 
			
		||||
)]
 | 
			
		||||
class ListFloatItem extends ListItemBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
 | 
			
		||||
    $properties['value'] = DataDefinition::create('float')
 | 
			
		||||
      ->setLabel(new TranslatableMarkup('Float value'))
 | 
			
		||||
      ->setRequired(TRUE);
 | 
			
		||||
 | 
			
		||||
    return $properties;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public static function schema(FieldStorageDefinitionInterface $field_definition) {
 | 
			
		||||
    return [
 | 
			
		||||
      'columns' => [
 | 
			
		||||
        'value' => [
 | 
			
		||||
          'type' => 'float',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
      'indexes' => [
 | 
			
		||||
        'value' => ['value'],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function allowedValuesDescription() {
 | 
			
		||||
    $description = '<p>' . $this->t('The name will be used in displayed options and edit forms. The value is the stored value, and must be numeric.') . '</p>';
 | 
			
		||||
    $description .= '<p>' . $this->t('Allowed HTML tags in labels: @tags', ['@tags' => FieldFilteredMarkup::displayAllowedTags()]) . '</p>';
 | 
			
		||||
    return $description;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static function extractAllowedValues($string, $has_data) {
 | 
			
		||||
    $values = parent::extractAllowedValues($string, $has_data);
 | 
			
		||||
    if ($values) {
 | 
			
		||||
      $keys = array_keys($values);
 | 
			
		||||
      $labels = array_values($values);
 | 
			
		||||
      $keys = array_map(function ($key) {
 | 
			
		||||
        // Float keys are represented as strings and need to be disambiguated
 | 
			
		||||
        // ('.5' is '0.5').
 | 
			
		||||
        return is_numeric($key) ? (string) (float) $key : $key;
 | 
			
		||||
      }, $keys);
 | 
			
		||||
 | 
			
		||||
      return array_combine($keys, $labels);
 | 
			
		||||
    }
 | 
			
		||||
    return $values;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static function validateAllowedValue($option) {
 | 
			
		||||
    if (!is_numeric($option)) {
 | 
			
		||||
      return new TranslatableMarkup('Allowed values list: each key must be a valid integer or decimal.');
 | 
			
		||||
    }
 | 
			
		||||
    return NULL;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public static function simplifyAllowedValues(array $structured_values) {
 | 
			
		||||
    $values = [];
 | 
			
		||||
    foreach ($structured_values as $item) {
 | 
			
		||||
      // Nested elements are embedded in the label.
 | 
			
		||||
      if (is_array($item['label'])) {
 | 
			
		||||
        $item['label'] = static::simplifyAllowedValues($item['label']);
 | 
			
		||||
      }
 | 
			
		||||
      // Cast the value to a float first so that .5 and 0.5 are the same value
 | 
			
		||||
      // and then cast to a string so that values like 0.5 can be used as array
 | 
			
		||||
      // keys.
 | 
			
		||||
      // @see http://php.net/manual/language.types.array.php
 | 
			
		||||
      $values[(string) (float) $item['value']] = $item['label'];
 | 
			
		||||
    }
 | 
			
		||||
    return $values;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static function castAllowedValue($value) {
 | 
			
		||||
    return (float) $value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
 | 
			
		||||
    $element = parent::storageSettingsForm($form, $form_state, $has_data);
 | 
			
		||||
 | 
			
		||||
    foreach (Element::children($element['allowed_values']['table']) as $delta => $row) {
 | 
			
		||||
      // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number
 | 
			
		||||
      // @see \Drupal\Core\Field\Plugin\Field\FieldWidget\NumberWidget::formElement()
 | 
			
		||||
      $element['allowed_values']['table'][$delta]['item']['key']['#step'] = 'any';
 | 
			
		||||
      $element['allowed_values']['table'][$delta]['item']['key']['#type'] = 'number';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return $element;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,98 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\options\Plugin\Field\FieldType;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Field\Attribute\FieldType;
 | 
			
		||||
use Drupal\Core\Field\FieldFilteredMarkup;
 | 
			
		||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
 | 
			
		||||
use Drupal\Core\Form\FormStateInterface;
 | 
			
		||||
use Drupal\Core\Render\Element;
 | 
			
		||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
 | 
			
		||||
use Drupal\Core\TypedData\DataDefinition;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Plugin implementation of the 'list_integer' field type.
 | 
			
		||||
 */
 | 
			
		||||
#[FieldType(
 | 
			
		||||
  id: "list_integer",
 | 
			
		||||
  label: new TranslatableMarkup("List (integer)"),
 | 
			
		||||
  description: [
 | 
			
		||||
    new TranslatableMarkup("Values stored are numbers without decimals"),
 | 
			
		||||
    new TranslatableMarkup("For example, 'Lifetime in days': 1 => 1 day, 7 => 1 week, 31 => 1 month"),
 | 
			
		||||
  ],
 | 
			
		||||
  category: "selection_list",
 | 
			
		||||
  weight: -30,
 | 
			
		||||
  default_widget: "options_select",
 | 
			
		||||
  default_formatter: "list_default",
 | 
			
		||||
)]
 | 
			
		||||
class ListIntegerItem extends ListItemBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
 | 
			
		||||
    $properties['value'] = DataDefinition::create('integer')
 | 
			
		||||
      ->setLabel(new TranslatableMarkup('Integer value'))
 | 
			
		||||
      ->setRequired(TRUE);
 | 
			
		||||
 | 
			
		||||
    return $properties;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public static function schema(FieldStorageDefinitionInterface $field_definition) {
 | 
			
		||||
    return [
 | 
			
		||||
      'columns' => [
 | 
			
		||||
        'value' => [
 | 
			
		||||
          'type' => 'int',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
      'indexes' => [
 | 
			
		||||
        'value' => ['value'],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function allowedValuesDescription() {
 | 
			
		||||
    $description = '<p>' . $this->t('The name will be used in displayed options and edit forms. The value is the stored value, and must be numeric.') . '</p>';
 | 
			
		||||
    $description .= '<p>' . $this->t('Allowed HTML tags in labels: @tags', ['@tags' => FieldFilteredMarkup::displayAllowedTags()]) . '</p>';
 | 
			
		||||
    return $description;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static function validateAllowedValue($option) {
 | 
			
		||||
    if (!preg_match('/^-?\d+$/', $option)) {
 | 
			
		||||
      return new TranslatableMarkup('Allowed values list: keys must be integers.');
 | 
			
		||||
    }
 | 
			
		||||
    return NULL;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static function castAllowedValue($value) {
 | 
			
		||||
    return (int) $value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
 | 
			
		||||
    $element = parent::storageSettingsForm($form, $form_state, $has_data);
 | 
			
		||||
 | 
			
		||||
    foreach (Element::children($element['allowed_values']['table']) as $delta => $row) {
 | 
			
		||||
      // @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number
 | 
			
		||||
      // @see \Drupal\Core\Field\Plugin\Field\FieldWidget\NumberWidget::formElement()
 | 
			
		||||
      $element['allowed_values']['table'][$delta]['item']['key']['#type'] = 'number';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return $element;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,576 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\options\Plugin\Field\FieldType;
 | 
			
		||||
 | 
			
		||||
use Drupal\Component\Utility\Html;
 | 
			
		||||
use Drupal\Component\Utility\NestedArray;
 | 
			
		||||
use Drupal\Core\Ajax\AjaxResponse;
 | 
			
		||||
use Drupal\Core\Ajax\FocusFirstCommand;
 | 
			
		||||
use Drupal\Core\Ajax\InsertCommand;
 | 
			
		||||
use Drupal\Core\Field\FieldDefinitionInterface;
 | 
			
		||||
use Drupal\Core\Field\FieldItemBase;
 | 
			
		||||
use Drupal\Core\Form\FormStateInterface;
 | 
			
		||||
use Drupal\Core\Form\OptGroup;
 | 
			
		||||
use Drupal\Core\Render\Element;
 | 
			
		||||
use Drupal\Core\Session\AccountInterface;
 | 
			
		||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
 | 
			
		||||
use Drupal\Core\TypedData\OptionsProviderInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Plugin base class inherited by the options field types.
 | 
			
		||||
 */
 | 
			
		||||
abstract class ListItemBase extends FieldItemBase implements OptionsProviderInterface {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public static function defaultStorageSettings() {
 | 
			
		||||
    return [
 | 
			
		||||
      'allowed_values' => [],
 | 
			
		||||
      'allowed_values_function' => '',
 | 
			
		||||
    ] + parent::defaultStorageSettings();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function getPossibleValues(?AccountInterface $account = NULL) {
 | 
			
		||||
    // Flatten options firstly, because Possible Options may contain group
 | 
			
		||||
    // arrays.
 | 
			
		||||
    $flatten_options = OptGroup::flattenOptions($this->getPossibleOptions($account));
 | 
			
		||||
    return array_keys($flatten_options);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function getPossibleOptions(?AccountInterface $account = NULL) {
 | 
			
		||||
    return $this->getSettableOptions($account);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function getSettableValues(?AccountInterface $account = NULL) {
 | 
			
		||||
    // Flatten options firstly, because Settable Options may contain group
 | 
			
		||||
    // arrays.
 | 
			
		||||
    $flatten_options = OptGroup::flattenOptions($this->getSettableOptions($account));
 | 
			
		||||
    return array_keys($flatten_options);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function getSettableOptions(?AccountInterface $account = NULL) {
 | 
			
		||||
    $allowed_options = options_allowed_values($this->getFieldDefinition()->getFieldStorageDefinition(), $this->getEntity());
 | 
			
		||||
    return $allowed_options;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
 | 
			
		||||
    $allowed_options = options_allowed_values($field_definition->getFieldStorageDefinition());
 | 
			
		||||
    if (empty($allowed_options)) {
 | 
			
		||||
      $values['value'] = NULL;
 | 
			
		||||
      return $values;
 | 
			
		||||
    }
 | 
			
		||||
    $values['value'] = array_rand($allowed_options);
 | 
			
		||||
    return $values;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function isEmpty() {
 | 
			
		||||
    $value = $this->get('value')->getValue();
 | 
			
		||||
 | 
			
		||||
    return empty($value) && (string) $value !== '0';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
 | 
			
		||||
    if (!array_key_exists('allowed_values', $form_state->getStorage())) {
 | 
			
		||||
      $form_state->set('allowed_values', $this->getFieldDefinition()->getSetting('allowed_values'));
 | 
			
		||||
    }
 | 
			
		||||
    $form['field_storage_submit']['#submit'][] = [static::class, 'submitFieldStorageUpdate'];
 | 
			
		||||
    $form['field_storage_submit']['#limit_validation_errors'] = [];
 | 
			
		||||
 | 
			
		||||
    $allowed_values = $form_state->getStorage()['allowed_values'];
 | 
			
		||||
    $allowed_values_function = $this->getSetting('allowed_values_function');
 | 
			
		||||
 | 
			
		||||
    if (!$form_state->get('items_count')) {
 | 
			
		||||
      $form_state->set('items_count', max(count($allowed_values), 0));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $wrapper_id = Html::getUniqueId('allowed-values-wrapper');
 | 
			
		||||
    $element['allowed_values'] = [
 | 
			
		||||
      '#element_validate' => [[static::class, 'validateAllowedValues']],
 | 
			
		||||
      '#field_has_data' => $has_data,
 | 
			
		||||
      '#allowed_values' => $allowed_values,
 | 
			
		||||
      '#required' => TRUE,
 | 
			
		||||
      '#prefix' => '<div id="' . $wrapper_id . '">',
 | 
			
		||||
      '#suffix' => '</div>',
 | 
			
		||||
      '#access' => empty($allowed_values_function),
 | 
			
		||||
      'help_text' => ['#markup' => $this->allowedValuesDescription()],
 | 
			
		||||
    ];
 | 
			
		||||
    $element['allowed_values']['table'] = [
 | 
			
		||||
      '#type' => 'table',
 | 
			
		||||
      '#header' => [
 | 
			
		||||
        $this->t('Allowed values'),
 | 
			
		||||
        $this->t('Delete'),
 | 
			
		||||
        $this->t('Weight'),
 | 
			
		||||
      ],
 | 
			
		||||
      '#attributes' => [
 | 
			
		||||
        'id' => 'allowed-values-order',
 | 
			
		||||
        'data-field-list-table' => TRUE,
 | 
			
		||||
        'class' => ['allowed-values-table'],
 | 
			
		||||
      ],
 | 
			
		||||
      '#tabledrag' => [
 | 
			
		||||
        [
 | 
			
		||||
          'action' => 'order',
 | 
			
		||||
          'relationship' => 'sibling',
 | 
			
		||||
          'group' => 'weight',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
      '#attached' => [
 | 
			
		||||
        'library' => [
 | 
			
		||||
          'core/drupal.fieldListKeyboardNavigation',
 | 
			
		||||
          'field_ui/drupal.field_ui',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $max = $form_state->get('items_count');
 | 
			
		||||
    $entity_type_id = $this->getFieldDefinition()->getTargetEntityTypeId();
 | 
			
		||||
    $field_name = $this->getFieldDefinition()->getName();
 | 
			
		||||
    $current_keys = array_keys($allowed_values);
 | 
			
		||||
    for ($delta = 0; $delta <= $max; $delta++) {
 | 
			
		||||
      $element['allowed_values']['table'][$delta] = [
 | 
			
		||||
        '#attributes' => [
 | 
			
		||||
          'class' => ['draggable'],
 | 
			
		||||
        ],
 | 
			
		||||
        '#weight' => $delta,
 | 
			
		||||
      ];
 | 
			
		||||
      $element['allowed_values']['table'][$delta]['item'] = [
 | 
			
		||||
        'label' => [
 | 
			
		||||
          '#type' => 'textfield',
 | 
			
		||||
          '#title' => $this->t('Name'),
 | 
			
		||||
          '#weight' => -30,
 | 
			
		||||
          '#default_value' => isset($current_keys[$delta]) ? $allowed_values[$current_keys[$delta]] : '',
 | 
			
		||||
          '#required' => $delta === 0,
 | 
			
		||||
        ],
 | 
			
		||||
        'key' => [
 | 
			
		||||
          '#type' => 'textfield',
 | 
			
		||||
          '#maxlength' => 255,
 | 
			
		||||
          '#title' => $this->t('Value'),
 | 
			
		||||
          '#default_value' => $current_keys[$delta] ?? '',
 | 
			
		||||
          '#weight' => -20,
 | 
			
		||||
          '#required' => $delta === 0,
 | 
			
		||||
        ],
 | 
			
		||||
      ];
 | 
			
		||||
      $element['allowed_values']['table'][$delta]['delete'] = [
 | 
			
		||||
        '#type' => 'submit',
 | 
			
		||||
        '#value' => $this->t('Remove'),
 | 
			
		||||
        '#name' => "remove_row_button__$delta",
 | 
			
		||||
        '#id' => "remove_row_button__$delta",
 | 
			
		||||
        '#delta' => $delta,
 | 
			
		||||
        '#submit' => [[static::class, 'deleteSubmit']],
 | 
			
		||||
        '#limit_validation_errors' => [],
 | 
			
		||||
        '#ajax' => [
 | 
			
		||||
          'callback' => [static::class, 'deleteAjax'],
 | 
			
		||||
          'wrapper' => $wrapper_id,
 | 
			
		||||
          'effect' => 'fade',
 | 
			
		||||
        ],
 | 
			
		||||
      ];
 | 
			
		||||
      $element['allowed_values']['table'][$delta]['weight'] = [
 | 
			
		||||
        '#type' => 'weight',
 | 
			
		||||
        '#title' => $this->t('Weight for row @number', ['@number' => $delta + 1]),
 | 
			
		||||
        '#title_display' => 'invisible',
 | 
			
		||||
        '#delta' => 50,
 | 
			
		||||
        '#default_value' => 0,
 | 
			
		||||
        '#attributes' => ['class' => ['weight']],
 | 
			
		||||
      ];
 | 
			
		||||
      // Disable the remove button if there is only one row in the table.
 | 
			
		||||
      if ($max === 0) {
 | 
			
		||||
        $element['allowed_values']['table'][0]['delete']['#attributes']['disabled'] = 'disabled';
 | 
			
		||||
      }
 | 
			
		||||
      if ($delta < count($allowed_values)) {
 | 
			
		||||
        $query = \Drupal::entityQuery($entity_type_id)
 | 
			
		||||
          ->accessCheck(FALSE)
 | 
			
		||||
          ->condition($field_name, $current_keys[$delta]);
 | 
			
		||||
        $entity_ids = $query->execute();
 | 
			
		||||
        if (!empty($entity_ids)) {
 | 
			
		||||
          $element['allowed_values']['table'][$delta]['item']['key']['#attributes']['disabled'] = 'disabled';
 | 
			
		||||
          $element['allowed_values']['table'][$delta]['delete']['#attributes']['disabled'] = 'disabled';
 | 
			
		||||
          $element['allowed_values']['table'][$delta]['delete'] += [
 | 
			
		||||
            'message' => [
 | 
			
		||||
              '#type' => 'item',
 | 
			
		||||
              '#markup' => $this->t('Cannot be removed: option in use.'),
 | 
			
		||||
            ],
 | 
			
		||||
          ];
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    $element['allowed_values']['table']['#max_delta'] = $max;
 | 
			
		||||
    $element['allowed_values']['add_more_allowed_values'] = [
 | 
			
		||||
      '#type' => 'submit',
 | 
			
		||||
      '#name' => 'add_more_allowed_values',
 | 
			
		||||
      '#value' => $this->t('Add another item'),
 | 
			
		||||
      '#attributes' => [
 | 
			
		||||
        'class' => ['field-add-more-submit'],
 | 
			
		||||
        'data-field-list-button' => TRUE,
 | 
			
		||||
      ],
 | 
			
		||||
      // Allow users to add another row without requiring existing rows to have
 | 
			
		||||
      // values.
 | 
			
		||||
      '#limit_validation_errors' => [],
 | 
			
		||||
      '#submit' => [[static::class, 'addMoreSubmit']],
 | 
			
		||||
      '#ajax' => [
 | 
			
		||||
        'callback' => [static::class, 'addMoreAjax'],
 | 
			
		||||
        'wrapper' => $wrapper_id,
 | 
			
		||||
        'effect' => 'fade',
 | 
			
		||||
        'progress' => [
 | 
			
		||||
          'type' => 'throbber',
 | 
			
		||||
          'message' => $this->t('Adding a new item...'),
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $element['allowed_values_function'] = [
 | 
			
		||||
      '#type' => 'item',
 | 
			
		||||
      '#title' => $this->t('Allowed values list'),
 | 
			
		||||
      '#markup' => $this->t('The value of this field is being determined by the %function function and may not be changed.', ['%function' => $allowed_values_function]),
 | 
			
		||||
      '#access' => !empty($allowed_values_function),
 | 
			
		||||
      '#value' => $allowed_values_function,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    return $element;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Adds a new option.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $form
 | 
			
		||||
   *   The form array to add elements to.
 | 
			
		||||
   * @param \Drupal\Core\Form\FormStateInterface $form_state
 | 
			
		||||
   *   The current state of the form.
 | 
			
		||||
   */
 | 
			
		||||
  public static function addMoreSubmit(array $form, FormStateInterface $form_state) {
 | 
			
		||||
    $form_state->set('items_count', $form_state->get('items_count') + 1);
 | 
			
		||||
    $form_state->setRebuild();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Ajax callback for the "Add another item" button.
 | 
			
		||||
   */
 | 
			
		||||
  public static function addMoreAjax(array $form, FormStateInterface $form_state) {
 | 
			
		||||
    $button = $form_state->getTriggeringElement();
 | 
			
		||||
 | 
			
		||||
    // Go one level up in the form.
 | 
			
		||||
    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
 | 
			
		||||
    $delta = $element['table']['#max_delta'];
 | 
			
		||||
    $element['table'][$delta]['item']['#prefix'] = '<div class="ajax-new-content" data-drupal-selector="field-list-add-more-focus-target">' . ($element['table'][$delta]['item']['#prefix'] ?? '');
 | 
			
		||||
    $element['table'][$delta]['item']['#suffix'] = ($element['table'][$delta]['item']['#suffix'] ?? '') . '</div>';
 | 
			
		||||
    // Enable the remove button for the first row if there are more rows.
 | 
			
		||||
    if ($delta > 0 && isset($element['table'][0]['delete']['#attributes']['disabled']) && !isset($element['table'][0]['item']['key']['#attributes']['disabled'])) {
 | 
			
		||||
      unset($element['table'][0]['delete']['#attributes']['disabled']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $response = new AjaxResponse();
 | 
			
		||||
    $response->addCommand(new InsertCommand(NULL, $element));
 | 
			
		||||
    $response->addCommand(new FocusFirstCommand('[data-drupal-selector="field-list-add-more-focus-target"]'));
 | 
			
		||||
 | 
			
		||||
    return $response;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Deletes a row/option.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $form
 | 
			
		||||
   *   The form array to add elements to.
 | 
			
		||||
   * @param \Drupal\Core\Form\FormStateInterface $form_state
 | 
			
		||||
   *   The current state of the form.
 | 
			
		||||
   */
 | 
			
		||||
  public static function deleteSubmit(array $form, FormStateInterface $form_state) {
 | 
			
		||||
    $allowed_values = $form_state->getStorage()['allowed_values'];
 | 
			
		||||
    $button = $form_state->getTriggeringElement();
 | 
			
		||||
    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
 | 
			
		||||
    $item_to_be_removed = $element['item']['label']['#default_value'];
 | 
			
		||||
    $remaining_allowed_values = array_diff($allowed_values, [$item_to_be_removed]);
 | 
			
		||||
    $form_state->set('allowed_values', $remaining_allowed_values);
 | 
			
		||||
 | 
			
		||||
    // The user input is directly modified to preserve the rest of the data on
 | 
			
		||||
    // the page as it cannot be rebuilt from a fresh form state.
 | 
			
		||||
    $user_input = $form_state->getUserInput();
 | 
			
		||||
    NestedArray::unsetValue($user_input, $element['#parents']);
 | 
			
		||||
 | 
			
		||||
    // Reset the keys in the array.
 | 
			
		||||
    $table_parents = $element['#parents'];
 | 
			
		||||
    array_pop($table_parents);
 | 
			
		||||
    $new_values = array_values(NestedArray::getValue($user_input, $table_parents));
 | 
			
		||||
    NestedArray::setValue($user_input, $table_parents, $new_values);
 | 
			
		||||
 | 
			
		||||
    $form_state->setUserInput($user_input);
 | 
			
		||||
    $form_state->set('items_count', $form_state->get('items_count') - 1);
 | 
			
		||||
 | 
			
		||||
    $form_state->setRebuild();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Ajax callback for per row delete button.
 | 
			
		||||
   */
 | 
			
		||||
  public static function deleteAjax(array $form, FormStateInterface $form_state) {
 | 
			
		||||
    $button = $form_state->getTriggeringElement();
 | 
			
		||||
 | 
			
		||||
    return NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -3));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Provides the field type specific allowed values form element #description.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   The field type allowed values form specific description.
 | 
			
		||||
   */
 | 
			
		||||
  abstract protected function allowedValuesDescription();
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Render API callback: Validates the allowed values of an options field.
 | 
			
		||||
   *
 | 
			
		||||
   * This function is assigned as a #element_validate callback.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $element
 | 
			
		||||
   *   An associative array containing the properties and children of the
 | 
			
		||||
   *   generic form element.
 | 
			
		||||
   * @param \Drupal\Core\Form\FormStateInterface $form_state
 | 
			
		||||
   *   The current state of the form for the form this element belongs to.
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\Core\Render\Element\FormElementBase::processPattern()
 | 
			
		||||
   */
 | 
			
		||||
  public static function validateAllowedValues($element, FormStateInterface $form_state) {
 | 
			
		||||
    $items = array_filter(array_map(function ($item) use ($element) {
 | 
			
		||||
      $current_element = $element['table'][$item];
 | 
			
		||||
      $key_has_input = isset($current_element['item']['key']['#value']) && $current_element['item']['key']['#value'] !== '';
 | 
			
		||||
      $label_has_input = isset($current_element['item']['label']['#value']) && $current_element['item']['label']['#value'] !== '';
 | 
			
		||||
      if ($key_has_input) {
 | 
			
		||||
        if ($label_has_input) {
 | 
			
		||||
          return $current_element['item']['key']['#value'] . '|' . $current_element['item']['label']['#value'];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $current_element['item']['key']['#value'];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if ($label_has_input) {
 | 
			
		||||
        return $current_element['item']['label']['#value'];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return NULL;
 | 
			
		||||
    }, Element::children($element['table'])), function ($item) {
 | 
			
		||||
      return $item;
 | 
			
		||||
    });
 | 
			
		||||
    if ($reordered_items = $form_state->getValue([...$element['#parents'], 'table'])) {
 | 
			
		||||
      uksort($items, function ($a, $b) use ($reordered_items) {
 | 
			
		||||
        $a_weight = $reordered_items[$a]['weight'] ?? 0;
 | 
			
		||||
        $b_weight = $reordered_items[$b]['weight'] ?? 0;
 | 
			
		||||
        return $a_weight <=> $b_weight;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    $values = static::extractAllowedValues($items, $element['#field_has_data']);
 | 
			
		||||
 | 
			
		||||
    if (!is_array($values)) {
 | 
			
		||||
      $form_state->setError($element, new TranslatableMarkup('Allowed values list: invalid input.'));
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      // Check that keys are valid for the field type.
 | 
			
		||||
      foreach ($values as $key => $value) {
 | 
			
		||||
        if ($error = static::validateAllowedValue($key)) {
 | 
			
		||||
          $form_state->setError($element, $error);
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      $form_state->setValueForElement($element, $values);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Extracts the allowed values array from the allowed_values element.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $list
 | 
			
		||||
   *   The raw string or array to extract values from.
 | 
			
		||||
   * @param bool $has_data
 | 
			
		||||
   *   The current field already has data inserted or not.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array|null
 | 
			
		||||
   *   The array of extracted key/value pairs, or NULL if the string is invalid.
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\options\Plugin\Field\FieldType\ListItemBase::allowedValuesString()
 | 
			
		||||
   */
 | 
			
		||||
  protected static function extractAllowedValues(array $list, bool $has_data) {
 | 
			
		||||
    $values = [];
 | 
			
		||||
 | 
			
		||||
    $generated_keys = $explicit_keys = FALSE;
 | 
			
		||||
    foreach ($list as $position => $text) {
 | 
			
		||||
      // Check for an explicit key.
 | 
			
		||||
      $matches = [];
 | 
			
		||||
      if (preg_match('/(.*)\|(.*)/', $text, $matches)) {
 | 
			
		||||
        // Trim key and value to avoid unwanted spaces issues.
 | 
			
		||||
        $key = trim($matches[1]);
 | 
			
		||||
        $value = trim($matches[2]);
 | 
			
		||||
        $explicit_keys = TRUE;
 | 
			
		||||
      }
 | 
			
		||||
      // Otherwise see if we can use the value as the key.
 | 
			
		||||
      elseif (!static::validateAllowedValue($text)) {
 | 
			
		||||
        $key = $value = $text;
 | 
			
		||||
        $explicit_keys = TRUE;
 | 
			
		||||
      }
 | 
			
		||||
      // Otherwise see if we can generate a key from the position.
 | 
			
		||||
      elseif (!$has_data) {
 | 
			
		||||
        $key = (string) $position;
 | 
			
		||||
        $value = $text;
 | 
			
		||||
        $generated_keys = TRUE;
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      $values[$key] = $value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // We generate keys only if the list contains no explicit key at all.
 | 
			
		||||
    if ($explicit_keys && $generated_keys) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return $values;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Checks whether a candidate allowed value is valid.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $option
 | 
			
		||||
   *   The option value entered by the user.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\StringTranslation\TranslatableMarkup|string|null
 | 
			
		||||
   *   The error message if the specified value is invalid, NULL otherwise.
 | 
			
		||||
   */
 | 
			
		||||
  protected static function validateAllowedValue($option) {
 | 
			
		||||
    return NULL;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Generates a string representation of an array of 'allowed values'.
 | 
			
		||||
   *
 | 
			
		||||
   * This string format is suitable for edition in a textarea.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $values
 | 
			
		||||
   *   An array of values, where array keys are values and array values are
 | 
			
		||||
   *   labels.
 | 
			
		||||
   *
 | 
			
		||||
   * @return string
 | 
			
		||||
   *   The string representation of the $values array:
 | 
			
		||||
   *    - Values are separated by a carriage return.
 | 
			
		||||
   *    - Each value is in the format "value|label" or "value".
 | 
			
		||||
   */
 | 
			
		||||
  protected function allowedValuesString($values) {
 | 
			
		||||
    $lines = [];
 | 
			
		||||
    foreach ($values as $key => $value) {
 | 
			
		||||
      $lines[] = "$key|$value";
 | 
			
		||||
    }
 | 
			
		||||
    return implode("\n", $lines);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public static function storageSettingsToConfigData(array $settings) {
 | 
			
		||||
    if (isset($settings['allowed_values'])) {
 | 
			
		||||
      $settings['allowed_values'] = static::structureAllowedValues($settings['allowed_values']);
 | 
			
		||||
    }
 | 
			
		||||
    return $settings;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public static function storageSettingsFromConfigData(array $settings) {
 | 
			
		||||
    if (isset($settings['allowed_values'])) {
 | 
			
		||||
      $settings['allowed_values'] = static::simplifyAllowedValues($settings['allowed_values']);
 | 
			
		||||
    }
 | 
			
		||||
    return $settings;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Simplifies allowed values to a key-value array from the structured array.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $structured_values
 | 
			
		||||
   *   Array of items with a 'value' and 'label' key each for the allowed
 | 
			
		||||
   *   values.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array
 | 
			
		||||
   *   Allowed values were the array key is the 'value' value, the value is
 | 
			
		||||
   *   the 'label' value.
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\options\Plugin\Field\FieldType\ListItemBase::structureAllowedValues()
 | 
			
		||||
   */
 | 
			
		||||
  protected static function simplifyAllowedValues(array $structured_values) {
 | 
			
		||||
    $values = [];
 | 
			
		||||
    foreach ($structured_values as $item) {
 | 
			
		||||
      if (is_array($item['label'])) {
 | 
			
		||||
        // Nested elements are embedded in the label.
 | 
			
		||||
        $item['label'] = static::simplifyAllowedValues($item['label']);
 | 
			
		||||
      }
 | 
			
		||||
      $values[$item['value']] = $item['label'];
 | 
			
		||||
    }
 | 
			
		||||
    return $values;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a structured array of allowed values from a key-value array.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $values
 | 
			
		||||
   *   Allowed values were the array key is the 'value' value, the value is
 | 
			
		||||
   *   the 'label' value.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array
 | 
			
		||||
   *   Array of items with a 'value' and 'label' key each for the allowed
 | 
			
		||||
   *   values.
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\options\Plugin\Field\FieldType\ListItemBase::simplifyAllowedValues()
 | 
			
		||||
   */
 | 
			
		||||
  protected static function structureAllowedValues(array $values) {
 | 
			
		||||
    $structured_values = [];
 | 
			
		||||
    foreach ($values as $value => $label) {
 | 
			
		||||
      if (is_array($label)) {
 | 
			
		||||
        $label = static::structureAllowedValues($label);
 | 
			
		||||
      }
 | 
			
		||||
      $structured_values[] = [
 | 
			
		||||
        'value' => static::castAllowedValue($value),
 | 
			
		||||
        'label' => $label,
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
    return $structured_values;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Converts a value to the correct type.
 | 
			
		||||
   *
 | 
			
		||||
   * @param mixed $value
 | 
			
		||||
   *   The value to cast.
 | 
			
		||||
   *
 | 
			
		||||
   * @return mixed
 | 
			
		||||
   *   The casted value.
 | 
			
		||||
   */
 | 
			
		||||
  protected static function castAllowedValue($value) {
 | 
			
		||||
    return $value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Resets the static variable on field storage update.
 | 
			
		||||
   */
 | 
			
		||||
  public static function submitFieldStorageUpdate() {
 | 
			
		||||
    drupal_static_reset('options_allowed_values');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,144 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\options\Plugin\Field\FieldType;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Field\Attribute\FieldType;
 | 
			
		||||
use Drupal\Core\Field\FieldFilteredMarkup;
 | 
			
		||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
 | 
			
		||||
use Drupal\Core\Form\FormStateInterface;
 | 
			
		||||
use Drupal\Core\Render\Element;
 | 
			
		||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
 | 
			
		||||
use Drupal\Core\TypedData\DataDefinition;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Plugin implementation of the 'list_string' field type.
 | 
			
		||||
 */
 | 
			
		||||
#[FieldType(
 | 
			
		||||
  id: "list_string",
 | 
			
		||||
  label: new TranslatableMarkup("List (text)"),
 | 
			
		||||
  description: [
 | 
			
		||||
    new TranslatableMarkup("Values stored are text values"),
 | 
			
		||||
    new TranslatableMarkup("For example, 'US States': IL => Illinois, IA => Iowa, IN => Indiana"),
 | 
			
		||||
  ],
 | 
			
		||||
  category: "selection_list",
 | 
			
		||||
  weight: -50,
 | 
			
		||||
  default_widget: "options_select",
 | 
			
		||||
  default_formatter: "list_default",
 | 
			
		||||
)]
 | 
			
		||||
class ListStringItem extends ListItemBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
 | 
			
		||||
    $properties['value'] = DataDefinition::create('string')
 | 
			
		||||
      ->setLabel(new TranslatableMarkup('Text value'))
 | 
			
		||||
      ->addConstraint('Length', ['max' => 255])
 | 
			
		||||
      ->setRequired(TRUE);
 | 
			
		||||
 | 
			
		||||
    return $properties;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public static function schema(FieldStorageDefinitionInterface $field_definition) {
 | 
			
		||||
    return [
 | 
			
		||||
      'columns' => [
 | 
			
		||||
        'value' => [
 | 
			
		||||
          'type' => 'varchar',
 | 
			
		||||
          'length' => 255,
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
      'indexes' => [
 | 
			
		||||
        'value' => ['value'],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function allowedValuesDescription() {
 | 
			
		||||
    $description = '<p>' . $this->t('The name will be used in displayed options and edit forms.');
 | 
			
		||||
    $description .= '<br/>' . $this->t('The value is automatically generated machine name of the name provided and will be the stored value.');
 | 
			
		||||
    $description .= '</p>';
 | 
			
		||||
    $description .= '<p>' . $this->t('Allowed HTML tags in labels: @tags', ['@tags' => FieldFilteredMarkup::displayAllowedTags()]) . '</p>';
 | 
			
		||||
    return $description;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static function validateAllowedValue($option) {
 | 
			
		||||
    if (mb_strlen($option) > 255) {
 | 
			
		||||
      return new TranslatableMarkup('Allowed values list: each key must be a string at most 255 characters long.');
 | 
			
		||||
    }
 | 
			
		||||
    return NULL;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static function castAllowedValue($value) {
 | 
			
		||||
    return (string) $value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
 | 
			
		||||
    $element = parent::storageSettingsForm($form, $form_state, $has_data);
 | 
			
		||||
 | 
			
		||||
    // Improve user experience by using an automatically generated machine name.
 | 
			
		||||
    foreach (Element::children($element['allowed_values']['table']) as $delta => $row) {
 | 
			
		||||
      $element['allowed_values']['table'][$delta]['item']['key']['#type'] = 'machine_name';
 | 
			
		||||
      // ListItemBase::storageSettingsForm() will set the default value to an
 | 
			
		||||
      // integer if the key is a decimal integer string, so cast it back here.
 | 
			
		||||
      $element['allowed_values']['table'][$delta]['item']['key']['#default_value'] = (string) $element['allowed_values']['table'][$delta]['item']['key']['#default_value'];
 | 
			
		||||
      $element['allowed_values']['table'][$delta]['item']['key']['#machine_name'] = [
 | 
			
		||||
        'exists' => [static::class, 'exists'],
 | 
			
		||||
      ];
 | 
			
		||||
      $element['allowed_values']['table'][$delta]['item']['key']['#process'] = array_merge(
 | 
			
		||||
        [[static::class, 'processAllowedValuesKey']],
 | 
			
		||||
        // Workaround for https://drupal.org/i/1300290#comment-12873635.
 | 
			
		||||
        \Drupal::service('plugin.manager.element_info')->getInfoProperty('machine_name', '#process', []),
 | 
			
		||||
      );
 | 
			
		||||
      // Remove #element_validate from the machine name so that any value can be
 | 
			
		||||
      // used as a key, while keeping the widget's behavior for generating
 | 
			
		||||
      // defaults the same.
 | 
			
		||||
      $element['allowed_values']['table'][$delta]['item']['key']['#element_validate'] = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return $element;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Sets the machine name source to be the label.
 | 
			
		||||
   */
 | 
			
		||||
  public static function processAllowedValuesKey(array &$element): array {
 | 
			
		||||
    $parents = $element['#parents'];
 | 
			
		||||
    array_pop($parents);
 | 
			
		||||
    $parents[] = 'label';
 | 
			
		||||
    $element['#machine_name']['source'] = $parents;
 | 
			
		||||
 | 
			
		||||
    // Override the default description which is not applicable to this use of
 | 
			
		||||
    // the machine name element given that it allows users to manually enter
 | 
			
		||||
    // characters usually not allowed in machine names.
 | 
			
		||||
    if (!isset($element['#description'])) {
 | 
			
		||||
      $element['#description'] = '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return $element;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Checks for existing keys for allowed values.
 | 
			
		||||
   */
 | 
			
		||||
  public static function exists(): bool {
 | 
			
		||||
    // Without access to the current form state, we cannot know if a given key
 | 
			
		||||
    // is in use. Return FALSE in all cases.
 | 
			
		||||
    return FALSE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,18 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\options\Plugin\migrate\field\d6;
 | 
			
		||||
 | 
			
		||||
use Drupal\migrate_drupal\Attribute\MigrateField;
 | 
			
		||||
use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase;
 | 
			
		||||
 | 
			
		||||
// cspell:ignore optionwidgets
 | 
			
		||||
/**
 | 
			
		||||
 * MigrateField Plugin for Drupal 6 options fields.
 | 
			
		||||
 */
 | 
			
		||||
#[MigrateField(
 | 
			
		||||
  id: 'optionwidgets',
 | 
			
		||||
  core: [6],
 | 
			
		||||
  source_module: 'optionwidgets',
 | 
			
		||||
  destination_module: 'options',
 | 
			
		||||
)]
 | 
			
		||||
class OptionWidgetsField extends FieldPluginBase {}
 | 
			
		||||
@ -0,0 +1,23 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\options\Plugin\migrate\field\d7;
 | 
			
		||||
 | 
			
		||||
use Drupal\migrate_drupal\Attribute\MigrateField;
 | 
			
		||||
use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Migrate field plugin for Drupal 7 list fields.
 | 
			
		||||
 */
 | 
			
		||||
#[MigrateField(
 | 
			
		||||
  id: 'list',
 | 
			
		||||
  core: [7],
 | 
			
		||||
  type_map: [
 | 
			
		||||
    'list_boolean' => 'boolean',
 | 
			
		||||
    'list_integer' => 'list_integer',
 | 
			
		||||
    'list_text' => 'list_string',
 | 
			
		||||
    'list_float' => 'list_float',
 | 
			
		||||
  ],
 | 
			
		||||
  source_module: 'list',
 | 
			
		||||
  destination_module: 'options',
 | 
			
		||||
)]
 | 
			
		||||
class ListField extends FieldPluginBase {}
 | 
			
		||||
@ -0,0 +1,17 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\options\Plugin\migrate\field\d7;
 | 
			
		||||
 | 
			
		||||
use Drupal\migrate_drupal\Attribute\MigrateField;
 | 
			
		||||
use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Migrate field plugin for Drupal 7 options fields.
 | 
			
		||||
 */
 | 
			
		||||
#[MigrateField(
 | 
			
		||||
  id: 'options',
 | 
			
		||||
  core: [7],
 | 
			
		||||
  source_module: 'options',
 | 
			
		||||
  destination_module: 'options',
 | 
			
		||||
)]
 | 
			
		||||
class OptionsField extends FieldPluginBase {}
 | 
			
		||||
@ -0,0 +1,85 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\options\Plugin\views\argument;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Field\FieldFilteredMarkup;
 | 
			
		||||
use Drupal\Core\Form\FormStateInterface;
 | 
			
		||||
use Drupal\views\Attribute\ViewsArgument;
 | 
			
		||||
use Drupal\views\FieldAPIHandlerTrait;
 | 
			
		||||
use Drupal\views\ViewExecutable;
 | 
			
		||||
use Drupal\views\Plugin\views\display\DisplayPluginBase;
 | 
			
		||||
use Drupal\views\Plugin\views\argument\NumericArgument;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Argument handler for list field to show human readable name in the summary.
 | 
			
		||||
 *
 | 
			
		||||
 * @ingroup views_argument_handlers
 | 
			
		||||
 */
 | 
			
		||||
#[ViewsArgument(
 | 
			
		||||
  id: 'number_list_field',
 | 
			
		||||
)]
 | 
			
		||||
class NumberListField extends NumericArgument {
 | 
			
		||||
 | 
			
		||||
  use FieldAPIHandlerTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Stores the allowed values of this field.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $allowedValues = NULL;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$options = NULL) {
 | 
			
		||||
    parent::init($view, $display, $options);
 | 
			
		||||
 | 
			
		||||
    $field_storage = $this->getFieldStorageDefinition();
 | 
			
		||||
    $this->allowedValues = options_allowed_values($field_storage);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function defineOptions() {
 | 
			
		||||
    $options = parent::defineOptions();
 | 
			
		||||
    $options['summary']['contains']['human'] = ['default' => FALSE];
 | 
			
		||||
 | 
			
		||||
    return $options;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
 | 
			
		||||
    parent::buildOptionsForm($form, $form_state);
 | 
			
		||||
 | 
			
		||||
    $form['summary']['human'] = [
 | 
			
		||||
      '#title' => $this->t('Display list value as human readable'),
 | 
			
		||||
      '#type' => 'checkbox',
 | 
			
		||||
      '#default_value' => $this->options['summary']['human'],
 | 
			
		||||
      '#states' => [
 | 
			
		||||
        'visible' => [
 | 
			
		||||
          ':input[name="options[default_action]"]' => ['value' => 'summary'],
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function summaryName($data) {
 | 
			
		||||
    $value = $data->{$this->name_alias};
 | 
			
		||||
    // If the list element has a human readable name show it.
 | 
			
		||||
    if (isset($this->allowedValues[$value]) && !empty($this->options['summary']['human'])) {
 | 
			
		||||
      return FieldFilteredMarkup::create($this->allowedValues[$value]);
 | 
			
		||||
    }
 | 
			
		||||
    // Else, fallback to the key.
 | 
			
		||||
    else {
 | 
			
		||||
      return $value;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,83 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\options\Plugin\views\argument;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Field\FieldFilteredMarkup;
 | 
			
		||||
use Drupal\Core\Form\FormStateInterface;
 | 
			
		||||
use Drupal\views\Attribute\ViewsArgument;
 | 
			
		||||
use Drupal\views\FieldAPIHandlerTrait;
 | 
			
		||||
use Drupal\views\ViewExecutable;
 | 
			
		||||
use Drupal\views\Plugin\views\display\DisplayPluginBase;
 | 
			
		||||
use Drupal\views\Plugin\views\argument\StringArgument;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Argument handler for list field to show the human readable name in summary.
 | 
			
		||||
 *
 | 
			
		||||
 * @ingroup views_argument_handlers
 | 
			
		||||
 */
 | 
			
		||||
#[ViewsArgument(
 | 
			
		||||
  id: 'string_list_field',
 | 
			
		||||
)]
 | 
			
		||||
class StringListField extends StringArgument {
 | 
			
		||||
 | 
			
		||||
  use FieldAPIHandlerTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Stores the allowed values of this field.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $allowedValues = NULL;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$options = NULL) {
 | 
			
		||||
    parent::init($view, $display, $options);
 | 
			
		||||
 | 
			
		||||
    $field_storage = $this->getFieldStorageDefinition();
 | 
			
		||||
    $this->allowedValues = options_allowed_values($field_storage);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function defineOptions() {
 | 
			
		||||
    $options = parent::defineOptions();
 | 
			
		||||
 | 
			
		||||
    $options['summary']['contains']['human'] = ['default' => FALSE];
 | 
			
		||||
 | 
			
		||||
    return $options;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
 | 
			
		||||
    parent::buildOptionsForm($form, $form_state);
 | 
			
		||||
 | 
			
		||||
    $form['summary']['human'] = [
 | 
			
		||||
      '#title' => $this->t('Display list value as human readable'),
 | 
			
		||||
      '#type' => 'checkbox',
 | 
			
		||||
      '#default_value' => $this->options['summary']['human'],
 | 
			
		||||
      '#states' => [
 | 
			
		||||
        'visible' => [
 | 
			
		||||
          ':input[name="options[default_action]"]' => ['value' => 'summary'],
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function summaryName($data) {
 | 
			
		||||
    $value = $data->{$this->name_alias};
 | 
			
		||||
    // If the list element has a human readable name show it.
 | 
			
		||||
    if (isset($this->allowedValues[$value]) && !empty($this->options['summary']['human'])) {
 | 
			
		||||
      $value = $this->allowedValues[$value];
 | 
			
		||||
    }
 | 
			
		||||
    return FieldFilteredMarkup::create($this->caseTransform($value, $this->options['case']));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,32 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Drupal\options\Plugin\views\filter;
 | 
			
		||||
 | 
			
		||||
use Drupal\views\Attribute\ViewsFilter;
 | 
			
		||||
use Drupal\views\FieldAPIHandlerTrait;
 | 
			
		||||
use Drupal\views\Plugin\views\display\DisplayPluginBase;
 | 
			
		||||
use Drupal\views\Plugin\views\filter\ManyToOne;
 | 
			
		||||
use Drupal\views\ViewExecutable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Filter handler which uses list-fields as options.
 | 
			
		||||
 *
 | 
			
		||||
 * @ingroup views_filter_handlers
 | 
			
		||||
 */
 | 
			
		||||
#[ViewsFilter("list_field")]
 | 
			
		||||
class ListField extends ManyToOne {
 | 
			
		||||
 | 
			
		||||
  use FieldAPIHandlerTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$options = NULL) {
 | 
			
		||||
    parent::init($view, $display, $options);
 | 
			
		||||
 | 
			
		||||
    $field_storage = $this->getFieldStorageDefinition();
 | 
			
		||||
    // Set valueOptions here so getValueOptions() will just return it.
 | 
			
		||||
    $this->valueOptions = options_allowed_values($field_storage);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,63 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  config:
 | 
			
		||||
    - field.field.node.options_install_test.body
 | 
			
		||||
    - node.type.options_install_test
 | 
			
		||||
  module:
 | 
			
		||||
    - text
 | 
			
		||||
third_party_settings: {  }
 | 
			
		||||
id: node.options_install_test.default
 | 
			
		||||
targetEntityType: node
 | 
			
		||||
bundle: options_install_test
 | 
			
		||||
mode: default
 | 
			
		||||
content:
 | 
			
		||||
  title:
 | 
			
		||||
    type: string_textfield
 | 
			
		||||
    weight: -5
 | 
			
		||||
    region: content
 | 
			
		||||
    settings:
 | 
			
		||||
      size: 60
 | 
			
		||||
      placeholder: ''
 | 
			
		||||
    third_party_settings: {  }
 | 
			
		||||
  uid:
 | 
			
		||||
    type: entity_reference_autocomplete
 | 
			
		||||
    weight: 5
 | 
			
		||||
    region: content
 | 
			
		||||
    settings:
 | 
			
		||||
      match_operator: CONTAINS
 | 
			
		||||
      match_limit: 10
 | 
			
		||||
      size: 60
 | 
			
		||||
      placeholder: ''
 | 
			
		||||
    third_party_settings: {  }
 | 
			
		||||
  created:
 | 
			
		||||
    type: datetime_timestamp
 | 
			
		||||
    weight: 10
 | 
			
		||||
    region: content
 | 
			
		||||
    settings: {  }
 | 
			
		||||
    third_party_settings: {  }
 | 
			
		||||
  promote:
 | 
			
		||||
    type: boolean_checkbox
 | 
			
		||||
    weight: 15
 | 
			
		||||
    region: content
 | 
			
		||||
    settings:
 | 
			
		||||
      display_label: true
 | 
			
		||||
    third_party_settings: {  }
 | 
			
		||||
  sticky:
 | 
			
		||||
    type: boolean_checkbox
 | 
			
		||||
    weight: 16
 | 
			
		||||
    region: content
 | 
			
		||||
    settings:
 | 
			
		||||
      display_label: true
 | 
			
		||||
    third_party_settings: {  }
 | 
			
		||||
  body:
 | 
			
		||||
    type: text_textarea_with_summary
 | 
			
		||||
    weight: 26
 | 
			
		||||
    region: content
 | 
			
		||||
    settings:
 | 
			
		||||
      rows: 9
 | 
			
		||||
      summary_rows: 3
 | 
			
		||||
      placeholder: ''
 | 
			
		||||
      show_summary: false
 | 
			
		||||
    third_party_settings: {  }
 | 
			
		||||
hidden: {  }
 | 
			
		||||
@ -0,0 +1,27 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  config:
 | 
			
		||||
    - field.field.node.options_install_test.body
 | 
			
		||||
    - node.type.options_install_test
 | 
			
		||||
  module:
 | 
			
		||||
    - text
 | 
			
		||||
    - user
 | 
			
		||||
third_party_settings: {  }
 | 
			
		||||
id: node.options_install_test.default
 | 
			
		||||
targetEntityType: node
 | 
			
		||||
bundle: options_install_test
 | 
			
		||||
mode: default
 | 
			
		||||
content:
 | 
			
		||||
  links:
 | 
			
		||||
    weight: 100
 | 
			
		||||
    region: content
 | 
			
		||||
  body:
 | 
			
		||||
    type: text_default
 | 
			
		||||
    label: hidden
 | 
			
		||||
    settings: {  }
 | 
			
		||||
    third_party_settings: {  }
 | 
			
		||||
    weight: 101
 | 
			
		||||
    region: content
 | 
			
		||||
hidden:
 | 
			
		||||
  langcode: true
 | 
			
		||||
@ -0,0 +1,29 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  config:
 | 
			
		||||
    - core.entity_view_mode.node.teaser
 | 
			
		||||
    - field.field.node.options_install_test.body
 | 
			
		||||
    - node.type.options_install_test
 | 
			
		||||
  module:
 | 
			
		||||
    - text
 | 
			
		||||
    - user
 | 
			
		||||
third_party_settings: {  }
 | 
			
		||||
id: node.options_install_test.teaser
 | 
			
		||||
targetEntityType: node
 | 
			
		||||
bundle: options_install_test
 | 
			
		||||
mode: teaser
 | 
			
		||||
content:
 | 
			
		||||
  links:
 | 
			
		||||
    weight: 100
 | 
			
		||||
    region: content
 | 
			
		||||
  body:
 | 
			
		||||
    type: text_summary_or_trimmed
 | 
			
		||||
    label: hidden
 | 
			
		||||
    settings:
 | 
			
		||||
      trim_length: 600
 | 
			
		||||
    third_party_settings: {  }
 | 
			
		||||
    weight: 101
 | 
			
		||||
    region: content
 | 
			
		||||
hidden:
 | 
			
		||||
  langcode: true
 | 
			
		||||
@ -0,0 +1,21 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  config:
 | 
			
		||||
    - field.storage.node.body
 | 
			
		||||
    - node.type.options_install_test
 | 
			
		||||
third_party_settings: {  }
 | 
			
		||||
id: node.options_install_test.body
 | 
			
		||||
field_name: body
 | 
			
		||||
entity_type: node
 | 
			
		||||
bundle: options_install_test
 | 
			
		||||
label: Body
 | 
			
		||||
description: ''
 | 
			
		||||
required: false
 | 
			
		||||
translatable: true
 | 
			
		||||
default_value: {  }
 | 
			
		||||
default_value_callback: ''
 | 
			
		||||
settings:
 | 
			
		||||
  display_summary: true
 | 
			
		||||
  allowed_formats: {  }
 | 
			
		||||
field_type: text_with_summary
 | 
			
		||||
@ -0,0 +1,19 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  config:
 | 
			
		||||
    - field.storage.node.field_options_float
 | 
			
		||||
    - node.type.options_install_test
 | 
			
		||||
third_party_settings: {  }
 | 
			
		||||
id: node.options_install_test.field_options_float
 | 
			
		||||
field_name: field_options_float
 | 
			
		||||
entity_type: node
 | 
			
		||||
bundle: options_install_test
 | 
			
		||||
label: field_options_float
 | 
			
		||||
description: ''
 | 
			
		||||
required: false
 | 
			
		||||
translatable: true
 | 
			
		||||
default_value: {  }
 | 
			
		||||
default_value_callback: ''
 | 
			
		||||
settings: {  }
 | 
			
		||||
field_type: list_float
 | 
			
		||||
@ -0,0 +1,26 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  module:
 | 
			
		||||
    - node
 | 
			
		||||
    - options
 | 
			
		||||
id: node.field_options_float
 | 
			
		||||
field_name: field_options_float
 | 
			
		||||
entity_type: node
 | 
			
		||||
type: list_float
 | 
			
		||||
settings:
 | 
			
		||||
  allowed_values:
 | 
			
		||||
    -
 | 
			
		||||
      value: !!float 0
 | 
			
		||||
      label: Zero
 | 
			
		||||
    -
 | 
			
		||||
      value: 0.5
 | 
			
		||||
      label: 'Point five'
 | 
			
		||||
  allowed_values_function: ''
 | 
			
		||||
module: options
 | 
			
		||||
locked: false
 | 
			
		||||
cardinality: 1
 | 
			
		||||
translatable: true
 | 
			
		||||
indexes: {  }
 | 
			
		||||
persist_with_no_fields: false
 | 
			
		||||
custom_storage: false
 | 
			
		||||
@ -0,0 +1,11 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies: {  }
 | 
			
		||||
third_party_settings: {  }
 | 
			
		||||
name: options_install_test
 | 
			
		||||
type: options_install_test
 | 
			
		||||
description: null
 | 
			
		||||
help: null
 | 
			
		||||
new_revision: false
 | 
			
		||||
preview_mode: 1
 | 
			
		||||
display_submitted: true
 | 
			
		||||
@ -0,0 +1,8 @@
 | 
			
		||||
name: 'Options config install test'
 | 
			
		||||
type: module
 | 
			
		||||
description: 'Support module for the Options module tests.'
 | 
			
		||||
package: Testing
 | 
			
		||||
version: VERSION
 | 
			
		||||
dependencies:
 | 
			
		||||
  - drupal:node
 | 
			
		||||
  - drupal:options
 | 
			
		||||
@ -0,0 +1,5 @@
 | 
			
		||||
name: 'Options test'
 | 
			
		||||
type: module
 | 
			
		||||
description: 'Support module for the Options module tests.'
 | 
			
		||||
package: Testing
 | 
			
		||||
version: VERSION
 | 
			
		||||
@ -0,0 +1,40 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\options_test\Hook;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Form\FormStateInterface;
 | 
			
		||||
use Drupal\Core\Hook\Attribute\Hook;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Hook implementations for options_test.
 | 
			
		||||
 */
 | 
			
		||||
class OptionsTestHooks {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Implements hook_form_FORM_ID_alter().
 | 
			
		||||
   */
 | 
			
		||||
  #[Hook('form_entity_test_entity_test_form_alter')]
 | 
			
		||||
  public function formEntityTestEntityTestFormAlter(&$form, FormStateInterface $form_state, $form_id) : void {
 | 
			
		||||
    if (\Drupal::state()->get('options_test.form_alter_enable', FALSE)) {
 | 
			
		||||
      $form['card_1']['widget']['#required_error'] = 'This is custom message for required field.';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Implements hook_options_list_alter().
 | 
			
		||||
   */
 | 
			
		||||
  #[Hook('options_list_alter')]
 | 
			
		||||
  public function optionsListAlter(array &$options, array $context): void {
 | 
			
		||||
    if ($context['fieldDefinition']->getName() === 'card_4' && $context['widget']->getPluginId() === 'options_select') {
 | 
			
		||||
      // Rename _none option.
 | 
			
		||||
      $options['_none'] = '- Select something -';
 | 
			
		||||
    }
 | 
			
		||||
    if ($context['fieldDefinition']->getName() === 'card_4' && $context['entity']->bundle() === 'entity_test') {
 | 
			
		||||
      // Remove 0 option.
 | 
			
		||||
      unset($options[0]);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,60 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\options_test;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\FieldableEntityInterface;
 | 
			
		||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provide allowed values callback.
 | 
			
		||||
 */
 | 
			
		||||
class OptionsAllowedValues {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Implements callback_allowed_values_function().
 | 
			
		||||
   *
 | 
			
		||||
   * @see options_allowed_values()
 | 
			
		||||
   */
 | 
			
		||||
  public static function simpleValues(FieldStorageDefinitionInterface $definition, ?FieldableEntityInterface $entity = NULL): array {
 | 
			
		||||
    return [
 | 
			
		||||
      'Group 1' => [
 | 
			
		||||
        0 => 'Zero',
 | 
			
		||||
      ],
 | 
			
		||||
      1 => 'One',
 | 
			
		||||
      'Group 2' => [
 | 
			
		||||
        2 => 'Some <script>dangerous</script> & unescaped <strong>markup</strong>',
 | 
			
		||||
      ],
 | 
			
		||||
      'More <script>dangerous</script> markup' => [
 | 
			
		||||
        3 => 'Three',
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Implements callback_allowed_values_function().
 | 
			
		||||
   *
 | 
			
		||||
   * @todo This function violates the recommendation in options_allowed_values()
 | 
			
		||||
   *   to return a list of all possible values in any context when $items is
 | 
			
		||||
   *   NULL. Since this is not yet used for testing Views integration, that is
 | 
			
		||||
   *   alright for now. Fix this in https://www.drupal.org/node/2012130.
 | 
			
		||||
   *
 | 
			
		||||
   * @see options_allowed_values()
 | 
			
		||||
   */
 | 
			
		||||
  public static function dynamicValues(FieldStorageDefinitionInterface $definition, ?FieldableEntityInterface $entity = NULL, &$cacheable = NULL): array {
 | 
			
		||||
    $values = [];
 | 
			
		||||
    if (isset($entity)) {
 | 
			
		||||
      $cacheable = FALSE;
 | 
			
		||||
      $values = [
 | 
			
		||||
        $entity->label(),
 | 
			
		||||
        $entity->toUrl()->toString(),
 | 
			
		||||
        $entity->uuid(),
 | 
			
		||||
        $entity->bundle(),
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
    // We need the values of the entity as keys.
 | 
			
		||||
    return array_combine($values, $values);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,8 @@
 | 
			
		||||
name: 'Options test views'
 | 
			
		||||
type: module
 | 
			
		||||
description: 'Provides default views for views options tests.'
 | 
			
		||||
package: Testing
 | 
			
		||||
version: VERSION
 | 
			
		||||
dependencies:
 | 
			
		||||
  - drupal:options
 | 
			
		||||
  - drupal:views
 | 
			
		||||
@ -0,0 +1,198 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  config:
 | 
			
		||||
    - node.type.article
 | 
			
		||||
  module:
 | 
			
		||||
    - node
 | 
			
		||||
    - user
 | 
			
		||||
id: test_options_list_argument_numeric
 | 
			
		||||
label: 'test options list argument (numeric)'
 | 
			
		||||
module: views
 | 
			
		||||
description: ''
 | 
			
		||||
tag: ''
 | 
			
		||||
base_table: node_field_data
 | 
			
		||||
base_field: nid
 | 
			
		||||
display:
 | 
			
		||||
  default:
 | 
			
		||||
    display_plugin: default
 | 
			
		||||
    id: default
 | 
			
		||||
    display_title: Default
 | 
			
		||||
    position: 1
 | 
			
		||||
    display_options:
 | 
			
		||||
      access:
 | 
			
		||||
        type: perm
 | 
			
		||||
        options:
 | 
			
		||||
          perm: 'access content'
 | 
			
		||||
      cache:
 | 
			
		||||
        type: tag
 | 
			
		||||
        options: {  }
 | 
			
		||||
      query:
 | 
			
		||||
        type: views_query
 | 
			
		||||
        options:
 | 
			
		||||
          disable_sql_rewrite: false
 | 
			
		||||
          distinct: false
 | 
			
		||||
          replica: false
 | 
			
		||||
          query_comment: ''
 | 
			
		||||
          query_tags: {  }
 | 
			
		||||
      exposed_form:
 | 
			
		||||
        type: basic
 | 
			
		||||
        options:
 | 
			
		||||
          submit_button: Apply
 | 
			
		||||
          reset_button: false
 | 
			
		||||
          reset_button_label: Reset
 | 
			
		||||
          exposed_sorts_label: 'Sort by'
 | 
			
		||||
          expose_sort_order: true
 | 
			
		||||
          sort_asc_label: Asc
 | 
			
		||||
          sort_desc_label: Desc
 | 
			
		||||
      pager:
 | 
			
		||||
        type: some
 | 
			
		||||
        options:
 | 
			
		||||
          items_per_page: 5
 | 
			
		||||
          offset: 0
 | 
			
		||||
      style:
 | 
			
		||||
        type: default
 | 
			
		||||
      row:
 | 
			
		||||
        type: fields
 | 
			
		||||
      fields:
 | 
			
		||||
        title:
 | 
			
		||||
          id: title
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: title
 | 
			
		||||
          label: ''
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            make_link: false
 | 
			
		||||
            absolute: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            word_boundary: false
 | 
			
		||||
            ellipsis: false
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            html: false
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          exclude: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: true
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
      filters:
 | 
			
		||||
        status:
 | 
			
		||||
          value: '1'
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: status
 | 
			
		||||
          id: status
 | 
			
		||||
          expose:
 | 
			
		||||
            operator: ''
 | 
			
		||||
          group: 1
 | 
			
		||||
          plugin_id: boolean
 | 
			
		||||
        type:
 | 
			
		||||
          id: type
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: type
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          operator: in
 | 
			
		||||
          value:
 | 
			
		||||
            article: article
 | 
			
		||||
          group: 1
 | 
			
		||||
          exposed: false
 | 
			
		||||
          expose:
 | 
			
		||||
            operator_id: '0'
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            use_operator: false
 | 
			
		||||
            operator: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            required: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember_roles:
 | 
			
		||||
              authenticated: authenticated
 | 
			
		||||
            reduce: false
 | 
			
		||||
          is_grouped: false
 | 
			
		||||
          group_info:
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            optional: true
 | 
			
		||||
            widget: select
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            default_group: All
 | 
			
		||||
            default_group_multiple: {  }
 | 
			
		||||
            group_items: {  }
 | 
			
		||||
          plugin_id: bundle
 | 
			
		||||
      sorts:
 | 
			
		||||
        nid:
 | 
			
		||||
          id: nid
 | 
			
		||||
          table: nid
 | 
			
		||||
          field: nid
 | 
			
		||||
          order: DESC
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          exposed: false
 | 
			
		||||
          expose:
 | 
			
		||||
            label: ''
 | 
			
		||||
          plugin_id: standard
 | 
			
		||||
      title: 'test options list argument'
 | 
			
		||||
      header: {  }
 | 
			
		||||
      footer: {  }
 | 
			
		||||
      empty: {  }
 | 
			
		||||
      relationships: {  }
 | 
			
		||||
      arguments:
 | 
			
		||||
        field_test_list_integer_value:
 | 
			
		||||
          id: field_test_list_integer_value
 | 
			
		||||
          table: field_data_field_test_list_integer
 | 
			
		||||
          field: field_test_list_integer_value
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          default_action: empty
 | 
			
		||||
          exception:
 | 
			
		||||
            value: all
 | 
			
		||||
            title_enable: false
 | 
			
		||||
            title: All
 | 
			
		||||
          title_enable: false
 | 
			
		||||
          title: ''
 | 
			
		||||
          default_argument_type: fixed
 | 
			
		||||
          default_argument_options:
 | 
			
		||||
            argument: ''
 | 
			
		||||
          summary_options:
 | 
			
		||||
            base_path: ''
 | 
			
		||||
            items_per_page: 25
 | 
			
		||||
            count: false
 | 
			
		||||
            override: false
 | 
			
		||||
          summary:
 | 
			
		||||
            sort_order: asc
 | 
			
		||||
            number_of_records: 0
 | 
			
		||||
            format: default_summary
 | 
			
		||||
            human: true
 | 
			
		||||
          specify_validation: false
 | 
			
		||||
          validate:
 | 
			
		||||
            type: none
 | 
			
		||||
            fail: 'not found'
 | 
			
		||||
          validate_options: {  }
 | 
			
		||||
          break_phrase: false
 | 
			
		||||
          not: false
 | 
			
		||||
          plugin_id: number_list_field
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
  block_1:
 | 
			
		||||
    display_plugin: block
 | 
			
		||||
    id: block_1
 | 
			
		||||
    display_title: Block
 | 
			
		||||
    position: 1
 | 
			
		||||
    display_options:
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
@ -0,0 +1,197 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  config:
 | 
			
		||||
    - node.type.article
 | 
			
		||||
  module:
 | 
			
		||||
    - node
 | 
			
		||||
    - user
 | 
			
		||||
id: test_options_list_argument_string
 | 
			
		||||
label: 'test options list argument (string)'
 | 
			
		||||
module: views
 | 
			
		||||
description: ''
 | 
			
		||||
tag: ''
 | 
			
		||||
base_table: node_field_data
 | 
			
		||||
base_field: nid
 | 
			
		||||
display:
 | 
			
		||||
  default:
 | 
			
		||||
    display_plugin: default
 | 
			
		||||
    id: default
 | 
			
		||||
    display_title: Default
 | 
			
		||||
    position: 1
 | 
			
		||||
    display_options:
 | 
			
		||||
      access:
 | 
			
		||||
        type: perm
 | 
			
		||||
        options:
 | 
			
		||||
          perm: 'access content'
 | 
			
		||||
      cache:
 | 
			
		||||
        type: tag
 | 
			
		||||
        options: {  }
 | 
			
		||||
      query:
 | 
			
		||||
        type: views_query
 | 
			
		||||
        options:
 | 
			
		||||
          disable_sql_rewrite: false
 | 
			
		||||
          distinct: false
 | 
			
		||||
          replica: false
 | 
			
		||||
          query_comment: ''
 | 
			
		||||
          query_tags: {  }
 | 
			
		||||
      exposed_form:
 | 
			
		||||
        type: basic
 | 
			
		||||
        options:
 | 
			
		||||
          submit_button: Apply
 | 
			
		||||
          reset_button: false
 | 
			
		||||
          reset_button_label: Reset
 | 
			
		||||
          exposed_sorts_label: 'Sort by'
 | 
			
		||||
          expose_sort_order: true
 | 
			
		||||
          sort_asc_label: Asc
 | 
			
		||||
          sort_desc_label: Desc
 | 
			
		||||
      pager:
 | 
			
		||||
        type: some
 | 
			
		||||
        options:
 | 
			
		||||
          items_per_page: 5
 | 
			
		||||
          offset: 0
 | 
			
		||||
      style:
 | 
			
		||||
        type: default
 | 
			
		||||
      row:
 | 
			
		||||
        type: fields
 | 
			
		||||
      fields:
 | 
			
		||||
        title:
 | 
			
		||||
          id: title
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: title
 | 
			
		||||
          label: ''
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            make_link: false
 | 
			
		||||
            absolute: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            word_boundary: false
 | 
			
		||||
            ellipsis: false
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            html: false
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          exclude: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: true
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
      filters:
 | 
			
		||||
        status:
 | 
			
		||||
          value: '1'
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: status
 | 
			
		||||
          id: status
 | 
			
		||||
          expose:
 | 
			
		||||
            operator: ''
 | 
			
		||||
          group: 1
 | 
			
		||||
          plugin_id: boolean
 | 
			
		||||
        type:
 | 
			
		||||
          id: type
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: type
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          operator: in
 | 
			
		||||
          value:
 | 
			
		||||
            article: article
 | 
			
		||||
          group: 1
 | 
			
		||||
          exposed: false
 | 
			
		||||
          expose:
 | 
			
		||||
            operator_id: '0'
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            use_operator: false
 | 
			
		||||
            operator: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            required: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember_roles:
 | 
			
		||||
              authenticated: authenticated
 | 
			
		||||
            reduce: false
 | 
			
		||||
          is_grouped: false
 | 
			
		||||
          group_info:
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            optional: true
 | 
			
		||||
            widget: select
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            default_group: All
 | 
			
		||||
            default_group_multiple: {  }
 | 
			
		||||
            group_items: {  }
 | 
			
		||||
          plugin_id: bundle
 | 
			
		||||
      sorts:
 | 
			
		||||
        nid:
 | 
			
		||||
          id: nid
 | 
			
		||||
          table: nid
 | 
			
		||||
          field: nid
 | 
			
		||||
          order: DESC
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          exposed: false
 | 
			
		||||
          expose:
 | 
			
		||||
            label: ''
 | 
			
		||||
          plugin_id: standard
 | 
			
		||||
      title: 'test options list argument'
 | 
			
		||||
      header: {  }
 | 
			
		||||
      footer: {  }
 | 
			
		||||
      empty: {  }
 | 
			
		||||
      relationships: {  }
 | 
			
		||||
      arguments:
 | 
			
		||||
        field_test_list_string_value:
 | 
			
		||||
          id: field_test_list_string_value
 | 
			
		||||
          table: field_data_field_test_list_string
 | 
			
		||||
          field: field_test_list_string_value
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          default_action: empty
 | 
			
		||||
          exception:
 | 
			
		||||
            value: all
 | 
			
		||||
            title_enable: false
 | 
			
		||||
            title: All
 | 
			
		||||
          title_enable: false
 | 
			
		||||
          title: ''
 | 
			
		||||
          default_argument_type: fixed
 | 
			
		||||
          default_argument_options:
 | 
			
		||||
            argument: ''
 | 
			
		||||
          summary_options:
 | 
			
		||||
            base_path: ''
 | 
			
		||||
            items_per_page: 25
 | 
			
		||||
            count: false
 | 
			
		||||
            override: false
 | 
			
		||||
          summary:
 | 
			
		||||
            sort_order: asc
 | 
			
		||||
            number_of_records: 0
 | 
			
		||||
            format: default_summary
 | 
			
		||||
            human: true
 | 
			
		||||
          specify_validation: false
 | 
			
		||||
          validate:
 | 
			
		||||
            type: none
 | 
			
		||||
            fail: 'not found'
 | 
			
		||||
          validate_options: {  }
 | 
			
		||||
          break_phrase: false
 | 
			
		||||
          plugin_id: string_list_field
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
  block_1:
 | 
			
		||||
    display_plugin: block
 | 
			
		||||
    id: block_1
 | 
			
		||||
    display_title: Block
 | 
			
		||||
    position: 1
 | 
			
		||||
    display_options:
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
@ -0,0 +1,203 @@
 | 
			
		||||
langcode: en
 | 
			
		||||
status: true
 | 
			
		||||
dependencies:
 | 
			
		||||
  config:
 | 
			
		||||
    - node.type.article
 | 
			
		||||
  module:
 | 
			
		||||
    - node
 | 
			
		||||
    - user
 | 
			
		||||
id: test_options_list_filter
 | 
			
		||||
label: test_options_list_filter
 | 
			
		||||
module: views
 | 
			
		||||
description: ''
 | 
			
		||||
tag: ''
 | 
			
		||||
base_table: node_field_data
 | 
			
		||||
base_field: nid
 | 
			
		||||
display:
 | 
			
		||||
  default:
 | 
			
		||||
    display_plugin: default
 | 
			
		||||
    id: default
 | 
			
		||||
    display_title: Default
 | 
			
		||||
    position: 1
 | 
			
		||||
    display_options:
 | 
			
		||||
      access:
 | 
			
		||||
        type: perm
 | 
			
		||||
        options:
 | 
			
		||||
          perm: 'access content'
 | 
			
		||||
      cache:
 | 
			
		||||
        type: tag
 | 
			
		||||
        options: {  }
 | 
			
		||||
      query:
 | 
			
		||||
        type: views_query
 | 
			
		||||
        options:
 | 
			
		||||
          disable_sql_rewrite: false
 | 
			
		||||
          distinct: false
 | 
			
		||||
          replica: false
 | 
			
		||||
          query_comment: ''
 | 
			
		||||
          query_tags: {  }
 | 
			
		||||
      exposed_form:
 | 
			
		||||
        type: basic
 | 
			
		||||
        options:
 | 
			
		||||
          submit_button: Apply
 | 
			
		||||
          reset_button: false
 | 
			
		||||
          reset_button_label: Reset
 | 
			
		||||
          exposed_sorts_label: 'Sort by'
 | 
			
		||||
          expose_sort_order: true
 | 
			
		||||
          sort_asc_label: Asc
 | 
			
		||||
          sort_desc_label: Desc
 | 
			
		||||
      pager:
 | 
			
		||||
        type: some
 | 
			
		||||
        options:
 | 
			
		||||
          items_per_page: 5
 | 
			
		||||
          offset: 0
 | 
			
		||||
      style:
 | 
			
		||||
        type: default
 | 
			
		||||
      row:
 | 
			
		||||
        type: fields
 | 
			
		||||
      fields:
 | 
			
		||||
        title:
 | 
			
		||||
          id: title
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: title
 | 
			
		||||
          label: ''
 | 
			
		||||
          alter:
 | 
			
		||||
            alter_text: false
 | 
			
		||||
            make_link: false
 | 
			
		||||
            absolute: false
 | 
			
		||||
            trim: false
 | 
			
		||||
            word_boundary: false
 | 
			
		||||
            ellipsis: false
 | 
			
		||||
            strip_tags: false
 | 
			
		||||
            html: false
 | 
			
		||||
          hide_empty: false
 | 
			
		||||
          empty_zero: false
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          exclude: false
 | 
			
		||||
          element_type: ''
 | 
			
		||||
          element_class: ''
 | 
			
		||||
          element_label_type: ''
 | 
			
		||||
          element_label_class: ''
 | 
			
		||||
          element_label_colon: true
 | 
			
		||||
          element_wrapper_type: ''
 | 
			
		||||
          element_wrapper_class: ''
 | 
			
		||||
          element_default_classes: true
 | 
			
		||||
          empty: ''
 | 
			
		||||
          hide_alter_empty: true
 | 
			
		||||
          plugin_id: field
 | 
			
		||||
      filters:
 | 
			
		||||
        status:
 | 
			
		||||
          value: '1'
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: status
 | 
			
		||||
          id: status
 | 
			
		||||
          expose:
 | 
			
		||||
            operator: ''
 | 
			
		||||
          group: 1
 | 
			
		||||
          plugin_id: boolean
 | 
			
		||||
        field_test_list_string_value:
 | 
			
		||||
          id: field_test_list_string_value
 | 
			
		||||
          table: field_data_field_test_list_string
 | 
			
		||||
          field: field_test_list_string_value
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          operator: or
 | 
			
		||||
          value:
 | 
			
		||||
            man: man
 | 
			
		||||
            woman: woman
 | 
			
		||||
          group: 1
 | 
			
		||||
          exposed: false
 | 
			
		||||
          expose:
 | 
			
		||||
            operator_id: ''
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            use_operator: false
 | 
			
		||||
            operator: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            required: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember_roles:
 | 
			
		||||
              authenticated: authenticated
 | 
			
		||||
            reduce: false
 | 
			
		||||
          is_grouped: false
 | 
			
		||||
          group_info:
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            optional: true
 | 
			
		||||
            widget: select
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            default_group: All
 | 
			
		||||
            default_group_multiple: {  }
 | 
			
		||||
            group_items: {  }
 | 
			
		||||
          reduce_duplicates: false
 | 
			
		||||
          plugin_id: list_field
 | 
			
		||||
        type:
 | 
			
		||||
          id: type
 | 
			
		||||
          table: node_field_data
 | 
			
		||||
          field: type
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          operator: in
 | 
			
		||||
          value:
 | 
			
		||||
            article: article
 | 
			
		||||
          group: 1
 | 
			
		||||
          exposed: false
 | 
			
		||||
          expose:
 | 
			
		||||
            operator_id: ''
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            use_operator: false
 | 
			
		||||
            operator: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            required: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember_roles:
 | 
			
		||||
              authenticated: authenticated
 | 
			
		||||
            reduce: false
 | 
			
		||||
          is_grouped: false
 | 
			
		||||
          group_info:
 | 
			
		||||
            label: ''
 | 
			
		||||
            description: ''
 | 
			
		||||
            identifier: ''
 | 
			
		||||
            optional: true
 | 
			
		||||
            widget: select
 | 
			
		||||
            multiple: false
 | 
			
		||||
            remember: false
 | 
			
		||||
            default_group: All
 | 
			
		||||
            default_group_multiple: {  }
 | 
			
		||||
            group_items: {  }
 | 
			
		||||
          plugin_id: bundle
 | 
			
		||||
      sorts:
 | 
			
		||||
        nid:
 | 
			
		||||
          id: nid
 | 
			
		||||
          table: nid
 | 
			
		||||
          field: nid
 | 
			
		||||
          order: DESC
 | 
			
		||||
          relationship: none
 | 
			
		||||
          group_type: group
 | 
			
		||||
          admin_label: ''
 | 
			
		||||
          exposed: false
 | 
			
		||||
          expose:
 | 
			
		||||
            label: ''
 | 
			
		||||
          plugin_id: standard
 | 
			
		||||
      title: test_options_list_filter
 | 
			
		||||
      header: {  }
 | 
			
		||||
      footer: {  }
 | 
			
		||||
      empty: {  }
 | 
			
		||||
      relationships: {  }
 | 
			
		||||
      arguments: {  }
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
  block_1:
 | 
			
		||||
    display_plugin: block
 | 
			
		||||
    id: block_1
 | 
			
		||||
    display_title: Block
 | 
			
		||||
    position: 1
 | 
			
		||||
    display_options:
 | 
			
		||||
      display_extenders: {  }
 | 
			
		||||
@ -0,0 +1,14 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\options\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generic module test for options.
 | 
			
		||||
 *
 | 
			
		||||
 * @group options
 | 
			
		||||
 */
 | 
			
		||||
class GenericTest extends GenericModuleTestBase {}
 | 
			
		||||
@ -0,0 +1,95 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\options\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestRev;
 | 
			
		||||
use Drupal\Tests\field\Functional\FieldTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Base class for testing allowed values of options fields.
 | 
			
		||||
 */
 | 
			
		||||
abstract class OptionsDynamicValuesTestBase extends FieldTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['options', 'entity_test', 'options_test'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The created entity.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Entity\EntityInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $entity;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The field storage.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Field\FieldStorageDefinitionInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $fieldStorage;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @var int
 | 
			
		||||
   */
 | 
			
		||||
  protected int $field;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test data.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected array $test;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $field_name = 'test_options';
 | 
			
		||||
    $this->fieldStorage = FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => $field_name,
 | 
			
		||||
      'entity_type' => 'entity_test_rev',
 | 
			
		||||
      'type' => 'list_string',
 | 
			
		||||
      'cardinality' => 1,
 | 
			
		||||
      'settings' => [
 | 
			
		||||
        'allowed_values_function' => '\Drupal\options_test\OptionsAllowedValues::dynamicValues',
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->fieldStorage->save();
 | 
			
		||||
 | 
			
		||||
    $this->field = FieldConfig::create([
 | 
			
		||||
      'field_name' => $field_name,
 | 
			
		||||
      'entity_type' => 'entity_test_rev',
 | 
			
		||||
      'bundle' => 'entity_test_rev',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ])->save();
 | 
			
		||||
    \Drupal::service('entity_display.repository')
 | 
			
		||||
      ->getFormDisplay('entity_test_rev', 'entity_test_rev')
 | 
			
		||||
      ->setComponent($field_name, [
 | 
			
		||||
        'type' => 'options_select',
 | 
			
		||||
      ])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    // Create an entity and prepare test data that will be used by
 | 
			
		||||
    // \Drupal\options_test\OptionsAllowedValues::dynamicValues().
 | 
			
		||||
    $values = [
 | 
			
		||||
      'user_id' => mt_rand(1, 10),
 | 
			
		||||
      'name' => $this->randomMachineName(),
 | 
			
		||||
    ];
 | 
			
		||||
    $this->entity = EntityTestRev::create($values);
 | 
			
		||||
    $this->entity->save();
 | 
			
		||||
    $this->test = [
 | 
			
		||||
      'label' => $this->entity->label(),
 | 
			
		||||
      'uuid' => $this->entity->uuid(),
 | 
			
		||||
      'bundle' => $this->entity->bundle(),
 | 
			
		||||
      'uri' => $this->entity->toUrl()->toString(),
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,488 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\options\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\Tests\field\Functional\FieldTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the Options field UI functionality.
 | 
			
		||||
 *
 | 
			
		||||
 * @group options
 | 
			
		||||
 * @group #slow
 | 
			
		||||
 */
 | 
			
		||||
class OptionsFieldUITest extends FieldTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'node',
 | 
			
		||||
    'options',
 | 
			
		||||
    'field_test',
 | 
			
		||||
    'taxonomy',
 | 
			
		||||
    'field_ui',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The name of the created content type.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $typeName;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Machine name of the created content type.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $type;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Name of the option field.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $fieldName;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Admin path to manage field storage settings.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $adminPath;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    // Create test user.
 | 
			
		||||
    $admin_user = $this->drupalCreateUser([
 | 
			
		||||
      'access content',
 | 
			
		||||
      'administer taxonomy',
 | 
			
		||||
      'access administration pages',
 | 
			
		||||
      'administer site configuration',
 | 
			
		||||
      'administer content types',
 | 
			
		||||
      'administer nodes',
 | 
			
		||||
      'bypass node access',
 | 
			
		||||
      'administer node fields',
 | 
			
		||||
      'administer node display',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($admin_user);
 | 
			
		||||
 | 
			
		||||
    // Create content type, with underscores.
 | 
			
		||||
    $this->typeName = 'test_' . $this->randomMachineName();
 | 
			
		||||
    $type = $this->drupalCreateContentType(['name' => $this->typeName, 'type' => $this->typeName]);
 | 
			
		||||
    $this->type = $type->id();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Options (integer) : test 'allowed values' input.
 | 
			
		||||
   */
 | 
			
		||||
  public function testOptionsAllowedValuesInteger(): void {
 | 
			
		||||
    $this->fieldName = 'field_options_integer';
 | 
			
		||||
    $this->createOptionsField('list_integer');
 | 
			
		||||
    $page = $this->getSession()->getPage();
 | 
			
		||||
    $assert_session = $this->assertSession();
 | 
			
		||||
 | 
			
		||||
    // Explicit integer keys.
 | 
			
		||||
    $input = [
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][key]' => 0,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][label]' => 'Zero',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][key]' => 2,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][label]' => 'Two',
 | 
			
		||||
    ];
 | 
			
		||||
    $array = [0 => 'Zero', 2 => 'Two'];
 | 
			
		||||
    $this->assertAllowedValuesInput($input, $array, 'Integer keys are accepted.');
 | 
			
		||||
 | 
			
		||||
    // Non-integer keys.
 | 
			
		||||
    $input = [
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][key]' => 1.1,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][label]' => 'One',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertAllowedValuesInput($input, 'keys must be integers', 'Non integer keys are rejected.');
 | 
			
		||||
    $input = [
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][key]' => 'abc',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][label]' => 'abc',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertAllowedValuesInput($input, 'keys must be integers', 'Non integer keys are rejected.');
 | 
			
		||||
 | 
			
		||||
    $input = [
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][key]' => 0,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][label]' => 'Zero',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][key]' => 1,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][label]' => 'One',
 | 
			
		||||
    ];
 | 
			
		||||
    $array = [0 => 'Zero', 1 => 'One'];
 | 
			
		||||
    $this->assertAllowedValuesInput($input, $array, '');
 | 
			
		||||
    // Create a node with actual data for the field.
 | 
			
		||||
    $settings = [
 | 
			
		||||
      'type' => $this->type,
 | 
			
		||||
      $this->fieldName => [['value' => 1]],
 | 
			
		||||
    ];
 | 
			
		||||
    $node = $this->drupalCreateNode($settings);
 | 
			
		||||
 | 
			
		||||
    // Check that the values in use cannot be removed.
 | 
			
		||||
    $this->drupalGet($this->adminPath);
 | 
			
		||||
    $assert_session->elementExists('css', '#remove_row_button__1');
 | 
			
		||||
    $delete_button_1 = $page->findById('remove_row_button__1');
 | 
			
		||||
    $this->assertTrue($delete_button_1->hasAttribute('disabled'), 'Button is disabled');
 | 
			
		||||
 | 
			
		||||
    // Delete the node, remove the value.
 | 
			
		||||
    $node->delete();
 | 
			
		||||
    $this->drupalGet($this->adminPath);
 | 
			
		||||
    $delete_button_1->click();
 | 
			
		||||
    $assert_session->pageTextNotContains('Processing...');
 | 
			
		||||
    $page->findById('edit-submit')->click();
 | 
			
		||||
    $field_storage = FieldStorageConfig::loadByName('node', $this->fieldName);
 | 
			
		||||
    $this->assertSame($field_storage->getSetting('allowed_values'), [0 => 'Zero']);
 | 
			
		||||
 | 
			
		||||
    // Check that the same key can only be used once.
 | 
			
		||||
    $input = [
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][key]' => 0,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][label]' => 'Zero',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][key]' => 0,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][label]' => 'One',
 | 
			
		||||
    ];
 | 
			
		||||
    $array = ['0' => 'One'];
 | 
			
		||||
    $this->assertAllowedValuesInput($input, $array, 'Same value cannot be used multiple times.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Options (float) : test 'allowed values' input.
 | 
			
		||||
   */
 | 
			
		||||
  public function testOptionsAllowedValuesFloat(): void {
 | 
			
		||||
    $this->fieldName = 'field_options_float';
 | 
			
		||||
    $this->createOptionsField('list_float');
 | 
			
		||||
    $page = $this->getSession()->getPage();
 | 
			
		||||
    $assert_session = $this->assertSession();
 | 
			
		||||
 | 
			
		||||
    // Explicit numeric keys.
 | 
			
		||||
    $input = [
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][key]' => 0,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][label]' => 'Zero',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][key]' => .5,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][label]' => 'Point five',
 | 
			
		||||
    ];
 | 
			
		||||
    $array = ['0' => 'Zero', '0.5' => 'Point five'];
 | 
			
		||||
    $this->assertAllowedValuesInput($input, $array, 'Integer keys are accepted.');
 | 
			
		||||
 | 
			
		||||
    // Check that values can be added.
 | 
			
		||||
    $input = [
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][key]' => 0,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][label]' => 'Zero',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][key]' => .5,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][label]' => 'Point five',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][2][item][key]' => 1,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][2][item][label]' => 'One',
 | 
			
		||||
    ];
 | 
			
		||||
    $array = ['0' => 'Zero', '0.5' => 'Point five', '1' => 'One'];
 | 
			
		||||
    $this->assertAllowedValuesInput($input, $array, 'Values can be added.');
 | 
			
		||||
    // Non-numeric keys.
 | 
			
		||||
    $input = [
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][key]' => 'abc',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][label]' => 'abc',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertAllowedValuesInput($input, 'each key must be a valid integer or decimal', 'Non numeric keys are rejected.');
 | 
			
		||||
 | 
			
		||||
    $input = [
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][key]' => 0,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][label]' => 'Zero',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][key]' => .5,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][label]' => 'Point five',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][2][item][key]' => 2,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][2][item][label]' => 'Two',
 | 
			
		||||
    ];
 | 
			
		||||
    $array = ['0' => 'Zero', '0.5' => 'Point five', '2' => 'Two'];
 | 
			
		||||
    $this->assertAllowedValuesInput($input, $array, '');
 | 
			
		||||
    // Create a node with actual data for the field.
 | 
			
		||||
    $settings = [
 | 
			
		||||
      'type' => $this->type,
 | 
			
		||||
      $this->fieldName => [['value' => .5]],
 | 
			
		||||
    ];
 | 
			
		||||
    $node = $this->drupalCreateNode($settings);
 | 
			
		||||
 | 
			
		||||
    // Check that the values in use cannot be removed.
 | 
			
		||||
    $this->drupalGet($this->adminPath);
 | 
			
		||||
    $assert_session->elementExists('css', '#remove_row_button__1');
 | 
			
		||||
    $delete_button_1 = $page->findById('remove_row_button__1');
 | 
			
		||||
    $this->assertTrue($delete_button_1->hasAttribute('disabled'), 'Button is disabled');
 | 
			
		||||
 | 
			
		||||
    // Delete the node, remove the value.
 | 
			
		||||
    $node->delete();
 | 
			
		||||
    $this->drupalGet($this->adminPath);
 | 
			
		||||
    $delete_button_1->click();
 | 
			
		||||
    $assert_session->pageTextNotContains('Processing...');
 | 
			
		||||
    $page->findById('edit-submit')->click();
 | 
			
		||||
    $field_storage = FieldStorageConfig::loadByName('node', $this->fieldName);
 | 
			
		||||
    $this->assertSame($field_storage->getSetting('allowed_values'), [0 => 'Zero', 2 => 'Two']);
 | 
			
		||||
 | 
			
		||||
    $input = [
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][key]' => .5,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][label]' => 'Point five',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][key]' => .5,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][label]' => 'Half',
 | 
			
		||||
    ];
 | 
			
		||||
    $array = ['0.5' => 'Half'];
 | 
			
		||||
    $this->assertAllowedValuesInput($input, $array, 'Same value cannot be used multiple times.');
 | 
			
		||||
 | 
			
		||||
    // Check that different forms of the same float value cannot be used.
 | 
			
		||||
    $input = [
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][key]' => .5,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][label]' => 'Point five',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][key]' => 0.5,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][label]' => 'Half',
 | 
			
		||||
    ];
 | 
			
		||||
    $array = ['0.5' => 'Half'];
 | 
			
		||||
    $this->assertAllowedValuesInput($input, $array, 'Different forms of the same value cannot be used.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Options (text) : test 'allowed values' input.
 | 
			
		||||
   */
 | 
			
		||||
  public function testOptionsAllowedValuesText(): void {
 | 
			
		||||
    $this->fieldName = 'field_options_text';
 | 
			
		||||
    $this->createOptionsField('list_string');
 | 
			
		||||
    $page = $this->getSession()->getPage();
 | 
			
		||||
    $assert_session = $this->assertSession();
 | 
			
		||||
 | 
			
		||||
    // Explicit keys.
 | 
			
		||||
    $input = [
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][key]' => '_zero',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][label]' => 'Zero',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][key]' => '_one',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][label]' => 'One',
 | 
			
		||||
    ];
 | 
			
		||||
    $array = ['_zero' => 'Zero', '_one' => 'One'];
 | 
			
		||||
    $this->assertAllowedValuesInput($input, $array, 'Explicit keys are accepted.');
 | 
			
		||||
 | 
			
		||||
    // Overly long keys.
 | 
			
		||||
    $input = [
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][key]' => 'zero',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][label]' => 'Zero',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][key]' => $this->randomMachineName(256),
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][label]' => 'One',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertAllowedValuesInput($input, 'each key must be a string at most 255 characters long', 'Overly long keys are rejected.');
 | 
			
		||||
 | 
			
		||||
    $input = [
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][key]' => 'zero',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][label]' => 'Zero',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][key]' => 'one',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][label]' => 'One',
 | 
			
		||||
    ];
 | 
			
		||||
    $array = ['zero' => 'Zero', 'one' => 'One'];
 | 
			
		||||
    $this->assertAllowedValuesInput($input, $array, '');
 | 
			
		||||
    // Create a node with actual data for the field.
 | 
			
		||||
    $settings = [
 | 
			
		||||
      'type' => $this->type,
 | 
			
		||||
      $this->fieldName => [['value' => 'one']],
 | 
			
		||||
    ];
 | 
			
		||||
    $node = $this->drupalCreateNode($settings);
 | 
			
		||||
 | 
			
		||||
    // Check that the values in use cannot be removed.
 | 
			
		||||
    $this->drupalGet($this->adminPath);
 | 
			
		||||
    $assert_session->elementExists('css', '#remove_row_button__1');
 | 
			
		||||
    $delete_button_1 = $page->findById('remove_row_button__1');
 | 
			
		||||
    $value_field_1 = $page->findField('field_storage[subform][settings][allowed_values][table][1][item][key]');
 | 
			
		||||
    $this->assertTrue($delete_button_1->hasAttribute('disabled'), 'Button is disabled');
 | 
			
		||||
    $this->assertTrue($value_field_1->hasAttribute('disabled'), 'Button is disabled');
 | 
			
		||||
 | 
			
		||||
    // Delete the node, remove the value.
 | 
			
		||||
    $node->delete();
 | 
			
		||||
    $this->drupalGet($this->adminPath);
 | 
			
		||||
    $delete_button_1->click();
 | 
			
		||||
    $assert_session->pageTextNotContains('Processing...');
 | 
			
		||||
    $page->findById('edit-submit')->click();
 | 
			
		||||
    $field_storage = FieldStorageConfig::loadByName('node', $this->fieldName);
 | 
			
		||||
    $this->assertSame($field_storage->getSetting('allowed_values'), ['zero' => 'Zero']);
 | 
			
		||||
 | 
			
		||||
    // Check that string values with special characters can be used.
 | 
			
		||||
    $input = [
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][key]' => 'zero',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][label]' => 'Zero',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][key]' => '.example #example',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][label]' => 'Example',
 | 
			
		||||
    ];
 | 
			
		||||
    $array = ['zero' => 'Zero', '.example #example' => 'Example'];
 | 
			
		||||
    $this->assertAllowedValuesInput($input, $array, '');
 | 
			
		||||
 | 
			
		||||
    // Check that the same key can only be used once.
 | 
			
		||||
    $input = [
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][key]' => 'zero',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][label]' => 'Zero',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][key]' => 'zero',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][label]' => 'One',
 | 
			
		||||
    ];
 | 
			
		||||
    $array = ['zero' => 'One'];
 | 
			
		||||
    $this->assertAllowedValuesInput($input, $array, 'Same value cannot be used multiple times.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Helper function to create list field of a given type.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $type
 | 
			
		||||
   *   One of 'list_integer', 'list_float' or 'list_string'.
 | 
			
		||||
   */
 | 
			
		||||
  protected function createOptionsField($type): void {
 | 
			
		||||
    // Create a field.
 | 
			
		||||
    FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => $this->fieldName,
 | 
			
		||||
      'entity_type' => 'node',
 | 
			
		||||
      'type' => $type,
 | 
			
		||||
    ])->save();
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'field_name' => $this->fieldName,
 | 
			
		||||
      'entity_type' => 'node',
 | 
			
		||||
      'bundle' => $this->type,
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    \Drupal::service('entity_display.repository')
 | 
			
		||||
      ->getFormDisplay('node', $this->type)
 | 
			
		||||
      ->setComponent($this->fieldName)
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    $this->adminPath = 'admin/structure/types/manage/' . $this->type . '/fields/node.' . $this->type . '.' . $this->fieldName;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests an input array for the 'allowed values' form element.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $input
 | 
			
		||||
   *   The input array.
 | 
			
		||||
   * @param array|string $result
 | 
			
		||||
   *   Either an expected resulting array in
 | 
			
		||||
   *   $field->getSetting('allowed_values'), or an expected error message.
 | 
			
		||||
   * @param string $message
 | 
			
		||||
   *   Message to display.
 | 
			
		||||
   *
 | 
			
		||||
   * @internal
 | 
			
		||||
   */
 | 
			
		||||
  public function assertAllowedValuesInput(array $input, $result, string $message): void {
 | 
			
		||||
    $this->drupalGet($this->adminPath);
 | 
			
		||||
    $page = $this->getSession()->getPage();
 | 
			
		||||
    $add_button = $page->findButton('Add another item');
 | 
			
		||||
    $add_button->click();
 | 
			
		||||
    $add_button->click();
 | 
			
		||||
 | 
			
		||||
    $this->submitForm($input, 'Save');
 | 
			
		||||
    // Verify that the page does not have double escaped HTML tags.
 | 
			
		||||
    $this->assertSession()->responseNotContains('&lt;');
 | 
			
		||||
 | 
			
		||||
    if (is_string($result)) {
 | 
			
		||||
      $this->assertSession()->pageTextContains($result);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $field_storage = FieldStorageConfig::loadByName('node', $this->fieldName);
 | 
			
		||||
      $this->assertSame($field_storage->getSetting('allowed_values'), $result, $message);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests normal and key formatter display on node display.
 | 
			
		||||
   */
 | 
			
		||||
  public function testNodeDisplay(): void {
 | 
			
		||||
    $this->fieldName = $this->randomMachineName();
 | 
			
		||||
    $this->createOptionsField('list_integer');
 | 
			
		||||
    $node = $this->drupalCreateNode(['type' => $this->type]);
 | 
			
		||||
 | 
			
		||||
    $on = $this->randomMachineName();
 | 
			
		||||
    $off = $this->randomMachineName();
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][key]' => 1,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][label]' => $on,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][key]' => 0,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][label]' => $off,
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($this->adminPath);
 | 
			
		||||
    $page = $this->getSession()->getPage();
 | 
			
		||||
    $page->findButton('Add another item')->click();
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
 | 
			
		||||
    // Select a default value.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      $this->fieldName => '1',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet('node/' . $node->id() . '/edit');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
 | 
			
		||||
    // Check the node page and see if the values are correct.
 | 
			
		||||
    $file_formatters = ['list_default', 'list_key'];
 | 
			
		||||
    foreach ($file_formatters as $formatter) {
 | 
			
		||||
      $edit = [
 | 
			
		||||
        "fields[$this->fieldName][type]" => $formatter,
 | 
			
		||||
        "fields[$this->fieldName][region]" => 'content',
 | 
			
		||||
      ];
 | 
			
		||||
      $this->drupalGet('admin/structure/types/manage/' . $this->typeName . '/display');
 | 
			
		||||
      $this->submitForm($edit, 'Save');
 | 
			
		||||
      $this->drupalGet('node/' . $node->id());
 | 
			
		||||
 | 
			
		||||
      if ($formatter == 'list_default') {
 | 
			
		||||
        $output = $on;
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        $output = '1';
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Verify that correct options are found.
 | 
			
		||||
      $this->assertSession()->elementsCount('xpath', '//div[text()="' . $output . '"]', 1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Confirms the allowed value list is a required field.
 | 
			
		||||
   */
 | 
			
		||||
  public function testRequiredPropertyForAllowedValuesList(): void {
 | 
			
		||||
    $field_types = [
 | 
			
		||||
      'list_float',
 | 
			
		||||
      'list_string',
 | 
			
		||||
      'list_integer',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    foreach ($field_types as $field_type) {
 | 
			
		||||
      $this->fieldName = "field_options_$field_type";
 | 
			
		||||
      $this->createOptionsField($field_type);
 | 
			
		||||
      $page = $this->getSession()->getPage();
 | 
			
		||||
 | 
			
		||||
      $this->drupalGet($this->adminPath);
 | 
			
		||||
      // Assert that the delete button for a single row is disabled.
 | 
			
		||||
      $this->assertCount(1, $page->findAll('css', '#allowed-values-order tr.draggable'));
 | 
			
		||||
      $delete_button_0 = $page->findById('remove_row_button__0');
 | 
			
		||||
      $this->assertTrue($delete_button_0->hasAttribute('disabled'), 'Button is disabled');
 | 
			
		||||
      $page->findButton('Add another item')->click();
 | 
			
		||||
      // Assert that the delete button for the first row is enabled if there are
 | 
			
		||||
      // more that one rows.
 | 
			
		||||
      $this->assertCount(2, $page->findAll('css', '#allowed-values-order tr.draggable'));
 | 
			
		||||
      $this->assertFalse($delete_button_0->hasAttribute('disabled'), 'Button is enabled');
 | 
			
		||||
      // Delete a row.
 | 
			
		||||
      $delete_button_0->click();
 | 
			
		||||
      // Assert that the button is disabled again.
 | 
			
		||||
      $this->assertTrue($delete_button_0->hasAttribute('disabled'), 'Button is disabled');
 | 
			
		||||
      // Try to proceed without entering any value.
 | 
			
		||||
      $page->findButton('Save')->click();
 | 
			
		||||
 | 
			
		||||
      if ($field_type == 'list_string') {
 | 
			
		||||
        // Asserting only name field as there is no value field for list_string.
 | 
			
		||||
        $this->assertSession()->pageTextContains('Name field is required.');
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        // Confirmation message that name and value are required fields for
 | 
			
		||||
        // list_float and list_integer.
 | 
			
		||||
        $this->assertSession()->pageTextContains('Name field is required.');
 | 
			
		||||
        $this->assertSession()->pageTextContains('Value field is required.');
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,102 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\options\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\Tests\field\Functional\FieldTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests option fields can be updated and created by config synchronization.
 | 
			
		||||
 *
 | 
			
		||||
 * @group options
 | 
			
		||||
 */
 | 
			
		||||
class OptionsFloatFieldImportTest extends FieldTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'node',
 | 
			
		||||
    'options',
 | 
			
		||||
    'field_ui',
 | 
			
		||||
    'config',
 | 
			
		||||
    'options_config_install_test',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    // Create test user.
 | 
			
		||||
    $admin_user = $this->drupalCreateUser([
 | 
			
		||||
      'synchronize configuration',
 | 
			
		||||
      'access content',
 | 
			
		||||
      'access administration pages',
 | 
			
		||||
      'administer site configuration',
 | 
			
		||||
      'administer content types',
 | 
			
		||||
      'administer nodes',
 | 
			
		||||
      'bypass node access',
 | 
			
		||||
      'administer node fields',
 | 
			
		||||
      'administer node display',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($admin_user);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that importing list_float fields works.
 | 
			
		||||
   */
 | 
			
		||||
  public function testImport(): void {
 | 
			
		||||
    $field_name = 'field_options_float';
 | 
			
		||||
    $type = 'options_install_test';
 | 
			
		||||
 | 
			
		||||
    // Test the results on installing options_config_install_test. All the
 | 
			
		||||
    // necessary configuration for this test is created by installing that
 | 
			
		||||
    // module.
 | 
			
		||||
    $field_storage = FieldStorageConfig::loadByName('node', $field_name);
 | 
			
		||||
    $this->assertSame($array = ['0' => 'Zero', '0.5' => 'Point five'], $field_storage->getSetting('allowed_values'));
 | 
			
		||||
 | 
			
		||||
    $admin_path = 'admin/structure/types/manage/' . $type . '/fields/node.' . $type . '.' . $field_name;
 | 
			
		||||
 | 
			
		||||
    // Export active config to sync.
 | 
			
		||||
    $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
 | 
			
		||||
 | 
			
		||||
    // Set the active to not use dots in the allowed values key names.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][key]' => 0,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][0][item][label]' => 'Zero',
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][key]' => 1,
 | 
			
		||||
      'field_storage[subform][settings][allowed_values][table][1][item][label]' => 'One',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->drupalGet($admin_path);
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $field_storage = FieldStorageConfig::loadByName('node', $field_name);
 | 
			
		||||
    $this->assertSame($array = ['0' => 'Zero', '1' => 'One'], $field_storage->getSetting('allowed_values'));
 | 
			
		||||
 | 
			
		||||
    // Import configuration with dots in the allowed values key names. This
 | 
			
		||||
    // tests \Drupal\Core\Config\Entity\ConfigEntityStorage::importUpdate().
 | 
			
		||||
    $this->drupalGet('admin/config/development/configuration');
 | 
			
		||||
    $this->submitForm([], 'Import all');
 | 
			
		||||
    $field_storage = FieldStorageConfig::loadByName('node', $field_name);
 | 
			
		||||
    $this->assertSame($array = ['0' => 'Zero', '0.5' => 'Point five'], $field_storage->getSetting('allowed_values'));
 | 
			
		||||
 | 
			
		||||
    // Delete field to test creation. This tests
 | 
			
		||||
    // \Drupal\Core\Config\Entity\ConfigEntityStorage::importCreate().
 | 
			
		||||
    FieldConfig::loadByName('node', $type, $field_name)->delete();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('admin/config/development/configuration');
 | 
			
		||||
    $this->submitForm([], 'Import all');
 | 
			
		||||
    $field_storage = FieldStorageConfig::loadByName('node', $field_name);
 | 
			
		||||
    $this->assertSame($array = ['0' => 'Zero', '0.5' => 'Point five'], $field_storage->getSetting('allowed_values'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,45 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\options\Functional;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests an options select with a dynamic allowed values function.
 | 
			
		||||
 *
 | 
			
		||||
 * @group options
 | 
			
		||||
 */
 | 
			
		||||
class OptionsSelectDynamicValuesTest extends OptionsDynamicValuesTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the 'options_select' widget (single select).
 | 
			
		||||
   */
 | 
			
		||||
  public function testSelectListDynamic(): void {
 | 
			
		||||
    // Create an entity.
 | 
			
		||||
    $this->entity->save();
 | 
			
		||||
 | 
			
		||||
    // Create a web user.
 | 
			
		||||
    $web_user = $this->drupalCreateUser([
 | 
			
		||||
      'view test entity',
 | 
			
		||||
      'administer entity_test content',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($web_user);
 | 
			
		||||
 | 
			
		||||
    // Display form.
 | 
			
		||||
    $this->drupalGet('entity_test_rev/manage/' . $this->entity->id() . '/edit');
 | 
			
		||||
    $options = $this->assertSession()->selectExists('edit-test-options')->findAll('css', 'option');
 | 
			
		||||
    $this->assertCount(count($this->test) + 1, $options);
 | 
			
		||||
    foreach ($options as $option) {
 | 
			
		||||
      $value = $option->getValue();
 | 
			
		||||
      if ($value != '_none') {
 | 
			
		||||
        $this->assertContains($value, $this->test);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,705 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\options\Functional;
 | 
			
		||||
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTest;
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\Tests\field\Functional\FieldTestBase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the Options widgets.
 | 
			
		||||
 *
 | 
			
		||||
 * @group options
 | 
			
		||||
 */
 | 
			
		||||
class OptionsWidgetsTest extends FieldTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'node',
 | 
			
		||||
    'options',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'options_test',
 | 
			
		||||
    'taxonomy',
 | 
			
		||||
    'field_ui',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A field storage with cardinality 1 to use in this test class.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\field\Entity\FieldStorageConfig
 | 
			
		||||
   */
 | 
			
		||||
  protected $card1;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A field storage with cardinality 2 to use in this test class.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\field\Entity\FieldStorageConfig
 | 
			
		||||
   */
 | 
			
		||||
  protected $card2;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A field storage with float values to use in this test class.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\field\Entity\FieldStorageConfig
 | 
			
		||||
   */
 | 
			
		||||
  protected $float;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    // Field storage with cardinality 1.
 | 
			
		||||
    $this->card1 = FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => 'card_1',
 | 
			
		||||
      'entity_type' => 'entity_test',
 | 
			
		||||
      'type' => 'list_integer',
 | 
			
		||||
      'cardinality' => 1,
 | 
			
		||||
      'settings' => [
 | 
			
		||||
        'allowed_values' => [
 | 
			
		||||
          // Make sure that 0 works as an option.
 | 
			
		||||
          0 => 'Zero',
 | 
			
		||||
          1 => 'One',
 | 
			
		||||
          // Make sure that option text is properly sanitized.
 | 
			
		||||
          2 => 'Some <script>dangerous</script> & unescaped <strong>markup</strong>',
 | 
			
		||||
          // Make sure that HTML entities in option text are not double-encoded.
 | 
			
		||||
          3 => 'Some HTML encoded markup with < & >',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->card1->save();
 | 
			
		||||
 | 
			
		||||
    // Field storage with cardinality 2.
 | 
			
		||||
    $this->card2 = FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => 'card_2',
 | 
			
		||||
      'entity_type' => 'entity_test',
 | 
			
		||||
      'type' => 'list_integer',
 | 
			
		||||
      'cardinality' => 2,
 | 
			
		||||
      'settings' => [
 | 
			
		||||
        'allowed_values' => [
 | 
			
		||||
          // Make sure that 0 works as an option.
 | 
			
		||||
          0 => 'Zero',
 | 
			
		||||
          1 => 'One',
 | 
			
		||||
          // Make sure that option text is properly sanitized.
 | 
			
		||||
          2 => 'Some <script>dangerous</script> & unescaped <strong>markup</strong>',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->card2->save();
 | 
			
		||||
 | 
			
		||||
    // Field storage with list of float values.
 | 
			
		||||
    $this->float = FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => 'float',
 | 
			
		||||
      'entity_type' => 'entity_test',
 | 
			
		||||
      'type' => 'list_float',
 | 
			
		||||
      'cardinality' => 1,
 | 
			
		||||
      'settings' => [
 | 
			
		||||
        'allowed_values' => [
 | 
			
		||||
          '0.0' => '0.0',
 | 
			
		||||
          '1.5' => '1.5',
 | 
			
		||||
          '2.0' => '2.0',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->float->save();
 | 
			
		||||
 | 
			
		||||
    // Create a web user.
 | 
			
		||||
    $this->drupalLogin($this->drupalCreateUser([
 | 
			
		||||
      'view test entity',
 | 
			
		||||
      'administer entity_test content',
 | 
			
		||||
    ]));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the 'options_buttons' widget (single select).
 | 
			
		||||
   */
 | 
			
		||||
  public function testRadioButtons(): void {
 | 
			
		||||
    // Create an instance of the 'single value' field.
 | 
			
		||||
    $field = FieldConfig::create([
 | 
			
		||||
      'field_storage' => $this->card1,
 | 
			
		||||
      'bundle' => 'entity_test',
 | 
			
		||||
    ]);
 | 
			
		||||
    $field->save();
 | 
			
		||||
    \Drupal::service('entity_display.repository')
 | 
			
		||||
      ->getFormDisplay('entity_test', 'entity_test')
 | 
			
		||||
      ->setComponent($this->card1->getName(), [
 | 
			
		||||
        'type' => 'options_buttons',
 | 
			
		||||
      ])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    // Create an entity.
 | 
			
		||||
    $entity = EntityTest::create([
 | 
			
		||||
      'user_id' => 1,
 | 
			
		||||
      'name' => $this->randomMachineName(),
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $entity_init = clone $entity;
 | 
			
		||||
 | 
			
		||||
    // With no field data, no buttons are checked.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->assertSession()->checkboxNotChecked('edit-card-1-0');
 | 
			
		||||
    $this->assertSession()->checkboxNotChecked('edit-card-1-1');
 | 
			
		||||
    $this->assertSession()->checkboxNotChecked('edit-card-1-2');
 | 
			
		||||
    $this->assertSession()->responseContains('Some dangerous & unescaped <strong>markup</strong>');
 | 
			
		||||
    $this->assertSession()->responseContains('Some HTML encoded markup with < & >');
 | 
			
		||||
 | 
			
		||||
    // Select first option.
 | 
			
		||||
    $edit = ['card_1' => 0];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertFieldValues($entity_init, 'card_1', [0]);
 | 
			
		||||
 | 
			
		||||
    // Check that the selected button is checked.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-card-1-0');
 | 
			
		||||
    $this->assertSession()->checkboxNotChecked('edit-card-1-1');
 | 
			
		||||
    $this->assertSession()->checkboxNotChecked('edit-card-1-2');
 | 
			
		||||
 | 
			
		||||
    // Unselect option.
 | 
			
		||||
    $edit = ['card_1' => '_none'];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertFieldValues($entity_init, 'card_1', []);
 | 
			
		||||
 | 
			
		||||
    // Check that required radios with one option is auto-selected.
 | 
			
		||||
    $this->card1->setSetting('allowed_values', [99 => 'Only allowed value']);
 | 
			
		||||
    $this->card1->save();
 | 
			
		||||
    $field->setRequired(TRUE);
 | 
			
		||||
    $field->save();
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-card-1-99');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the 'options_buttons' widget (multiple select).
 | 
			
		||||
   */
 | 
			
		||||
  public function testCheckBoxes(): void {
 | 
			
		||||
    // Create an instance of the 'multiple values' field.
 | 
			
		||||
    $field = FieldConfig::create([
 | 
			
		||||
      'field_storage' => $this->card2,
 | 
			
		||||
      'bundle' => 'entity_test',
 | 
			
		||||
    ]);
 | 
			
		||||
    $field->save();
 | 
			
		||||
    \Drupal::service('entity_display.repository')
 | 
			
		||||
      ->getFormDisplay('entity_test', 'entity_test')
 | 
			
		||||
      ->setComponent($this->card2->getName(), [
 | 
			
		||||
        'type' => 'options_buttons',
 | 
			
		||||
      ])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    // Create an entity.
 | 
			
		||||
    $entity = EntityTest::create([
 | 
			
		||||
      'user_id' => 1,
 | 
			
		||||
      'name' => $this->randomMachineName(),
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $entity_init = clone $entity;
 | 
			
		||||
 | 
			
		||||
    // Display form: with no field data, nothing is checked.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->assertSession()->checkboxNotChecked('edit-card-2-0');
 | 
			
		||||
    $this->assertSession()->checkboxNotChecked('edit-card-2-1');
 | 
			
		||||
    $this->assertSession()->checkboxNotChecked('edit-card-2-2');
 | 
			
		||||
    $this->assertSession()->responseContains('Some dangerous & unescaped <strong>markup</strong>');
 | 
			
		||||
 | 
			
		||||
    // Submit form: select first and third options.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'card_2[0]' => TRUE,
 | 
			
		||||
      'card_2[1]' => FALSE,
 | 
			
		||||
      'card_2[2]' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertFieldValues($entity_init, 'card_2', [0, 2]);
 | 
			
		||||
 | 
			
		||||
    // Display form: check that the right options are selected.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-card-2-0');
 | 
			
		||||
    $this->assertSession()->checkboxNotChecked('edit-card-2-1');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-card-2-2');
 | 
			
		||||
 | 
			
		||||
    // Submit form: select only first option.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'card_2[0]' => TRUE,
 | 
			
		||||
      'card_2[1]' => FALSE,
 | 
			
		||||
      'card_2[2]' => FALSE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertFieldValues($entity_init, 'card_2', [0]);
 | 
			
		||||
 | 
			
		||||
    // Display form: check that the right options are selected.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-card-2-0');
 | 
			
		||||
    $this->assertSession()->checkboxNotChecked('edit-card-2-1');
 | 
			
		||||
    $this->assertSession()->checkboxNotChecked('edit-card-2-2');
 | 
			
		||||
 | 
			
		||||
    // Submit form: select the three options while the field accepts only 2.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'card_2[0]' => TRUE,
 | 
			
		||||
      'card_2[1]' => TRUE,
 | 
			
		||||
      'card_2[2]' => TRUE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertSession()->pageTextContains('this field cannot hold more than 2 values');
 | 
			
		||||
 | 
			
		||||
    // Submit form: uncheck all options.
 | 
			
		||||
    $edit = [
 | 
			
		||||
      'card_2[0]' => FALSE,
 | 
			
		||||
      'card_2[1]' => FALSE,
 | 
			
		||||
      'card_2[2]' => FALSE,
 | 
			
		||||
    ];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    // Check that the value was saved.
 | 
			
		||||
    $this->assertFieldValues($entity_init, 'card_2', []);
 | 
			
		||||
 | 
			
		||||
    // Required checkbox with one option is auto-selected.
 | 
			
		||||
    $this->card2->setSetting('allowed_values', [99 => 'Only allowed value']);
 | 
			
		||||
    $this->card2->save();
 | 
			
		||||
    $field->setRequired(TRUE);
 | 
			
		||||
    $field->save();
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->assertSession()->checkboxChecked('edit-card-2-99');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the 'options_select' widget (single select).
 | 
			
		||||
   */
 | 
			
		||||
  public function testSelectListSingle(): void {
 | 
			
		||||
    // Create an instance of the 'single value' field.
 | 
			
		||||
    $field = FieldConfig::create([
 | 
			
		||||
      'field_storage' => $this->card1,
 | 
			
		||||
      'bundle' => 'entity_test',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ]);
 | 
			
		||||
    $field->save();
 | 
			
		||||
    \Drupal::service('entity_display.repository')
 | 
			
		||||
      ->getFormDisplay('entity_test', 'entity_test')
 | 
			
		||||
      ->setComponent($this->card1->getName(), [
 | 
			
		||||
        'type' => 'options_select',
 | 
			
		||||
      ])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    // Create an entity.
 | 
			
		||||
    $entity = EntityTest::create([
 | 
			
		||||
      'user_id' => 1,
 | 
			
		||||
      'name' => $this->randomMachineName(),
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $entity_init = clone $entity;
 | 
			
		||||
 | 
			
		||||
    // Display form.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    // A required field without any value has a "none" option.
 | 
			
		||||
    $option = $this->assertSession()->optionExists('edit-card-1', '_none');
 | 
			
		||||
    $this->assertSame('- Select a value -', $option->getText());
 | 
			
		||||
 | 
			
		||||
    // With no field data, nothing is selected.
 | 
			
		||||
    $this->assertTrue($this->assertSession()->optionExists('card_1', '_none')->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_1', 0)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_1', 1)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_1', 2)->isSelected());
 | 
			
		||||
    $this->assertSession()->responseContains('Some dangerous & unescaped markup');
 | 
			
		||||
 | 
			
		||||
    // Submit form: select invalid 'none' option.
 | 
			
		||||
    $edit = ['card_1' => '_none'];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertSession()->pageTextContains("{$field->getName()} field is required.");
 | 
			
		||||
 | 
			
		||||
    // Submit form: select first option.
 | 
			
		||||
    $edit = ['card_1' => 0];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertFieldValues($entity_init, 'card_1', [0]);
 | 
			
		||||
 | 
			
		||||
    // Display form: check that the right options are selected.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    // A required field with a value has no 'none' option.
 | 
			
		||||
    $this->assertSession()->optionNotExists('edit-card-1', '_none');
 | 
			
		||||
    $this->assertTrue($this->assertSession()->optionExists('card_1', 0)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_1', 1)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_1', 2)->isSelected());
 | 
			
		||||
 | 
			
		||||
    // Make the field non required.
 | 
			
		||||
    $field->setRequired(FALSE);
 | 
			
		||||
    $field->save();
 | 
			
		||||
 | 
			
		||||
    // Display form.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    // A non-required field has a 'none' option.
 | 
			
		||||
    $option = $this->assertSession()->optionExists('edit-card-1', '_none');
 | 
			
		||||
    $this->assertSame('- None -', $option->getText());
 | 
			
		||||
    // Submit form: Unselect the option.
 | 
			
		||||
    $edit = ['card_1' => '_none'];
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertFieldValues($entity_init, 'card_1', []);
 | 
			
		||||
 | 
			
		||||
    // Test optgroups.
 | 
			
		||||
 | 
			
		||||
    $this->card1->setSetting('allowed_values', []);
 | 
			
		||||
    $this->card1->setSetting('allowed_values_function', '\Drupal\options_test\OptionsAllowedValues::simpleValues');
 | 
			
		||||
    $this->card1->save();
 | 
			
		||||
 | 
			
		||||
    // Display form: with no field data, nothing is selected
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_1', 0)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_1', 1)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_1', 2)->isSelected());
 | 
			
		||||
    $this->assertSession()->responseContains('Some dangerous & unescaped markup');
 | 
			
		||||
    $this->assertSession()->responseContains('More <script>dangerous</script> markup');
 | 
			
		||||
    $this->assertSession()->responseContains('Group 1');
 | 
			
		||||
 | 
			
		||||
    // Submit form: select first option.
 | 
			
		||||
    $edit = ['card_1' => 0];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertFieldValues($entity_init, 'card_1', [0]);
 | 
			
		||||
 | 
			
		||||
    // Display form: check that the right options are selected.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->assertTrue($this->assertSession()->optionExists('card_1', 0)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_1', 1)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_1', 2)->isSelected());
 | 
			
		||||
 | 
			
		||||
    // Submit form: Unselect the option.
 | 
			
		||||
    $edit = ['card_1' => '_none'];
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertFieldValues($entity_init, 'card_1', []);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the '#required_error' attribute for the select list.
 | 
			
		||||
   */
 | 
			
		||||
  public function testSelectListRequiredErrorAttribute(): void {
 | 
			
		||||
    // Enable form alter hook.
 | 
			
		||||
    \Drupal::state()->set('options_test.form_alter_enable', TRUE);
 | 
			
		||||
    // Create an instance of the 'single value' field.
 | 
			
		||||
    $field = FieldConfig::create([
 | 
			
		||||
      'field_storage' => $this->card1,
 | 
			
		||||
      'bundle' => 'entity_test',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ]);
 | 
			
		||||
    $field->save();
 | 
			
		||||
    \Drupal::service('entity_display.repository')
 | 
			
		||||
      ->getFormDisplay('entity_test', 'entity_test')
 | 
			
		||||
      ->setComponent($this->card1->getName(), [
 | 
			
		||||
        'type' => 'options_select',
 | 
			
		||||
      ])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    // Create an entity.
 | 
			
		||||
    $entity = EntityTest::create([
 | 
			
		||||
      'user_id' => 1,
 | 
			
		||||
      'name' => $this->randomMachineName(),
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    // A required field without any value has a "none" option.
 | 
			
		||||
    $option = $this->assertSession()->optionExists('edit-card-1', '_none');
 | 
			
		||||
    $this->assertSame('- Select a value -', $option->getText());
 | 
			
		||||
 | 
			
		||||
    // Submit form: select invalid 'none' option.
 | 
			
		||||
    $edit = ['card_1' => '_none'];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertSession()->responseContains('This is custom message for required field.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the 'options_select' widget (multiple select).
 | 
			
		||||
   */
 | 
			
		||||
  public function testSelectListMultiple(): void {
 | 
			
		||||
    // Create an instance of the 'multiple values' field.
 | 
			
		||||
    $field = FieldConfig::create([
 | 
			
		||||
      'field_storage' => $this->card2,
 | 
			
		||||
      'bundle' => 'entity_test',
 | 
			
		||||
    ]);
 | 
			
		||||
    $field->save();
 | 
			
		||||
    \Drupal::service('entity_display.repository')
 | 
			
		||||
      ->getFormDisplay('entity_test', 'entity_test')
 | 
			
		||||
      ->setComponent($this->card2->getName(), [
 | 
			
		||||
        'type' => 'options_select',
 | 
			
		||||
      ])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    // Create an entity.
 | 
			
		||||
    $entity = EntityTest::create([
 | 
			
		||||
      'user_id' => 1,
 | 
			
		||||
      'name' => $this->randomMachineName(),
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $entity_init = clone $entity;
 | 
			
		||||
 | 
			
		||||
    // Display form: with no field data, nothing is selected.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->assertTrue($this->assertSession()->optionExists('card_2', '_none')->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_2', 0)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_2', 1)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_2', 2)->isSelected());
 | 
			
		||||
    $this->assertSession()->responseContains('Some dangerous & unescaped markup');
 | 
			
		||||
 | 
			
		||||
    // Submit form: select first and third options.
 | 
			
		||||
    $edit = ['card_2[]' => [0 => 0, 2 => 2]];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertFieldValues($entity_init, 'card_2', [0, 2]);
 | 
			
		||||
 | 
			
		||||
    // Display form: check that the right options are selected.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->assertTrue($this->assertSession()->optionExists('card_2', 0)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_2', 1)->isSelected());
 | 
			
		||||
    $this->assertTrue($this->assertSession()->optionExists('card_2', 2)->isSelected());
 | 
			
		||||
 | 
			
		||||
    // Submit form: select only first option.
 | 
			
		||||
    $edit = ['card_2[]' => [0 => 0]];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertFieldValues($entity_init, 'card_2', [0]);
 | 
			
		||||
 | 
			
		||||
    // Display form: check that the right options are selected.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->assertTrue($this->assertSession()->optionExists('card_2', 0)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_2', 1)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_2', 2)->isSelected());
 | 
			
		||||
 | 
			
		||||
    // Submit form: select the three options while the field accepts only 2.
 | 
			
		||||
    $edit = ['card_2[]' => [0 => 0, 1 => 1, 2 => 2]];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertSession()->pageTextContains('this field cannot hold more than 2 values');
 | 
			
		||||
 | 
			
		||||
    // Submit form: uncheck all options.
 | 
			
		||||
    $edit = ['card_2[]' => []];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertFieldValues($entity_init, 'card_2', []);
 | 
			
		||||
 | 
			
		||||
    // Test the 'None' option.
 | 
			
		||||
 | 
			
		||||
    // Check that the 'none' option has no effect if actual options are selected
 | 
			
		||||
    // as well.
 | 
			
		||||
    $edit = ['card_2[]' => ['_none' => '_none', 0 => 0]];
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertFieldValues($entity_init, 'card_2', [0]);
 | 
			
		||||
 | 
			
		||||
    // Check that selecting the 'none' option empties the field.
 | 
			
		||||
    $edit = ['card_2[]' => ['_none' => '_none']];
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertFieldValues($entity_init, 'card_2', []);
 | 
			
		||||
 | 
			
		||||
    // A required select list does not have an empty key.
 | 
			
		||||
    $field->setRequired(TRUE);
 | 
			
		||||
    $field->save();
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->assertEmpty($this->assertSession()->selectExists('edit-card-2')->find('xpath', 'option[@value=""]'));
 | 
			
		||||
 | 
			
		||||
    // We do not have to test that a required select list with one option is
 | 
			
		||||
    // auto-selected because the browser does it for us.
 | 
			
		||||
 | 
			
		||||
    // Test optgroups.
 | 
			
		||||
 | 
			
		||||
    // Use a callback function defining optgroups.
 | 
			
		||||
    $this->card2->setSetting('allowed_values', []);
 | 
			
		||||
    $this->card2->setSetting('allowed_values_function', '\Drupal\options_test\OptionsAllowedValues::simpleValues');
 | 
			
		||||
    $this->card2->save();
 | 
			
		||||
    $field->setRequired(FALSE);
 | 
			
		||||
    $field->save();
 | 
			
		||||
 | 
			
		||||
    // Display form: with no field data, nothing is selected.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_2', 0)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_2', 1)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_2', 2)->isSelected());
 | 
			
		||||
    $this->assertSession()->responseContains('Some dangerous & unescaped markup');
 | 
			
		||||
    $this->assertSession()->responseContains('More <script>dangerous</script> markup');
 | 
			
		||||
    $this->assertSession()->responseContains('Group 1');
 | 
			
		||||
 | 
			
		||||
    // Submit form: select first option.
 | 
			
		||||
    $edit = ['card_2[]' => [0 => 0]];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertFieldValues($entity_init, 'card_2', [0]);
 | 
			
		||||
 | 
			
		||||
    // Display form: check that the right options are selected.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->assertTrue($this->assertSession()->optionExists('card_2', 0)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_2', 1)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('card_2', 2)->isSelected());
 | 
			
		||||
 | 
			
		||||
    // Submit form: Unselect the option.
 | 
			
		||||
    $edit = ['card_2[]' => ['_none' => '_none']];
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertFieldValues($entity_init, 'card_2', []);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the 'options_select' widget (float values).
 | 
			
		||||
   */
 | 
			
		||||
  public function testSelectListFloat(): void {
 | 
			
		||||
 | 
			
		||||
    // Create an instance of the 'float value' field.
 | 
			
		||||
    $field = FieldConfig::create([
 | 
			
		||||
      'field_storage' => $this->float,
 | 
			
		||||
      'bundle' => 'entity_test',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ]);
 | 
			
		||||
    $field->save();
 | 
			
		||||
 | 
			
		||||
    $this->container
 | 
			
		||||
      ->get('entity_type.manager')
 | 
			
		||||
      ->getStorage('entity_form_display')
 | 
			
		||||
      ->load('entity_test.entity_test.default')
 | 
			
		||||
      ->setComponent($this->float->getName(), ['type' => 'options_select'])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    // Create an entity.
 | 
			
		||||
    $entity = EntityTest::create([
 | 
			
		||||
      'user_id' => 1,
 | 
			
		||||
      'name' => $this->randomMachineName(),
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    // Display form.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
 | 
			
		||||
    // With no field data, nothing is selected.
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('float', 0)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('float', 1.5)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('float', 2)->isSelected());
 | 
			
		||||
 | 
			
		||||
    // Submit form.
 | 
			
		||||
    $edit = ['float' => 1.5];
 | 
			
		||||
    $this->submitForm($edit, 'Save');
 | 
			
		||||
    $this->assertFieldValues($entity, 'float', [1.5]);
 | 
			
		||||
 | 
			
		||||
    // Display form: check that the right options are selected.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('float', 0)->isSelected());
 | 
			
		||||
    $this->assertTrue($this->assertSession()->optionExists('float', 1.5)->isSelected());
 | 
			
		||||
    $this->assertFalse($this->assertSession()->optionExists('float', 2)->isSelected());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the 'options_select' and 'options_button' widget for empty value.
 | 
			
		||||
   */
 | 
			
		||||
  public function testEmptyValue(): void {
 | 
			
		||||
    // Create an instance of the 'single value' field.
 | 
			
		||||
    $field = FieldConfig::create([
 | 
			
		||||
      'field_storage' => $this->card1,
 | 
			
		||||
      'bundle' => 'entity_test',
 | 
			
		||||
    ]);
 | 
			
		||||
    $field->save();
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
 | 
			
		||||
    $display_repository = \Drupal::service('entity_display.repository');
 | 
			
		||||
 | 
			
		||||
    // Change it to the check boxes/radio buttons widget.
 | 
			
		||||
    $display_repository->getFormDisplay('entity_test', 'entity_test')
 | 
			
		||||
      ->setComponent($this->card1->getName(), [
 | 
			
		||||
        'type' => 'options_buttons',
 | 
			
		||||
      ])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    // Create an entity.
 | 
			
		||||
    $entity = EntityTest::create([
 | 
			
		||||
      'user_id' => 1,
 | 
			
		||||
      'name' => $this->randomMachineName(),
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    // Display form: check that _none options are present and has label.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    // Verify that a test radio button has a "None" choice.
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', '//div[@id="edit-card-1"]//input[@value="_none"]');
 | 
			
		||||
    // Verify that a test radio button has a "N/A" choice..
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', '//div[@id="edit-card-1"]//label[@for="edit-card-1-none"]');
 | 
			
		||||
    $this->assertSession()->elementTextEquals('xpath', '//div[@id="edit-card-1"]//label[@for="edit-card-1-none"]', "N/A");
 | 
			
		||||
 | 
			
		||||
    // Change it to the select widget.
 | 
			
		||||
    $display_repository->getFormDisplay('entity_test', 'entity_test')
 | 
			
		||||
      ->setComponent($this->card1->getName(), [
 | 
			
		||||
        'type' => 'options_select',
 | 
			
		||||
      ])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    // Display form: check that _none options are present and has label.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    // A required field without any value has a "none" option.
 | 
			
		||||
    $option = $this->assertSession()->optionExists('edit-card-1', '_none');
 | 
			
		||||
    $this->assertSame('- None -', $option->getText());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests hook_options_list_alter().
 | 
			
		||||
   *
 | 
			
		||||
   * @see options_test_options_list_alter()
 | 
			
		||||
   */
 | 
			
		||||
  public function testOptionsListAlter(): void {
 | 
			
		||||
    $field1 = FieldConfig::create([
 | 
			
		||||
      'field_storage' => $this->card1,
 | 
			
		||||
      'bundle' => 'entity_test',
 | 
			
		||||
    ]);
 | 
			
		||||
    $field1->save();
 | 
			
		||||
 | 
			
		||||
    // Create a new field that will be altered.
 | 
			
		||||
    $card4 = FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => 'card_4',
 | 
			
		||||
      'entity_type' => 'entity_test',
 | 
			
		||||
      'type' => 'list_integer',
 | 
			
		||||
      'cardinality' => 1,
 | 
			
		||||
      'settings' => [
 | 
			
		||||
        'allowed_values' => [
 | 
			
		||||
          0 => 'Zero',
 | 
			
		||||
          1 => 'One',
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
    $card4->save();
 | 
			
		||||
 | 
			
		||||
    $field2 = FieldConfig::create([
 | 
			
		||||
      'field_storage' => $card4,
 | 
			
		||||
      'bundle' => 'entity_test',
 | 
			
		||||
    ]);
 | 
			
		||||
    $field2->save();
 | 
			
		||||
 | 
			
		||||
    /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
 | 
			
		||||
    $display_repository = \Drupal::service('entity_display.repository');
 | 
			
		||||
 | 
			
		||||
    // Change it to the check boxes/radio buttons widget.
 | 
			
		||||
    $display_repository->getFormDisplay('entity_test', 'entity_test')
 | 
			
		||||
      ->setComponent($this->card1->getName(), [
 | 
			
		||||
        'type' => 'options_select',
 | 
			
		||||
      ])
 | 
			
		||||
      ->setComponent($card4->getName(), [
 | 
			
		||||
        'type' => 'options_select',
 | 
			
		||||
      ])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    // Create an entity.
 | 
			
		||||
    $entity = EntityTest::create([
 | 
			
		||||
      'user_id' => 1,
 | 
			
		||||
      'name' => $this->randomMachineName(),
 | 
			
		||||
    ]);
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    // Display form: check that _none options are present.
 | 
			
		||||
    $this->drupalGet('entity_test/manage/' . $entity->id() . '/edit');
 | 
			
		||||
    $xpath = '//select[@id=:id]//option[@value="_none" and text()=:label]';
 | 
			
		||||
    $xpath_args = [':id' => 'edit-card-1', ':label' => '- None -'];
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', $this->assertSession()->buildXPathQuery($xpath, $xpath_args));
 | 
			
		||||
    $xpath_args = [':id' => 'edit-card-4', ':label' => '- Select something -'];
 | 
			
		||||
    $this->assertSession()->elementExists('xpath', $this->assertSession()->buildXPathQuery($xpath, $xpath_args));
 | 
			
		||||
 | 
			
		||||
    // Display form: check that options are displayed correctly.
 | 
			
		||||
    $this->assertSession()->optionExists('card_1', 0);
 | 
			
		||||
    $this->assertSession()->optionExists('card_1', 1);
 | 
			
		||||
    $this->assertSession()->optionNotExists('card_4', 0);
 | 
			
		||||
    $this->assertSession()->optionExists('card_4', 1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,386 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\options\FunctionalJavascript;
 | 
			
		||||
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
 | 
			
		||||
use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the Options field allowed values UI functionality.
 | 
			
		||||
 *
 | 
			
		||||
 * @group options
 | 
			
		||||
 * @group #slow
 | 
			
		||||
 */
 | 
			
		||||
class OptionsFieldUIAllowedValuesTest extends WebDriverTestBase {
 | 
			
		||||
 | 
			
		||||
  use FieldUiJSTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'node',
 | 
			
		||||
    'options',
 | 
			
		||||
    'field_ui',
 | 
			
		||||
    'block',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Machine name of the created content type.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $type;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Name of the option field.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $fieldName;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Admin path to manage field storage settings.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $adminPath;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Node form path for created content type.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $nodeFormPath;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->drupalPlaceBlock('local_actions_block');
 | 
			
		||||
 | 
			
		||||
    // Create test user.
 | 
			
		||||
    $admin_user = $this->drupalCreateUser([
 | 
			
		||||
      'bypass node access',
 | 
			
		||||
      'administer node fields',
 | 
			
		||||
      'administer node display',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($admin_user);
 | 
			
		||||
 | 
			
		||||
    $type = $this->drupalCreateContentType(['type' => 'plan']);
 | 
			
		||||
    $this->type = $type->id();
 | 
			
		||||
    $this->nodeFormPath = 'node/add/' . $this->type;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests option types allowed values.
 | 
			
		||||
   *
 | 
			
		||||
   * @dataProvider providerTestOptionsAllowedValues
 | 
			
		||||
   */
 | 
			
		||||
  public function testOptionsAllowedValues($option_type, $options, $is_string_option, string $add_row_method): void {
 | 
			
		||||
    $assert = $this->assertSession();
 | 
			
		||||
    $this->fieldName = 'field_options_text';
 | 
			
		||||
    $this->createOptionsField($option_type);
 | 
			
		||||
    $page = $this->getSession()->getPage();
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($this->adminPath);
 | 
			
		||||
 | 
			
		||||
    $i = 0;
 | 
			
		||||
    $expected_rows = 1;
 | 
			
		||||
    $this->assertAllowValuesRowCount(1);
 | 
			
		||||
    foreach ($options as $option_key => $option_label) {
 | 
			
		||||
      $enter_element_name = $label_element_name = "field_storage[subform][settings][allowed_values][table][$i][item][label]";
 | 
			
		||||
      $page->fillField($label_element_name, $option_label);
 | 
			
		||||
      $this->assertSession()->assertWaitOnAjaxRequest();
 | 
			
		||||
      $key_element_name = "field_storage[subform][settings][allowed_values][table][$i][item][key]";
 | 
			
		||||
 | 
			
		||||
      // Add keys if not string option list.
 | 
			
		||||
      if (!$is_string_option) {
 | 
			
		||||
        $this->pressEnterOnElement("[name=\"$label_element_name\"]");
 | 
			
		||||
        // Assert that pressing enter on label field does not create the new
 | 
			
		||||
        // row if the key field is visible.
 | 
			
		||||
        $this->assertAllowValuesRowCount($expected_rows);
 | 
			
		||||
        $enter_element_name = $key_element_name;
 | 
			
		||||
        $this->assertHasFocusByAttribute('name', $key_element_name);
 | 
			
		||||
        $page->fillField($key_element_name, $option_key);
 | 
			
		||||
        $this->assertSession()->assertWaitOnAjaxRequest();
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        $this->assertFalse($assert->fieldExists($key_element_name)->isVisible());
 | 
			
		||||
      }
 | 
			
		||||
      switch ($add_row_method) {
 | 
			
		||||
        case 'Press button':
 | 
			
		||||
          $page->pressButton('Add another item');
 | 
			
		||||
          break;
 | 
			
		||||
 | 
			
		||||
        case 'Enter button':
 | 
			
		||||
          $button = $assert->buttonExists('Add another item');
 | 
			
		||||
          $this->pressEnterOnElement('[data-drupal-selector="' . $button->getAttribute('data-drupal-selector') . '"]');
 | 
			
		||||
          break;
 | 
			
		||||
 | 
			
		||||
        case 'Enter element':
 | 
			
		||||
          // If testing using the "enter" key while focused on element there a
 | 
			
		||||
          // few different scenarios to test.
 | 
			
		||||
          switch ($i) {
 | 
			
		||||
            case 0:
 | 
			
		||||
              // For string options the machine name input can be exposed which
 | 
			
		||||
              // will mean the label input will no longer create the next row.
 | 
			
		||||
              if ($is_string_option) {
 | 
			
		||||
                $this->exposeOptionMachineName($expected_rows);
 | 
			
		||||
                $this->pressEnterOnElement("[name=\"$enter_element_name\"]");
 | 
			
		||||
                $this->assertHasFocusByAttribute('name', $key_element_name);
 | 
			
		||||
                // Ensure that pressing enter while focused on the label input
 | 
			
		||||
                // did not create a new row if the machine name field is
 | 
			
		||||
                // visible.
 | 
			
		||||
                $this->assertAllowValuesRowCount($expected_rows);
 | 
			
		||||
                $enter_element_name = $key_element_name;
 | 
			
		||||
              }
 | 
			
		||||
              break;
 | 
			
		||||
          }
 | 
			
		||||
          $this->pressEnterOnElement("[name=\"$enter_element_name\"]");
 | 
			
		||||
          break;
 | 
			
		||||
 | 
			
		||||
        default:
 | 
			
		||||
          throw new \UnexpectedValueException("Unknown method $add_row_method");
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      $i++;
 | 
			
		||||
      $expected_rows++;
 | 
			
		||||
      $this->assertSession()->waitForElementVisible('css', "[name='field_storage[subform][settings][allowed_values][table][$i][item][label]']");
 | 
			
		||||
      $this->assertHasFocusByAttribute('name', "field_storage[subform][settings][allowed_values][table][$i][item][label]");
 | 
			
		||||
      $this->assertAllowValuesRowCount($expected_rows);
 | 
			
		||||
 | 
			
		||||
      if ($is_string_option) {
 | 
			
		||||
        // Expose the key input for string options for the previous row to test
 | 
			
		||||
        // shifting focus from the label to key inputs on the previous row by
 | 
			
		||||
        // pressing enter.
 | 
			
		||||
        $this->exposeOptionMachineName($expected_rows - 1);
 | 
			
		||||
      }
 | 
			
		||||
      // Test that pressing enter on the label input on previous row will shift
 | 
			
		||||
      // focus to key input of that row.
 | 
			
		||||
      $this->pressEnterOnElement("[name=\"$label_element_name\"]");
 | 
			
		||||
      $this->assertHasFocusByAttribute('name', $key_element_name);
 | 
			
		||||
      $this->assertAllowValuesRowCount($expected_rows);
 | 
			
		||||
    }
 | 
			
		||||
    $page->pressButton('Save settings');
 | 
			
		||||
    $this->assertTrue($this->assertSession()->waitForText('Saved field_options_text configuration.'));
 | 
			
		||||
 | 
			
		||||
    $option_labels = array_values($options);
 | 
			
		||||
    $this->assertCount(3, $option_labels);
 | 
			
		||||
 | 
			
		||||
    // Test the order of the option list on node form.
 | 
			
		||||
    $this->drupalGet($this->nodeFormPath);
 | 
			
		||||
    $this->assertNodeFormOrder(['- None -', $option_labels[0], $option_labels[1], $option_labels[2]]);
 | 
			
		||||
 | 
			
		||||
    // Test the order of the option list on admin path.
 | 
			
		||||
    $this->drupalGet($this->adminPath);
 | 
			
		||||
    $this->assertOrder([$option_labels[0], $option_labels[1], $option_labels[2], ''], $is_string_option);
 | 
			
		||||
    $drag_handle = $page->find('css', '[data-drupal-selector="edit-field-storage-subform-settings-allowed-values-table-0"] .tabledrag-handle');
 | 
			
		||||
    $target = $page->find('css', '[data-drupal-selector="edit-field-storage-subform-settings-allowed-values-table-2"]');
 | 
			
		||||
 | 
			
		||||
    // Change the order the items appear.
 | 
			
		||||
    $drag_handle->dragTo($target);
 | 
			
		||||
    $this->assertOrder([$option_labels[1], $option_labels[2], $option_labels[0], ''], $is_string_option);
 | 
			
		||||
    $page->pressButton('Save settings');
 | 
			
		||||
    $this->assertTrue($this->assertSession()->waitForText('Saved field_options_text configuration.'));
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($this->nodeFormPath);
 | 
			
		||||
    $this->assertNodeFormOrder(['- None -', $option_labels[1], $option_labels[2], $option_labels[0]]);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($this->adminPath);
 | 
			
		||||
 | 
			
		||||
    // Confirm the change in order was saved.
 | 
			
		||||
    $this->assertOrder([$option_labels[1], $option_labels[2], $option_labels[0], ''], $is_string_option);
 | 
			
		||||
 | 
			
		||||
    // Delete an item.
 | 
			
		||||
    $page->pressButton('remove_row_button__1');
 | 
			
		||||
    $this->assertSession()->assertWaitOnAjaxRequest();
 | 
			
		||||
    $this->assertOrder([$option_labels[1], $option_labels[0], ''], $is_string_option);
 | 
			
		||||
    $page->pressButton('Save settings');
 | 
			
		||||
    $this->assertTrue($this->assertSession()->waitForText('Saved field_options_text configuration.'));
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($this->nodeFormPath);
 | 
			
		||||
    $this->assertNodeFormOrder(['- None -', $option_labels[1], $option_labels[0]]);
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet($this->adminPath);
 | 
			
		||||
 | 
			
		||||
    // Confirm the item removal was saved.
 | 
			
		||||
    $this->assertOrder([$option_labels[1], $option_labels[0], ''], $is_string_option);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts the order of provided option list on admin path.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $expected
 | 
			
		||||
   *   Expected order.
 | 
			
		||||
   * @param bool $is_string_option
 | 
			
		||||
   *   Whether the request is for string option list.
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertOrder($expected, $is_string_option) {
 | 
			
		||||
    $page = $this->getSession()->getPage();
 | 
			
		||||
    if ($is_string_option) {
 | 
			
		||||
      $inputs = $page->findAll('css', '.draggable .form-text.machine-name-source');
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $inputs = $page->findAll('css', '.draggable .form-text');
 | 
			
		||||
    }
 | 
			
		||||
    foreach ($expected as $step => $expected_input_value) {
 | 
			
		||||
      $value = $inputs[$step]->getValue();
 | 
			
		||||
      $this->assertSame($expected_input_value, $value, "Item $step should be $expected_input_value, but got $value");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts the order of provided option list on node form.
 | 
			
		||||
   *
 | 
			
		||||
   * @param array $expected
 | 
			
		||||
   *   Expected order.
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertNodeFormOrder($expected) {
 | 
			
		||||
    $elements = $this->assertSession()->selectExists('field_options_text')->findAll('css', 'option');
 | 
			
		||||
    $elements = array_map(function ($element) {
 | 
			
		||||
      return $element->getText();
 | 
			
		||||
    }, $elements);
 | 
			
		||||
    $this->assertSame($expected, $elements);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Helper function to create list field of a given type.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $type
 | 
			
		||||
   *   One of 'list_integer', 'list_float' or 'list_string'.
 | 
			
		||||
   */
 | 
			
		||||
  protected function createOptionsField($type) {
 | 
			
		||||
    // Create a field.
 | 
			
		||||
    FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => $this->fieldName,
 | 
			
		||||
      'entity_type' => 'node',
 | 
			
		||||
      'type' => $type,
 | 
			
		||||
    ])->save();
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'field_name' => $this->fieldName,
 | 
			
		||||
      'entity_type' => 'node',
 | 
			
		||||
      'bundle' => $this->type,
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    \Drupal::service('entity_display.repository')
 | 
			
		||||
      ->getFormDisplay('node', $this->type)
 | 
			
		||||
      ->setComponent($this->fieldName)
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    $this->adminPath = 'admin/structure/types/manage/' . $this->type . '/fields/node.' . $this->type . '.' . $this->fieldName;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Presses "Enter" on the specified element.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $selector
 | 
			
		||||
   *   Current element having focus.
 | 
			
		||||
   */
 | 
			
		||||
  private function pressEnterOnElement(string $selector): void {
 | 
			
		||||
    $javascript = <<<JS
 | 
			
		||||
      const element = document.querySelector('$selector');
 | 
			
		||||
      const event = new KeyboardEvent('keypress', { key: 'Enter', keyCode: 13, bubbles: true });
 | 
			
		||||
      element.dispatchEvent(event);
 | 
			
		||||
JS;
 | 
			
		||||
    $this->getSession()->executeScript($javascript);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Data provider for testOptionsAllowedValues().
 | 
			
		||||
   *
 | 
			
		||||
   * @return array
 | 
			
		||||
   *   Array of arrays with the following elements:
 | 
			
		||||
   *   - Option type.
 | 
			
		||||
   *   - Array of option type values.
 | 
			
		||||
   *   - Whether option type is string type or not.
 | 
			
		||||
   *   - The method which should be used to add another row to the table. The
 | 
			
		||||
   *     possible values are 'Press button', 'Enter button' or 'Enter element'.
 | 
			
		||||
   */
 | 
			
		||||
  public static function providerTestOptionsAllowedValues() {
 | 
			
		||||
    $type_cases = [
 | 
			
		||||
      'List integer' => [
 | 
			
		||||
        'list_integer',
 | 
			
		||||
        [1 => 'First', 2 => 'Second', 3 => 'Third'],
 | 
			
		||||
        FALSE,
 | 
			
		||||
      ],
 | 
			
		||||
      'List float' => [
 | 
			
		||||
        'list_float',
 | 
			
		||||
        ['0.1' => 'First', '0.2' => 'Second', '0.3' => 'Third'],
 | 
			
		||||
        FALSE,
 | 
			
		||||
      ],
 | 
			
		||||
      'List string' => [
 | 
			
		||||
        'list_string',
 | 
			
		||||
        ['0' => '0', '1' => '1', 'two' => 'two'],
 | 
			
		||||
        TRUE,
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
    // Test adding options for each option field type using several possible
 | 
			
		||||
    // methods that could be used for navigating the options list:
 | 
			
		||||
    // - Press button: add a new item by pressing the 'Add another item'
 | 
			
		||||
    // button using mouse.
 | 
			
		||||
    // - Enter button: add a new item by pressing the 'Add another item'
 | 
			
		||||
    // button using enter key on the keyboard.
 | 
			
		||||
    // - Enter element: add a new item by pressing enter on the last text
 | 
			
		||||
    // field inside the table.
 | 
			
		||||
    $test_cases = [];
 | 
			
		||||
    foreach ($type_cases as $key => $type_case) {
 | 
			
		||||
      foreach (['Press button', 'Enter button', 'Enter element'] as $add_more_method) {
 | 
			
		||||
        $test_cases["$key: $add_more_method"] = array_merge($type_case, [$add_more_method]);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return $test_cases;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Assert the count of the allowed values rows.
 | 
			
		||||
   *
 | 
			
		||||
   * @param int $expected_count
 | 
			
		||||
   *   The expected row count.
 | 
			
		||||
   */
 | 
			
		||||
  private function assertAllowValuesRowCount(int $expected_count): void {
 | 
			
		||||
    $this->assertCount(
 | 
			
		||||
      $expected_count,
 | 
			
		||||
      $this->getSession()->getPage()->findAll('css', '#allowed-values-order tr.draggable')
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Exposes the machine name input for a row.
 | 
			
		||||
   *
 | 
			
		||||
   * @param int $row
 | 
			
		||||
   *   The row number.
 | 
			
		||||
   */
 | 
			
		||||
  private function exposeOptionMachineName(int $row): void {
 | 
			
		||||
    $index = $row - 1;
 | 
			
		||||
    $rows = $this->getSession()->getPage()->findAll('css', '#allowed-values-order tr.draggable');
 | 
			
		||||
    $this->assertSession()->buttonExists('Edit', $rows[$index])->click();
 | 
			
		||||
    $this->assertSession()->waitForElementVisible('css', "[name='field_storage[subform][settings][allowed_values][table][$index][item][key]']");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts an element specified by an attribute value has focus.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $name
 | 
			
		||||
   *   The attribute name.
 | 
			
		||||
   * @param string $value
 | 
			
		||||
   *   The attribute value.
 | 
			
		||||
   *
 | 
			
		||||
   * @todo Replace with assertHasFocus() in https://drupal.org/i/3041768.
 | 
			
		||||
   */
 | 
			
		||||
  private function assertHasFocusByAttribute(string $name, string $value): void {
 | 
			
		||||
    $active_element = $this->getSession()->evaluateScript('document.activeElement');
 | 
			
		||||
    $this->assertSame($value, $active_element->attribute($name));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,202 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\options\FunctionalJavascript;
 | 
			
		||||
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
 | 
			
		||||
use Drupal\Tests\field_ui\Traits\FieldUiJSTestTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the Options field UI functionality.
 | 
			
		||||
 *
 | 
			
		||||
 * @group options
 | 
			
		||||
 * @group #slow
 | 
			
		||||
 */
 | 
			
		||||
class OptionsFieldUITest extends WebDriverTestBase {
 | 
			
		||||
 | 
			
		||||
  use FieldUiJSTestTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'node',
 | 
			
		||||
    'options',
 | 
			
		||||
    'field_ui',
 | 
			
		||||
    'block',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected $defaultTheme = 'stark';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Machine name of the created content type.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $type;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Name of the option field.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $fieldName;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Admin path to manage field storage settings.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $adminPath;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Node form path for created content type.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $nodeFormPath;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->drupalPlaceBlock('local_actions_block');
 | 
			
		||||
 | 
			
		||||
    // Create test user.
 | 
			
		||||
    $admin_user = $this->drupalCreateUser([
 | 
			
		||||
      'bypass node access',
 | 
			
		||||
      'administer node fields',
 | 
			
		||||
      'administer node display',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->drupalLogin($admin_user);
 | 
			
		||||
 | 
			
		||||
    $type = $this->drupalCreateContentType(['type' => 'plan']);
 | 
			
		||||
    $this->type = $type->id();
 | 
			
		||||
    $this->nodeFormPath = 'node/add/' . $this->type;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that the allowed options are available to the default value widget.
 | 
			
		||||
   */
 | 
			
		||||
  public function testDefaultValueOptions(): void {
 | 
			
		||||
    $page = $this->getSession()->getPage();
 | 
			
		||||
    $assert_session = $this->assertSession();
 | 
			
		||||
    $bundle_path = 'admin/structure/types/manage/' . $this->type;
 | 
			
		||||
    // Create a field of type list:string.
 | 
			
		||||
    $this->fieldUIAddNewFieldJS($bundle_path, 'test_string_list', 'Test string list', 'list_string', FALSE);
 | 
			
		||||
    $page->findField('field_storage[subform][settings][allowed_values][table][0][item][label]')->setValue('first');
 | 
			
		||||
    $assert_session->assertWaitOnAjaxRequest();
 | 
			
		||||
    $page->findField('set_default_value')->setValue(TRUE);
 | 
			
		||||
    // Assert that the option added in the subform is available to the default
 | 
			
		||||
    // value field.
 | 
			
		||||
    $this->assertSession()->waitForElement('css', '[name="default_value_input[field_test_int_list]"] option:contains("first")');
 | 
			
		||||
    $page->pressButton('Add another item');
 | 
			
		||||
    $assert_session->waitForField('field_storage[subform][settings][allowed_values][table][1][item][label]');
 | 
			
		||||
    $field_input = $page->find('css', '[name="field_storage[subform][settings][allowed_values][table][1][item][label]"]');
 | 
			
		||||
    $field_input->setValue('second');
 | 
			
		||||
    $this->assertNotNull($assert_session->waitForElement('css', '[name="default_value_input[field_test_string_list]"] option:contains("second")'));
 | 
			
		||||
    $assert_session->optionExists('default_value_input[field_test_string_list]', 'second');
 | 
			
		||||
    $page->selectFieldOption('default_value_input[field_test_string_list]', 'second');
 | 
			
		||||
    $page->find('css', '.ui-dialog-buttonset button:contains("Save")')->press();
 | 
			
		||||
    $this->assertTrue($assert_session->waitForText('Saved Test string list configuration.'));
 | 
			
		||||
 | 
			
		||||
    // Create a field of type list:integer.
 | 
			
		||||
    $this->fieldUIAddNewFieldJS($bundle_path, 'test_int_list', 'Test int list', 'list_integer', FALSE);
 | 
			
		||||
    $page->findField('field_storage[subform][settings][allowed_values][table][0][item][label]')->setValue('first');
 | 
			
		||||
    $assert_session->assertWaitOnAjaxRequest();
 | 
			
		||||
    $page->findField('set_default_value')->setValue(TRUE);
 | 
			
		||||
    $page->selectFieldOption('default_value_input[field_test_int_list]', 'first');
 | 
			
		||||
    // Assert that no validation is performed.
 | 
			
		||||
    $assert_session->statusMessageNotContains('Value field is required.');
 | 
			
		||||
    $field_input = $page->find('css', '[name="field_storage[subform][settings][allowed_values][table][0][item][key]"]');
 | 
			
		||||
    $field_input->setValue(1);
 | 
			
		||||
    $this->assertSession()->assertWaitOnAjaxRequest();
 | 
			
		||||
    $page->checkField('set_default_value');
 | 
			
		||||
    $this->assertSession()->waitForElement('css', '[name="default_value_input[field_test_int_list]"] option:contains("first")');
 | 
			
		||||
    // Assert that the option added in the subform is available to the default
 | 
			
		||||
    // value field.
 | 
			
		||||
    $this->assertSession()->optionExists('default_value_input[field_test_int_list]', 'first');
 | 
			
		||||
    $page->selectFieldOption('default_value_input[field_test_int_list]', 'first');
 | 
			
		||||
    $page->find('css', '.ui-dialog-buttonset button:contains("Save")')->press();
 | 
			
		||||
    $this->assertTrue($this->assertSession()->waitForText('Saved Test int list configuration.'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Helper function to create list field of a given type.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $type
 | 
			
		||||
   *   One of 'list_integer', 'list_float' or 'list_string'.
 | 
			
		||||
   */
 | 
			
		||||
  protected function createOptionsField($type): void {
 | 
			
		||||
    // Create a field.
 | 
			
		||||
    FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => $this->fieldName,
 | 
			
		||||
      'entity_type' => 'node',
 | 
			
		||||
      'type' => $type,
 | 
			
		||||
    ])->save();
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'field_name' => $this->fieldName,
 | 
			
		||||
      'entity_type' => 'node',
 | 
			
		||||
      'bundle' => $this->type,
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    \Drupal::service('entity_display.repository')
 | 
			
		||||
      ->getFormDisplay('node', $this->type)
 | 
			
		||||
      ->setComponent($this->fieldName)
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    $this->adminPath = 'admin/structure/types/manage/' . $this->type . '/fields/node.' . $this->type . '.' . $this->fieldName;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests `list_string` machine name with special characters.
 | 
			
		||||
   */
 | 
			
		||||
  public function testMachineNameSpecialCharacters(): void {
 | 
			
		||||
    $this->fieldName = 'field_options_text';
 | 
			
		||||
    $this->createOptionsField('list_string');
 | 
			
		||||
    $this->drupalGet($this->adminPath);
 | 
			
		||||
 | 
			
		||||
    $label_element_name = "field_storage[subform][settings][allowed_values][table][0][item][label]";
 | 
			
		||||
    $this->getSession()->getPage()->fillField($label_element_name, 'Hello world');
 | 
			
		||||
    $this->assertSession()->assertWaitOnAjaxRequest();
 | 
			
		||||
    $this->exposeOptionMachineName(1);
 | 
			
		||||
 | 
			
		||||
    $key_element_name = "field_storage[subform][settings][allowed_values][table][0][item][key]";
 | 
			
		||||
 | 
			
		||||
    // Ensure that the machine name was generated correctly.
 | 
			
		||||
    $this->assertSession()->fieldValueEquals($key_element_name, 'hello_world');
 | 
			
		||||
 | 
			
		||||
    // Ensure that the machine name can be overridden with a value that includes
 | 
			
		||||
    // special characters.
 | 
			
		||||
    $this->getSession()->getPage()->fillField($key_element_name, '.hello #world');
 | 
			
		||||
    $this->assertSession()->assertWaitOnAjaxRequest();
 | 
			
		||||
    $this->getSession()->getPage()->pressButton('Save settings');
 | 
			
		||||
    $this->assertSession()->statusMessageContains("Saved {$this->fieldName} configuration.");
 | 
			
		||||
 | 
			
		||||
    // Ensure that the machine name was saved correctly.
 | 
			
		||||
    $allowed_values = FieldStorageConfig::loadByName('node', $this->fieldName)
 | 
			
		||||
      ->getSetting('allowed_values');
 | 
			
		||||
    $this->assertSame(['.hello #world'], array_keys($allowed_values));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Exposes the machine name input for a row.
 | 
			
		||||
   *
 | 
			
		||||
   * @param int $row
 | 
			
		||||
   *   The row number.
 | 
			
		||||
   */
 | 
			
		||||
  private function exposeOptionMachineName(int $row): void {
 | 
			
		||||
    $index = $row - 1;
 | 
			
		||||
    $rows = $this->getSession()->getPage()->findAll('css', '#allowed-values-order tr.draggable');
 | 
			
		||||
    $this->assertSession()->buttonExists('Edit', $rows[$index])->click();
 | 
			
		||||
    $this->assertSession()->waitForElementVisible('css', "[name='field_storage[subform][settings][allowed_values][table][$index][item][key]']");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,99 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\options\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\EntityInterface;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestRev;
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the options allowed values api.
 | 
			
		||||
 *
 | 
			
		||||
 * @group options
 | 
			
		||||
 */
 | 
			
		||||
class OptionsDynamicValuesApiTest extends OptionsFieldUnitTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'options_test',
 | 
			
		||||
    'node',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The created entity.
 | 
			
		||||
   */
 | 
			
		||||
  protected EntityInterface $entity;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->installEntitySchema('entity_test_rev');
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
 | 
			
		||||
    $this->fieldStorage = FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => 'test_options',
 | 
			
		||||
      'entity_type' => 'entity_test_rev',
 | 
			
		||||
      'type' => 'list_string',
 | 
			
		||||
      'cardinality' => 1,
 | 
			
		||||
      'settings' => [
 | 
			
		||||
        'allowed_values_function' => '\Drupal\options_test\OptionsAllowedValues::dynamicValues',
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->fieldStorage->save();
 | 
			
		||||
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'field_name' => 'test_options',
 | 
			
		||||
      'entity_type' => 'entity_test_rev',
 | 
			
		||||
      'bundle' => 'entity_test_rev',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ])->save();
 | 
			
		||||
    \Drupal::service('entity_display.repository')
 | 
			
		||||
      ->getFormDisplay('entity_test_rev', 'entity_test_rev')
 | 
			
		||||
      ->setComponent('test_options', [
 | 
			
		||||
        'type' => 'options_select',
 | 
			
		||||
      ])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    // Create an entity and prepare test data that will be used by
 | 
			
		||||
    // \Drupal\options_test\OptionsAllowedValues::dynamicValues().
 | 
			
		||||
    $values = [
 | 
			
		||||
      'user_id' => 2,
 | 
			
		||||
      'name' => $this->randomMachineName(),
 | 
			
		||||
    ];
 | 
			
		||||
    $this->entity = EntityTestRev::create($values);
 | 
			
		||||
    $this->entity->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests options_allowed_values().
 | 
			
		||||
   *
 | 
			
		||||
   * @see \Drupal\options_test\OptionsAllowedValues::dynamicValues()
 | 
			
		||||
   */
 | 
			
		||||
  public function testOptionsAllowedValues(): void {
 | 
			
		||||
    // Test allowed values without passed $items.
 | 
			
		||||
    $values = options_allowed_values($this->fieldStorage);
 | 
			
		||||
    $this->assertEquals([], $values);
 | 
			
		||||
 | 
			
		||||
    $values = options_allowed_values($this->fieldStorage, $this->entity);
 | 
			
		||||
 | 
			
		||||
    $expected_values = [
 | 
			
		||||
      $this->entity->label(),
 | 
			
		||||
      $this->entity->toUrl()->toString(),
 | 
			
		||||
      $this->entity->uuid(),
 | 
			
		||||
      $this->entity->bundle(),
 | 
			
		||||
    ];
 | 
			
		||||
    $expected_values = array_combine($expected_values, $expected_values);
 | 
			
		||||
    $this->assertEquals($expected_values, $values);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,107 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\options\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\EntityInterface;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTestRev;
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the Options field allowed values function.
 | 
			
		||||
 *
 | 
			
		||||
 * @group options
 | 
			
		||||
 */
 | 
			
		||||
class OptionsDynamicValuesValidationTest extends OptionsFieldUnitTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'options_test',
 | 
			
		||||
    'node',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The created entity.
 | 
			
		||||
   */
 | 
			
		||||
  protected EntityInterface $entity;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Test data.
 | 
			
		||||
   */
 | 
			
		||||
  protected array $test;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->installEntitySchema('entity_test_rev');
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
 | 
			
		||||
    FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => 'test_options',
 | 
			
		||||
      'entity_type' => 'entity_test_rev',
 | 
			
		||||
      'type' => 'list_string',
 | 
			
		||||
      'cardinality' => 1,
 | 
			
		||||
      'settings' => [
 | 
			
		||||
        'allowed_values_function' => '\Drupal\options_test\OptionsAllowedValues::dynamicValues',
 | 
			
		||||
      ],
 | 
			
		||||
    ])->save();
 | 
			
		||||
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'field_name' => 'test_options',
 | 
			
		||||
      'entity_type' => 'entity_test_rev',
 | 
			
		||||
      'bundle' => 'entity_test_rev',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ])->save();
 | 
			
		||||
    \Drupal::service('entity_display.repository')
 | 
			
		||||
      ->getFormDisplay('entity_test_rev', 'entity_test_rev')
 | 
			
		||||
      ->setComponent('test_options', [
 | 
			
		||||
        'type' => 'options_select',
 | 
			
		||||
      ])
 | 
			
		||||
      ->save();
 | 
			
		||||
 | 
			
		||||
    // Create an entity and prepare test data that will be used by
 | 
			
		||||
    // \Drupal\options_test\OptionsAllowedValues::dynamicValues().
 | 
			
		||||
    $values = [
 | 
			
		||||
      'user_id' => 2,
 | 
			
		||||
      'name' => $this->randomMachineName(),
 | 
			
		||||
    ];
 | 
			
		||||
    $this->entity = EntityTestRev::create($values);
 | 
			
		||||
    $this->entity->save();
 | 
			
		||||
 | 
			
		||||
    $this->test = [
 | 
			
		||||
      'label' => $this->entity->label(),
 | 
			
		||||
      'uuid' => $this->entity->uuid(),
 | 
			
		||||
      'bundle' => $this->entity->bundle(),
 | 
			
		||||
      'uri' => $this->entity->toUrl()->toString(),
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that allowed values function gets the entity.
 | 
			
		||||
   */
 | 
			
		||||
  public function testDynamicAllowedValues(): void {
 | 
			
		||||
    // Verify that validation passes against every value we had.
 | 
			
		||||
    foreach ($this->test as $key => $value) {
 | 
			
		||||
      $this->entity->test_options->value = $value;
 | 
			
		||||
      $violations = $this->entity->test_options->validate();
 | 
			
		||||
      $this->assertCount(0, $violations, "$key is a valid value");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Now verify that validation does not pass against anything else.
 | 
			
		||||
    foreach ($this->test as $key => $value) {
 | 
			
		||||
      $this->entity->test_options->value = is_numeric($value) ? (100 - $value) : ('X' . $value);
 | 
			
		||||
      $violations = $this->entity->test_options->validate();
 | 
			
		||||
      $this->assertCount(1, $violations, "$key is not a valid value");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										111
									
								
								web/core/modules/options/tests/src/Kernel/OptionsFieldTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								web/core/modules/options/tests/src/Kernel/OptionsFieldTest.php
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,111 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\options\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTest;
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests for the 'Options' field types.
 | 
			
		||||
 *
 | 
			
		||||
 * @group options
 | 
			
		||||
 */
 | 
			
		||||
class OptionsFieldTest extends OptionsFieldUnitTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['options'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that allowed values can be updated.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUpdateAllowedValues(): void {
 | 
			
		||||
    // All three options appear.
 | 
			
		||||
    $entity = EntityTest::create();
 | 
			
		||||
    $form = \Drupal::service('entity.form_builder')->getForm($entity);
 | 
			
		||||
    $this->assertArrayHasKey(1, $form[$this->fieldName]['widget'], 'Option 1 exists');
 | 
			
		||||
    $this->assertArrayHasKey(2, $form[$this->fieldName]['widget'], 'Option 2 exists');
 | 
			
		||||
    $this->assertArrayHasKey(3, $form[$this->fieldName]['widget'], 'Option 3 exists');
 | 
			
		||||
 | 
			
		||||
    // Use one of the values in an actual entity, and check that this value
 | 
			
		||||
    // cannot be removed from the list.
 | 
			
		||||
    $entity = EntityTest::create();
 | 
			
		||||
    $entity->{$this->fieldName}->value = 1;
 | 
			
		||||
    $entity->save();
 | 
			
		||||
    $this->fieldStorage->setSetting('allowed_values', [2 => 'Two']);
 | 
			
		||||
    try {
 | 
			
		||||
      $this->fieldStorage->save();
 | 
			
		||||
      $this->fail('Cannot update a list field storage to not include keys with existing data.');
 | 
			
		||||
    }
 | 
			
		||||
    catch (FieldStorageDefinitionUpdateForbiddenException) {
 | 
			
		||||
      // Expected exception; just continue testing.
 | 
			
		||||
    }
 | 
			
		||||
    // Empty the value, so that we can actually remove the option.
 | 
			
		||||
    unset($entity->{$this->fieldName});
 | 
			
		||||
    $entity->save();
 | 
			
		||||
 | 
			
		||||
    // Removed options do not appear.
 | 
			
		||||
    $this->fieldStorage->setSetting('allowed_values', [2 => 'Two']);
 | 
			
		||||
    $this->fieldStorage->save();
 | 
			
		||||
    $entity = EntityTest::create();
 | 
			
		||||
    $form = \Drupal::service('entity.form_builder')->getForm($entity);
 | 
			
		||||
    $this->assertArrayNotHasKey(1, $form[$this->fieldName]['widget'], 'Option 1 does not exist');
 | 
			
		||||
    $this->assertArrayHasKey(2, $form[$this->fieldName]['widget'], 'Option 2 exists');
 | 
			
		||||
    $this->assertArrayNotHasKey(3, $form[$this->fieldName]['widget'], 'Option 3 does not exist');
 | 
			
		||||
 | 
			
		||||
    // Completely new options appear.
 | 
			
		||||
    $this->fieldStorage->setSetting('allowed_values', [10 => 'Update', 20 => 'Twenty']);
 | 
			
		||||
    $this->fieldStorage->save();
 | 
			
		||||
    // The entity holds an outdated field object with the old allowed values
 | 
			
		||||
    // setting, so we need to reinitialize the entity object.
 | 
			
		||||
    $entity = EntityTest::create();
 | 
			
		||||
    $form = \Drupal::service('entity.form_builder')->getForm($entity);
 | 
			
		||||
    $this->assertArrayNotHasKey(1, $form[$this->fieldName]['widget'], 'Option 1 does not exist');
 | 
			
		||||
    $this->assertArrayNotHasKey(2, $form[$this->fieldName]['widget'], 'Option 2 does not exist');
 | 
			
		||||
    $this->assertArrayNotHasKey(3, $form[$this->fieldName]['widget'], 'Option 3 does not exist');
 | 
			
		||||
    $this->assertArrayHasKey(10, $form[$this->fieldName]['widget'], 'Option 10 exists');
 | 
			
		||||
    $this->assertArrayHasKey(20, $form[$this->fieldName]['widget'], 'Option 20 exists');
 | 
			
		||||
 | 
			
		||||
    // Options are reset when a new field with the same name is created.
 | 
			
		||||
    $this->fieldStorage->delete();
 | 
			
		||||
    FieldStorageConfig::create($this->fieldStorageDefinition)->save();
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'field_name' => $this->fieldName,
 | 
			
		||||
      'entity_type' => 'entity_test',
 | 
			
		||||
      'bundle' => 'entity_test',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ])->save();
 | 
			
		||||
    \Drupal::service('entity_display.repository')
 | 
			
		||||
      ->getFormDisplay('entity_test', 'entity_test')
 | 
			
		||||
      ->setComponent($this->fieldName, [
 | 
			
		||||
        'type' => 'options_buttons',
 | 
			
		||||
      ])
 | 
			
		||||
      ->save();
 | 
			
		||||
    $entity = EntityTest::create();
 | 
			
		||||
    $form = \Drupal::service('entity.form_builder')->getForm($entity);
 | 
			
		||||
    $this->assertArrayHasKey(1, $form[$this->fieldName]['widget'], 'Option 1 exists');
 | 
			
		||||
    $this->assertArrayHasKey(2, $form[$this->fieldName]['widget'], 'Option 2 exists');
 | 
			
		||||
    $this->assertArrayHasKey(3, $form[$this->fieldName]['widget'], 'Option 3 exists');
 | 
			
		||||
 | 
			
		||||
    // Test the generateSampleValue() method.
 | 
			
		||||
    $entity = EntityTest::create();
 | 
			
		||||
    $entity->{$this->fieldName}->generateSampleItems();
 | 
			
		||||
    $this->entityValidateAndSave($entity);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests that ::generateSampleItems does not fail with empty allowed values.
 | 
			
		||||
   */
 | 
			
		||||
  public function testGenerateSampleItemsWithNoAllowedValues(): void {
 | 
			
		||||
    $this->fieldStorage->setSetting('allowed_values', [])->save();
 | 
			
		||||
    $entity = EntityTest::create();
 | 
			
		||||
    $value = $entity->{$this->fieldName}->generateSampleItems();
 | 
			
		||||
    $this->assertNull($value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,81 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\options\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\Tests\field\Kernel\FieldKernelTestBase;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Base class for Options module integration tests.
 | 
			
		||||
 */
 | 
			
		||||
abstract class OptionsFieldUnitTestBase extends FieldKernelTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = ['options'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The field name used in the test.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $fieldName = 'test_options';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The field storage definition used to created the field storage.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $fieldStorageDefinition;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The list field storage used in the test.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\field\Entity\FieldStorageConfig
 | 
			
		||||
   */
 | 
			
		||||
  protected $fieldStorage;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The list field used in the test.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\field\Entity\FieldConfig
 | 
			
		||||
   */
 | 
			
		||||
  protected $field;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp(): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->fieldStorageDefinition = [
 | 
			
		||||
      'field_name' => $this->fieldName,
 | 
			
		||||
      'entity_type' => 'entity_test',
 | 
			
		||||
      'type' => 'list_integer',
 | 
			
		||||
      'cardinality' => 1,
 | 
			
		||||
      'settings' => [
 | 
			
		||||
        'allowed_values' => [1 => 'One', 2 => 'Two', 3 => 'Three'],
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
    $this->fieldStorage = FieldStorageConfig::create($this->fieldStorageDefinition);
 | 
			
		||||
    $this->fieldStorage->save();
 | 
			
		||||
 | 
			
		||||
    $this->field = FieldConfig::create([
 | 
			
		||||
      'field_storage' => $this->fieldStorage,
 | 
			
		||||
      'bundle' => 'entity_test',
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->field->save();
 | 
			
		||||
 | 
			
		||||
    \Drupal::service('entity_display.repository')
 | 
			
		||||
      ->getFormDisplay('entity_test', 'entity_test')
 | 
			
		||||
      ->setComponent($this->fieldName, [
 | 
			
		||||
        'type' => 'options_buttons',
 | 
			
		||||
      ])
 | 
			
		||||
      ->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,36 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\options\Kernel;
 | 
			
		||||
 | 
			
		||||
use Drupal\entity_test\Entity\EntityTest;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests the Options field type formatters.
 | 
			
		||||
 *
 | 
			
		||||
 * @group options
 | 
			
		||||
 * @see \Drupal\options\Plugin\Field\FieldFormatter\OptionsDefaultFormatter
 | 
			
		||||
 * @see \Drupal\options\Plugin\Field\FieldFormatter\OptionsKeyFormatter
 | 
			
		||||
 */
 | 
			
		||||
class OptionsFormattersTest extends OptionsFieldUnitTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the formatters.
 | 
			
		||||
   */
 | 
			
		||||
  public function testFormatter(): void {
 | 
			
		||||
    $entity = EntityTest::create();
 | 
			
		||||
    $entity->{$this->fieldName}->value = 1;
 | 
			
		||||
 | 
			
		||||
    $items = $entity->get($this->fieldName);
 | 
			
		||||
 | 
			
		||||
    $build = $items->view();
 | 
			
		||||
    $this->assertEquals('list_default', $build['#formatter'], 'Ensure to fall back to the default formatter.');
 | 
			
		||||
    $this->assertEquals('One', $build[0]['#markup']);
 | 
			
		||||
 | 
			
		||||
    $build = $items->view(['type' => 'list_key']);
 | 
			
		||||
    $this->assertEquals('list_key', $build['#formatter'], 'The chosen formatter is used.');
 | 
			
		||||
    $this->assertEquals(1, (string) $build[0]['#markup']);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,107 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\options\Kernel\Views;
 | 
			
		||||
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
 | 
			
		||||
use Drupal\views\Views;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests file views data.
 | 
			
		||||
 *
 | 
			
		||||
 * @group file
 | 
			
		||||
 */
 | 
			
		||||
class FileViewsDataTest extends ViewsKernelTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'file',
 | 
			
		||||
    'views',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'user',
 | 
			
		||||
    'field',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp($import_test_views = TRUE): void {
 | 
			
		||||
    parent::setUp($import_test_views);
 | 
			
		||||
    $this->installEntitySchema('entity_test');
 | 
			
		||||
    $this->installEntitySchema('entity_test_mul');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests views data generated for file field relationship.
 | 
			
		||||
   *
 | 
			
		||||
   * @see file_field_views_data()
 | 
			
		||||
   * @see file_field_views_data_views_data_alter()
 | 
			
		||||
   */
 | 
			
		||||
  public function testRelationshipViewsData(): void {
 | 
			
		||||
    // Create file field to entity_test.
 | 
			
		||||
    FieldStorageConfig::create([
 | 
			
		||||
      'entity_type' => 'entity_test',
 | 
			
		||||
      'field_name' => 'field_base_file',
 | 
			
		||||
      'type' => 'file',
 | 
			
		||||
    ])->save();
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'entity_type' => 'entity_test',
 | 
			
		||||
      'field_name' => 'field_base_file',
 | 
			
		||||
      'bundle' => 'entity_test',
 | 
			
		||||
    ])->save();
 | 
			
		||||
    // Check the generated views data.
 | 
			
		||||
    $views_data = Views::viewsData()->get('entity_test__field_base_file');
 | 
			
		||||
    $relationship = $views_data['field_base_file_target_id']['relationship'];
 | 
			
		||||
    $this->assertEquals('standard', $relationship['id']);
 | 
			
		||||
    $this->assertEquals('file_managed', $relationship['base']);
 | 
			
		||||
    $this->assertEquals('fid', $relationship['base field']);
 | 
			
		||||
    $this->assertEquals('file', $relationship['entity type']);
 | 
			
		||||
    // Check the backwards reference.
 | 
			
		||||
    $views_data = Views::viewsData()->get('file_managed');
 | 
			
		||||
    $relationship = $views_data['reverse_field_base_file_entity_test']['relationship'];
 | 
			
		||||
    $this->assertEquals('entity_reverse', $relationship['id']);
 | 
			
		||||
    $this->assertEquals('entity_test', $relationship['base']);
 | 
			
		||||
    $this->assertEquals('id', $relationship['base field']);
 | 
			
		||||
    $this->assertEquals('entity_test__field_base_file', $relationship['field table']);
 | 
			
		||||
    $this->assertEquals('field_base_file_target_id', $relationship['field field']);
 | 
			
		||||
    $this->assertEquals('field_base_file', $relationship['field_name']);
 | 
			
		||||
    $this->assertEquals('entity_test', $relationship['entity_type']);
 | 
			
		||||
    $this->assertEquals(['field' => 'deleted', 'value' => 0, 'numeric' => TRUE], $relationship['join_extra'][0]);
 | 
			
		||||
 | 
			
		||||
    // Create file field to entity_test_mul.
 | 
			
		||||
    FieldStorageConfig::create([
 | 
			
		||||
      'entity_type' => 'entity_test_mul',
 | 
			
		||||
      'field_name' => 'field_data_file',
 | 
			
		||||
      'type' => 'file',
 | 
			
		||||
    ])->save();
 | 
			
		||||
    FieldConfig::create([
 | 
			
		||||
      'entity_type' => 'entity_test_mul',
 | 
			
		||||
      'field_name' => 'field_data_file',
 | 
			
		||||
      'bundle' => 'entity_test_mul',
 | 
			
		||||
    ])->save();
 | 
			
		||||
    // Check the generated views data.
 | 
			
		||||
    $views_data = Views::viewsData()->get('entity_test_mul__field_data_file');
 | 
			
		||||
    $relationship = $views_data['field_data_file_target_id']['relationship'];
 | 
			
		||||
    $this->assertEquals('standard', $relationship['id']);
 | 
			
		||||
    $this->assertEquals('file_managed', $relationship['base']);
 | 
			
		||||
    $this->assertEquals('fid', $relationship['base field']);
 | 
			
		||||
    $this->assertEquals('file', $relationship['entity type']);
 | 
			
		||||
    // Check the backwards reference.
 | 
			
		||||
    $views_data = Views::viewsData()->get('file_managed');
 | 
			
		||||
    $relationship = $views_data['reverse_field_data_file_entity_test_mul']['relationship'];
 | 
			
		||||
    $this->assertEquals('entity_reverse', $relationship['id']);
 | 
			
		||||
    $this->assertEquals('entity_test_mul_property_data', $relationship['base']);
 | 
			
		||||
    $this->assertEquals('id', $relationship['base field']);
 | 
			
		||||
    $this->assertEquals('entity_test_mul__field_data_file', $relationship['field table']);
 | 
			
		||||
    $this->assertEquals('field_data_file_target_id', $relationship['field field']);
 | 
			
		||||
    $this->assertEquals('field_data_file', $relationship['field_name']);
 | 
			
		||||
    $this->assertEquals('entity_test_mul', $relationship['entity_type']);
 | 
			
		||||
    $this->assertEquals(['field' => 'deleted', 'value' => 0, 'numeric' => TRUE], $relationship['join_extra'][0]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,51 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\options\Kernel\Views;
 | 
			
		||||
 | 
			
		||||
use Drupal\views\Views;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests options list argument for views.
 | 
			
		||||
 *
 | 
			
		||||
 * @see \Drupal\options\Plugin\views\argument\NumberListField.
 | 
			
		||||
 * @group views
 | 
			
		||||
 */
 | 
			
		||||
class OptionsListArgumentTest extends OptionsTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Views used by this test.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  public static $testViews = ['test_options_list_argument_numeric', 'test_options_list_argument_string'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the options field argument.
 | 
			
		||||
   */
 | 
			
		||||
  public function testViewsTestOptionsListArgument(): void {
 | 
			
		||||
    $view = Views::getView('test_options_list_argument_numeric');
 | 
			
		||||
    $this->executeView($view, [1]);
 | 
			
		||||
 | 
			
		||||
    $resultset = [
 | 
			
		||||
      ['nid' => $this->nodes[0]->nid->value],
 | 
			
		||||
      ['nid' => $this->nodes[1]->nid->value],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $column_map = ['nid' => 'nid'];
 | 
			
		||||
    $this->assertIdenticalResultset($view, $resultset, $column_map);
 | 
			
		||||
 | 
			
		||||
    $view = Views::getView('test_options_list_argument_string');
 | 
			
		||||
    $this->executeView($view, ['man', 'woman']);
 | 
			
		||||
 | 
			
		||||
    $resultset = [
 | 
			
		||||
      ['nid' => $this->nodes[0]->nid->value],
 | 
			
		||||
      ['nid' => $this->nodes[1]->nid->value],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $column_map = ['nid' => 'nid'];
 | 
			
		||||
    $this->assertIdenticalResultset($view, $resultset, $column_map);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,114 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\options\Kernel\Views;
 | 
			
		||||
 | 
			
		||||
use Drupal\views\Views;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests options list filter for views.
 | 
			
		||||
 *
 | 
			
		||||
 * @see \Drupal\field\Plugin\views\filter\ListField.
 | 
			
		||||
 * @group views
 | 
			
		||||
 */
 | 
			
		||||
class OptionsListFilterTest extends OptionsTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Views used by this test.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  public static $testViews = ['test_options_list_filter'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests options list field filter.
 | 
			
		||||
   */
 | 
			
		||||
  public function testViewsTestOptionsListFilter(): void {
 | 
			
		||||
    $view = Views::getView('test_options_list_filter');
 | 
			
		||||
    $this->executeView($view);
 | 
			
		||||
 | 
			
		||||
    $resultset = [
 | 
			
		||||
      ['nid' => $this->nodes[0]->nid->value],
 | 
			
		||||
      ['nid' => $this->nodes[1]->nid->value],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $column_map = ['nid' => 'nid'];
 | 
			
		||||
    $this->assertIdenticalResultset($view, $resultset, $column_map);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests options list field filter when grouped.
 | 
			
		||||
   */
 | 
			
		||||
  public function testViewsTestOptionsListGroupedFilter(): void {
 | 
			
		||||
    $view = Views::getView('test_options_list_filter');
 | 
			
		||||
 | 
			
		||||
    $filters = [
 | 
			
		||||
      'field_test_list_string_value' => [
 | 
			
		||||
        'id' => 'field_test_list_string_value',
 | 
			
		||||
        'table' => 'field_data_field_test_list_string',
 | 
			
		||||
        'field' => 'field_test_list_string_value',
 | 
			
		||||
        'relationship' => 'none',
 | 
			
		||||
        'group_type' => 'group',
 | 
			
		||||
        'admin_label' => '',
 | 
			
		||||
        'operator' => 'or',
 | 
			
		||||
        'value' => [
 | 
			
		||||
          'man' => 'man',
 | 
			
		||||
          'woman' => 'woman',
 | 
			
		||||
        ],
 | 
			
		||||
        'group' => '1',
 | 
			
		||||
        'exposed' => TRUE,
 | 
			
		||||
        'expose' => [
 | 
			
		||||
          'operator_id' => 'field_test_list_string_value_op',
 | 
			
		||||
          'label' => 'list-text',
 | 
			
		||||
          'description' => '',
 | 
			
		||||
          'identifier' => 'field_test_list_string_value',
 | 
			
		||||
        ],
 | 
			
		||||
        'is_grouped' => TRUE,
 | 
			
		||||
        'group_info' => [
 | 
			
		||||
          'label' => 'list-text (field_list_text)',
 | 
			
		||||
          'description' => '',
 | 
			
		||||
          'identifier' => 'field_test_list_string_value',
 | 
			
		||||
          'optional' => TRUE,
 | 
			
		||||
          'widget' => 'radios',
 | 
			
		||||
          'multiple' => TRUE,
 | 
			
		||||
          'remember' => FALSE,
 | 
			
		||||
          'default_group' => '1',
 | 
			
		||||
          'group_items' => [
 | 
			
		||||
            1 => [
 | 
			
		||||
              'title' => 'First',
 | 
			
		||||
              'operator' => 'or',
 | 
			
		||||
              'value' => [
 | 
			
		||||
                $this->fieldValues[0] => $this->fieldValues[0],
 | 
			
		||||
              ],
 | 
			
		||||
            ],
 | 
			
		||||
            2 => [
 | 
			
		||||
              'title' => 'Second',
 | 
			
		||||
              'operator' => 'or',
 | 
			
		||||
              'value' => [
 | 
			
		||||
                $this->fieldValues[1] => $this->fieldValues[1],
 | 
			
		||||
              ],
 | 
			
		||||
            ],
 | 
			
		||||
          ],
 | 
			
		||||
        ],
 | 
			
		||||
        'reduce_duplicates' => '',
 | 
			
		||||
        'plugin_id' => 'list_field',
 | 
			
		||||
      ],
 | 
			
		||||
    ];
 | 
			
		||||
    $view->setDisplay();
 | 
			
		||||
    $view->displayHandlers->get('default')->overrideOption('filters', $filters);
 | 
			
		||||
 | 
			
		||||
    $view->storage->save();
 | 
			
		||||
 | 
			
		||||
    $this->executeView($view);
 | 
			
		||||
 | 
			
		||||
    $resultset = [
 | 
			
		||||
      ['nid' => $this->nodes[0]->nid->value],
 | 
			
		||||
      ['nid' => $this->nodes[1]->nid->value],
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $column_map = ['nid' => 'nid'];
 | 
			
		||||
    $this->assertIdenticalResultset($view, $resultset, $column_map);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,127 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\options\Kernel\Views;
 | 
			
		||||
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
use Drupal\node\Entity\Node;
 | 
			
		||||
use Drupal\node\Entity\NodeType;
 | 
			
		||||
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
 | 
			
		||||
use Drupal\views\Tests\ViewTestData;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Base class for options views tests.
 | 
			
		||||
 */
 | 
			
		||||
abstract class OptionsTestBase extends ViewsKernelTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'options',
 | 
			
		||||
    'options_test_views',
 | 
			
		||||
    'node',
 | 
			
		||||
    'user',
 | 
			
		||||
    'field',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Stores the nodes used for the different tests.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $nodes = [];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Stores the field values used for the different tests.
 | 
			
		||||
   *
 | 
			
		||||
   * @var array
 | 
			
		||||
   */
 | 
			
		||||
  protected $fieldValues = [];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The used field names.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string[]
 | 
			
		||||
   */
 | 
			
		||||
  protected $fieldNames;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp($import_test_views = TRUE): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
    $this->mockStandardInstall();
 | 
			
		||||
 | 
			
		||||
    ViewTestData::createTestViews(static::class, ['options_test_views']);
 | 
			
		||||
 | 
			
		||||
    $settings = [];
 | 
			
		||||
    $settings['type'] = 'article';
 | 
			
		||||
    $settings['title'] = $this->randomString();
 | 
			
		||||
    $settings['field_test_list_string'][]['value'] = $this->fieldValues[0];
 | 
			
		||||
    $settings['field_test_list_integer'][]['value'] = 0;
 | 
			
		||||
 | 
			
		||||
    $node = Node::create($settings);
 | 
			
		||||
    $node->save();
 | 
			
		||||
 | 
			
		||||
    $this->nodes[] = $node;
 | 
			
		||||
    $node = $node->createDuplicate();
 | 
			
		||||
    $node->save();
 | 
			
		||||
    $this->nodes[] = $node;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Provides a workaround for the inability to use the standard profile.
 | 
			
		||||
   *
 | 
			
		||||
   * @see https://www.drupal.org/node/1708692
 | 
			
		||||
   */
 | 
			
		||||
  protected function mockStandardInstall() {
 | 
			
		||||
    $this->installEntitySchema('user');
 | 
			
		||||
    $this->installEntitySchema('node');
 | 
			
		||||
 | 
			
		||||
    NodeType::create(['type' => 'article', 'name' => 'Article'])->save();
 | 
			
		||||
    $this->fieldValues = [
 | 
			
		||||
      $this->randomMachineName(),
 | 
			
		||||
      $this->randomMachineName(),
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    $this->fieldNames = ['field_test_list_string', 'field_test_list_integer'];
 | 
			
		||||
 | 
			
		||||
    // Create two field entities.
 | 
			
		||||
    FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => $this->fieldNames[0],
 | 
			
		||||
      'entity_type' => 'node',
 | 
			
		||||
      'type' => 'list_string',
 | 
			
		||||
      'cardinality' => 1,
 | 
			
		||||
      'settings' => [
 | 
			
		||||
        'allowed_values' => [
 | 
			
		||||
          $this->fieldValues[0] => $this->fieldValues[0],
 | 
			
		||||
          $this->fieldValues[1] => $this->fieldValues[1],
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ])->save();
 | 
			
		||||
    FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => $this->fieldNames[1],
 | 
			
		||||
      'entity_type' => 'node',
 | 
			
		||||
      'type' => 'list_integer',
 | 
			
		||||
      'cardinality' => 1,
 | 
			
		||||
      'settings' => [
 | 
			
		||||
        'allowed_values' => [
 | 
			
		||||
          $this->fieldValues[0],
 | 
			
		||||
          $this->fieldValues[1],
 | 
			
		||||
        ],
 | 
			
		||||
      ],
 | 
			
		||||
    ])->save();
 | 
			
		||||
    foreach ($this->fieldNames as $field_name) {
 | 
			
		||||
      FieldConfig::create([
 | 
			
		||||
        'field_name' => $field_name,
 | 
			
		||||
        'entity_type' => 'node',
 | 
			
		||||
        'label' => 'Test options list field',
 | 
			
		||||
        'bundle' => 'article',
 | 
			
		||||
      ])->save();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,78 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\options\Kernel\Views;
 | 
			
		||||
 | 
			
		||||
use Drupal\field\Entity\FieldConfig;
 | 
			
		||||
use Drupal\field\Entity\FieldStorageConfig;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test to ensure views data is properly created for the Options module.
 | 
			
		||||
 *
 | 
			
		||||
 * @group views
 | 
			
		||||
 */
 | 
			
		||||
class ViewsDataTest extends OptionsTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected static $modules = [
 | 
			
		||||
    'options',
 | 
			
		||||
    'options_test',
 | 
			
		||||
    'entity_test',
 | 
			
		||||
    'views',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The field storage.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Field\FieldStorageDefinitionInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $fieldStorage;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @var int
 | 
			
		||||
   */
 | 
			
		||||
  protected int $field;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp($import_test_views = TRUE): void {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $this->installEntitySchema('entity_test');
 | 
			
		||||
    $field_name = 'test_options';
 | 
			
		||||
    $this->fieldStorage = FieldStorageConfig::create([
 | 
			
		||||
      'field_name' => $field_name,
 | 
			
		||||
      'entity_type' => 'entity_test',
 | 
			
		||||
      'type' => 'list_string',
 | 
			
		||||
      'cardinality' => 1,
 | 
			
		||||
      'settings' => [
 | 
			
		||||
        'allowed_values_function' => '\Drupal\options_test\OptionsAllowedValues::dynamicValues',
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
    $this->fieldStorage->save();
 | 
			
		||||
 | 
			
		||||
    $this->field = FieldConfig::create([
 | 
			
		||||
      'field_name' => $field_name,
 | 
			
		||||
      'entity_type' => 'entity_test',
 | 
			
		||||
      'bundle' => 'entity_test',
 | 
			
		||||
      'required' => TRUE,
 | 
			
		||||
    ])->save();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests the option module's implementation of hook_field_views_data().
 | 
			
		||||
   */
 | 
			
		||||
  public function testOptionsFieldViewsData(): void {
 | 
			
		||||
    $field_data = \Drupal::service('views.views_data')->get('entity_test__test_options');
 | 
			
		||||
 | 
			
		||||
    // Check that the options module has properly overridden default views data.
 | 
			
		||||
    $test_options_field = $field_data['test_options_value'];
 | 
			
		||||
    $this->assertEquals('string_list_field', $test_options_field['argument']['id'], 'Argument handler is properly set for fields with allowed value callbacks.');
 | 
			
		||||
    $this->assertEquals('list_field', $test_options_field['filter']['id'], 'Filter handler is properly set for fields with allowed value callbacks.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user