select2-8.x-1.11/src/Element/Select2.php
src/Element/Select2.php
<?php
namespace Drupal\select2\Element;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Entity\Element\EntityAutocomplete;
use Drupal\Core\Form\FormState;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\OptGroup;
use Drupal\Core\Render\Element\Select;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\select2\Select2Trait;
/**
* Provides an select2 form element.
*
* Properties:
* - #cardinality: (optional) How many options can be selected. Default is
* unlimited.
* - #autocomplete_options_callback: (optional) A callback to return all valid
* currently selected options. @see static::getValidSelectedOptions().
* - #autocomplete_route_callback: (optional) A callback that sets the
* #autocomplete_route_name and autocomplete_route_parameters keys on the
* render element. @see static::setAutocompleteRouteParameters().
*
* Simple usage example:
* @code
* $form['example_select'] = [
* '#type' => 'select2',
* '#title' => $this->t('Select element'),
* '#options' => [
* '1' => $this->t('One'),
* '2' => [
* '2.1' => $this->t('Two point one'),
* '2.2' => $this->t('Two point two'),
* ],
* '3' => $this->t('Three'),
* ],
* ];
* @endcode
*
* If you want to prevent the rendering of all options and fetch the options via
* ajax instead, you can use the '#autocomplete' property. It's also needed to
* specify which entities are available with '#target_type',
* '#selection_handler' and '#selection_settings'.
* @code
* $form['my_element'] = [
* '#type' => 'select2',
* '#title' => $this->t('Select element'),
* '#options' => [
* '1' => $this->t('One'),
* '2' => $this->t('Two'),
* '3' => $this->t('Three'),
* ],
* '#autocomplete' => TRUE,
* '#target_type' => 'node',
* // The selection handler is optional and pre-populated to 'default'.
* '#selection_handler' => 'default',
* '#selection_settings' => [
* 'target_bundles' => ['article', 'page'],
* ],
* ];
* @endcode
*
* If you want to allow an input of an entity label that does not exist yet but
* can be created "on the fly" on form submission, the '#autocreate' property
* can be used:
* @code
* // #autocreate should be an array where the 'bundle' key is required and
* // should be the bundle name for the new entity.
* // The 'uid' key of the #autocreate array is optional and defaults to the
* // current logged-in user. It should be the user ID for the new entity,
* // if the target entity type implements \Drupal\user\EntityOwnerInterface.
* $form['my_element'] = [
* '#type' => 'select2',
* '#target_type' => 'taxonomy_term',
* '#autocreate' => [
* 'bundle' => 'tags',
* 'uid' => <a valid user ID>,
* ],
* ];
* @endcode
*
* The render element sets a bunch of default values to configure the select2
* element. Nevertheless all select2 config values can be overwritten with the
* '#select2' property.
* @code
* $form['my_element'] = [
* '#type' => 'select2',
* '#select2' => [
* 'allowClear' => TRUE,
* ],
* ];
* @endcode
*
* @see https://select2.org/configuration/options-api
*
* @FormElement("select2")
*/
class Select2 extends Select {
use Select2Trait;
/**
* {@inheritdoc}
*/
public function getInfo(): array {
$info = parent::getInfo();
$class = get_class($this);
// Apply default form element properties.
$info['#target_type'] = NULL;
$info['#selection_handler'] = 'default';
$info['#selection_settings'] = [];
$info['#autocomplete'] = FALSE;
$info['#autocreate'] = [];
$info['#empty_value'] = '';
$info['#cardinality'] = 0;
$info['#pre_render'][] = [$class, 'preRenderAutocomplete'];
$info['#pre_render'][] = [$class, 'preRenderOverwrites'];
$info['#element_validate'][] = [$class, 'validateEntityAutocomplete'];
$info['#select2'] = [];
return $info;
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
// Potentially the #value is set directly, so it contains the 'target_id'
// array structure instead of a string.
if ($input !== FALSE && is_array($input)) {
$input = array_map(fn($item) => $item['target_id'] ?? $item, $input);
return array_combine($input, $input);
}
return parent::valueCallback($element, $input, $form_state);
}
/**
* Form element validation handler for entity_autocomplete elements.
*
* @param array $element
* The render element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
* @param array $complete_form
* The form array.
*/
public static function validateEntityAutocomplete(array &$element, FormStateInterface $form_state, array &$complete_form): void {
if ($element['#target_type'] && !$element['#autocreate']) {
$value_callable = $element['#autocomplete_options_callback'] ?? NULL;
if (!$value_callable || !is_callable($value_callable)) {
$value_callable = '\Drupal\select2\Element\Select2::getValidSelectedOptions';
}
$value = [];
$input_values = call_user_func_array($value_callable, [
$element,
$form_state,
]);
foreach ($input_values as $id => $input) {
$value[] = [
'target_id' => $id,
];
}
$form_state->setValueForElement($element, $value);
}
}
/**
* {@inheritdoc}
*/
public static function processSelect(&$element, FormStateInterface $form_state, &$complete_form): array {
// Fill the options, because in autocomplete we cleared them and for the
// validation the at least selected options are needed.
if ($element['#autocomplete']) {
$value_callable = $element['#autocomplete_options_callback'] ?? NULL;
if (!$value_callable || !is_callable($value_callable)) {
$value_callable = '\Drupal\select2\Element\Select2::getValidSelectedOptions';
}
$element['#options'] = call_user_func_array($value_callable, [
$element,
$form_state,
]);
}
// We need to disable form validation, because with autocreation the options
// could contain non existing references. We still have validation in the
// entity reference field.
if ($element['#autocreate'] && $element['#target_type']) {
unset($element['#needs_validation']);
// Add back auto_create values.
$values = is_array($element['#value']) ? $element['#value'] : [$element['#value']];
foreach ($values as $key => $value) {
if (is_string($key) && substr($key, 0, 4) === "\$ID:") {
// Set option and remove ID from label.
$element['#options'][$key] = substr($value, 0, 4) === "\$ID:" ? substr($value, 4) : $value;
}
elseif (!$element['#multiple'] && substr($value, 0, 4) === "\$ID:") {
$element['#options'][$value] = substr($value, 4);
}
}
}
if (!$element['#multiple'] && !isset($element['#options'][$element['#empty_value']])) {
$empty_option = [$element['#empty_value'] => ''];
$element['#options'] = $empty_option + $element['#options'];
}
if ($element['#multiple']) {
$values = (array) ($element['#value'] ?? $element['#default_value']);
// Apply field values order on the options.
if (!empty($values)) {
$flipped_keys = array_intersect_key(array_flip($values), $element['#options']);
$element['#options'] = array_replace($flipped_keys, $element['#options']);
}
}
// Set the type from select2 to select to get proper form validation.
$element['#type'] = 'select';
return $element;
}
/**
* Get an array of currently selected options.
*
* @param array $element
* The render element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
*
* @return array
* Key => entity ID, Value => entity label.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected static function getValidSelectedOptions(array $element, FormStateInterface $form_state): array {
$handler_settings = $element['#selection_settings'] + [
'target_type' => $element['#target_type'],
'handler' => $element['#selection_handler'],
];
$value = is_array($element['#value']) ? $element['#value'] : [$element['#value']];
return $value ? static::getValidReferenceableEntities($value, $handler_settings) : [];
}
/**
* {@inheritdoc}
*
* @phpstan-ignore-next-line
*/
public static function preRenderSelect($element): array {
$element = parent::preRenderSelect($element);
$required = isset($element['#states']['required']) ? TRUE : $element['#required'];
$multiple = $element['#multiple'];
if ($multiple) {
$element['#attributes']['multiple'] = 'multiple';
$element['#attributes']['name'] = $element['#name'] . '[]';
}
// Support for https://www.drupal.org/project/field_config_cardinality.
$cardinality = $element['#attributes']['data-fcc'] ?? $element['#cardinality'];
$current_language = \Drupal::languageManager()->getCurrentLanguage();
$current_theme = \Drupal::theme()->getActiveTheme()->getName();
$select2_theme_exists = \Drupal::service('library.discovery')->getLibraryByName($current_theme, 'select2.theme');
// Placeholder should be taken from #placeholder property if it set.
// Otherwise we can take it from '#empty_option' property.
$placeholder_text = $required ? new TranslatableMarkup('- Select -') : new TranslatableMarkup('- None -');
$placeholder = ['id' => '', 'text' => $placeholder_text];
if (!empty($element['#empty_value'])) {
$placeholder['id'] = $element['#empty_value'];
}
if (!empty($element['#placeholder'])) {
$placeholder['text'] = $element['#placeholder'];
}
elseif (!empty($element['#empty_option'])) {
$placeholder['text'] = $element['#empty_option'];
}
// Defining the select2 configuration.
$settings = [
'multiple' => $multiple,
'placeholder' => $placeholder,
// @todo Enable allowClear for multiple fields. https://github.com/select2/select2/issues/3335.
'allowClear' => !$multiple && !$required,
'dir' => $current_language->getDirection(),
'language' => $current_language->getId(),
'tags' => (bool) $element['#autocreate'],
'theme' => $select2_theme_exists ? $current_theme : 'default',
'maximumSelectionLength' => $multiple ? $cardinality : 0,
'tokenSeparators' => $element['#autocreate'] ? [','] : [],
'selectOnClose' => $element['#autocomplete'],
'width' => '100%',
];
$element['#attributes']['class'][] = 'select2-widget';
$element['#attributes']['data-select2-config'] = $settings;
// Adding the select2 library.
$element['#attached']['library'][] = 'select2/select2';
$element['#attached']['library'][] = 'select2/select2.i18n.' . $current_language->getId();
if ($select2_theme_exists) {
$element['#attached']['library'][] = $current_theme . '/select2.theme';
}
return $element;
}
/**
* Attach autocomplete behavior to the render element.
*/
public static function preRenderAutocomplete(array $element): array {
if (!$element['#autocomplete']) {
return $element;
}
$value_callable = $element['#autocomplete_route_callback'] ?? NULL;
if (!$value_callable || !is_callable($value_callable)) {
$value_callable = '\Drupal\select2\Element\Select2::setAutocompleteRouteParameters';
}
$element = call_user_func_array($value_callable, [&$element]);
// Reduce options to the preselected ones and bring them in the correct
// order.
$options = OptGroup::flattenOptions($element['#options']);
$values = $element['#value'] ?? $element['#default_value'];
$values = is_array($values) ? $values : [$values];
$element['#options'] = [];
foreach ($values as $value) {
if (isset($options[$value])) {
$element['#options'][$value] = $options[$value];
}
}
/** @var \Drupal\Core\Access\AccessManagerInterface $access_manager */
$access_manager = \Drupal::service('access_manager');
$access = $access_manager->checkNamedRoute($element['#autocomplete_route_name'], $element['#autocomplete_route_parameters'], \Drupal::currentUser(), TRUE);
if ($access && $access->isAllowed()) {
$url = Url::fromRoute($element['#autocomplete_route_name'], $element['#autocomplete_route_parameters'])
->toString(TRUE);
// Provide a data attribute for the JavaScript behavior to bind to.
$element['#attributes']['data-select2-config'] += [
'minimumInputLength' => 1,
'ajax' => [
'delay' => 250,
'url' => $url->getGeneratedUrl(),
],
];
}
return $element;
}
/**
* Sets the autocomplete route parameters.
*
* @param array $element
* The render element.
*
* @return array
* The render element with autocomplete route parameters.
*/
protected static function setAutocompleteRouteParameters(array &$element): array {
$complete_form = [];
$element = EntityAutocomplete::processEntityAutocomplete($element, new FormState(), $complete_form);
$element['#autocomplete_route_name'] = 'select2.entity_autocomplete';
return $element;
}
/**
* Allows to modify the select2 settings.
*/
public static function preRenderOverwrites(array $element): array {
if (!$element['#multiple']) {
$empty_option = [$element['#empty_value'] => ''];
$element['#options'] = $empty_option + $element['#options'];
}
// Allow to overwrite the default settings and set additional settings.
foreach ($element["#select2"] as $key => $value) {
$element['#attributes']['data-select2-config'][$key] = $value;
}
$element['#attributes']['data-select2-config'] = Json::encode($element['#attributes']['data-select2-config']);
return $element;
}
}
