-
mschudders authored
Webform element finalize (didnt test the autocomplete but should work ++ bug fix for clear field description.
mschudders authoredWebform element finalize (didnt test the autocomplete but should work ++ bug fix for clear field description.
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
Datalist.php 6.25 KiB
<?php
namespace Drupal\datalist\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\RenderElement;
use Drupal\Core\Render\Element\Textfield;
/**
* Provides a render element for a datalist input.
*
* It will render a textfield connected to a datalist.
* Currently multivalue and grouping is not supported.
* By default we hide the keys when rendering.
*
* Be sure to fill in autocomplete for the fallback to work.
*
* Usage example:
*
* @code
* $form['author']['name'] = array(
* '#type' => 'datalist',
* '#options' => [],
* '#title' => '',
* '#required' => TRUE|FALSE,
* '#placeholder' => 'a placeholder for the input field',
* '#default_value' => ''
* '#clear_button' => FALSE|"string for the clear button label" (defaults to
* 'X')
* '#clear_button_description' => FALSE|"string for describing the clear
* button" (defaults to 'Clear field')
* '#use_keys' => TRUE|FALSE (default=FALSE)
* '#autocomplete_route_name' => NULL, OPTIONAL
* '#autocomplete_route_parameters' => [], OPTIONAL
* );
* @endcode
*
* @FormElement("datalist")
*/
class Datalist extends Textfield {
/**
* Switch back to a basic textfield.
*
* @param array $element
* The element.
*
* @return array
* The element modified to a simple textfield.
*/
public static function fallbackTextfield($element): array {
// Reset to base information.
$route = $element['#autocomplete_route_name'];
$element = array_merge($element, $element['#parent_textfield']);
$element['#autocomplete_route_name'] = $route;
$element['#type'] = 'textfield';
return $element;
}
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = static::class;
return [
'#input' => TRUE,
'#process' => [
[$class, 'processDatalist'],
[$class, 'processAutocomplete'],
[$class, 'processAjaxForm'],
[$class, 'processPattern'],
[$class, 'processGroup'],
],
'#pre_render' => [
[$class, 'preRenderDatalist'],
[$class, 'preRenderGroup'],
],
'#theme' => 'input__datalist',
'#options' => [],
'#clear_button' => self::getClearButton(),
'#down_button' => self::getDownButton(),
'#clear_button_description' => self::getClearButtonDescription(),
'#use_keys' => FALSE,
'#autocomplete_route_name' => NULL,
'#autocomplete_route_parameters' => [],
'#parent_textfield' => parent::getInfo(),
] + parent::getInfo();
}
public static function getClearButtonDescription() {
return t('Clear field');
}
public static function getClearButton() {
return '✖';
}
public static function getDownButton() {
return '▼';
}
/**
* {@inheritdoc}
*/
public static function processDatalist(&$element, FormStateInterface $form_state, &$complete_form) {
$element['#cache']['contexts'][] = 'headers';
self::fallbackTextfield($element);
$element['#list'] = "{$element['#id']}-datalist";
if (!empty($element['#clear_button'])) {
$element['#attached']['library'][] = 'datalist/datalist';
}
// Let validation on empty value also work by adding the empty value.
// Since we add #options, core will perform a required validation on our
// submitted value. Since datalist is basically an enhanced textfield, when
// the form is submitted without interacting with the field, the validation
// method will receive an empty string. In order for this string to be
// considered legal, we always add it to our options list. When a field is
// required, it will now get a required error and not an illegal choice.
// In preprocess we unset this empty choice if the field is required.
$element['#options'] = ['' => $element['#placeholder'] ?? ''] + $element['#options'];
return $element;
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input !== FALSE && $input !== NULL) {
// We might just have submitted without selecting or typing anything.
// In that case an empty string is used as input.
if ($input === '' || $element['#use_keys']) {
return $input;
}
else {
// If the input is coming from the textinput, and we don't #use_keys as
// values, we are getting the label as a value, so lookup the key for
// this label.
$lookup = array_search($input, $element['#options'], TRUE);
if ($lookup !== FALSE) {
return $lookup;
}
// Setting the input in code will result in the input being the key
// already, so in case we don't find it just return what was given.
if (array_key_exists($input, $element['#options'])) {
return $input;
}
}
}
return NULL;
}
/**
* Prepares a datalist render element.
*/
public static function preRenderDatalist($element) {
// Make sure we keep the datalist type class, even if we extend this class.
$element['#wrapper_attributes']['class'][] = 'form-type--datalist';
if (!($element['#required'] ?? FALSE)) {
// Don't show the empty value, it's shown by the placeholder.
unset($element['#options']['']);
}
if (!$element['#use_keys'] && $element['#value'] !== '') {
// Transform the actual field value which is a key into a label
// for displaying.
$element['#value'] = $element['#options'][$element['#value']] ?? '';
}
$element['#attributes']['class'][] = 'form-element';
$element['#attributes']['type'] = 'text';
$element['#attributes']['autocomplete'] = 'off';
Element::setAttributes($element, [
'id',
'name',
'value',
'size',
'maxlength',
'placeholder',
'list',
'autocomplete',
]);
RenderElement::setAttributes($element, ['form-text', 'form-datalist']);
return $element;
}
/**
* {@inheritdoc}
*/
public static function preRenderAjaxForm($element) {
if (
!empty($element['#ajax'])
&& !isset($element['#ajax_processed'])
&& !isset($element['#ajax']['event'])
) {
$element['#ajax']['event'] = 'change';
}
return parent::preRenderAjaxForm($element);
}
}