diff --git a/README.txt b/README.txt
index 6f003b7..fdca5cd 100644
--- a/README.txt
+++ b/README.txt
@@ -40,4 +40,4 @@ $form['example'] = array(
'#options' => array('' => 'Pick something or enter your own', 1 => 'ABC', 2 => 'DEF', 3 => 'GHI'),
);
-To see all the plugins and options available to you for overriding, see the plugin homepage at http://brianreavis.github.io/selectize.js/
\ No newline at end of file
+To see all the plugins and options available to you for overriding, see the plugin homepage at http://brianreavis.github.io/selectize.js/
diff --git a/composer.json b/composer.json
index fb7307b..db01744 100644
--- a/composer.json
+++ b/composer.json
@@ -6,6 +6,20 @@
"license": "GPL-2.0+",
"homepage": "http://drupal.org/project/selectize",
"minimum-stability": "dev",
+ "repositories": [
+ {
+ "type": "package",
+ "package": {
+ "name": "selectize/selectize",
+ "version": "0.12.4",
+ "type": "drupal-library",
+ "dist": {
+ "url": "https://github.com/selectize/selectize.js/archive/v0.12.4.zip",
+ "type": "zip"
+ }
+ }
+ }
+ ],
"authors": [
{
"name": "Kevin Quillen (kevinquillen)",
@@ -16,5 +30,8 @@
"support": {
"issues": "http://drupal.org/project/issues/selectize",
"source": "http://cgit.drupalcode.org/selectize"
+ },
+ "require": {
+ "selectize/selectize": "^0.12.4"
}
-}
\ No newline at end of file
+}
diff --git a/css/selectize.css b/css/selectize.css
index 2543dd2..dbcac21 100644
--- a/css/selectize.css
+++ b/css/selectize.css
@@ -1,3 +1,3 @@
.vertical-tabs {
overflow: visible !important;
-}
\ No newline at end of file
+}
diff --git a/js/selectize.drupal.js b/js/selectize.drupal.js
index 118151e..a120301 100644
--- a/js/selectize.drupal.js
+++ b/js/selectize.drupal.js
@@ -17,9 +17,65 @@
attach: function (context) {
if (typeof drupalSettings.selectize != 'undefined') {
$.each(drupalSettings.selectize, function (index, value) {
- $('#' + index).selectize(JSON.parse(value));
+ if (typeof value !== 'object') {
+ value = JSON.parse(value);
+ }
+ var config = value;
+ if (typeof config.field_type !== 'undefined' && config.field_type === 'entity_reference') {
+ config = Drupal.behaviors.selectize.appendAutocompleteConfig(index, config);
+ }
+ var $el = $('#' + index);
+ if (config.hideTermId && typeof $el.val() === 'string') {
+ $el.val($el.val().split(config.delimiter).map(function (value) {
+ return value.replace(/\(\d+\)/, '').trim();
+ }).join(config.delimiter));
+ }
+ $el.selectize(config);
});
}
+ },
+ appendAutocompleteConfig: function (index, config) {
+ var url = $('#' + index).data('selectize-autocomplete-path');
+ config.persist = true;
+ config.searchField = 'label';
+ if (config.hideTermId) {
+ config.valueField = config.searchField;
+ config.labelField = config.searchField;
+ }
+ config.render = {
+ option: function (item, escape) {
+ // Removes the term id and parenthesis of the shown text.
+ if (typeof item.label === 'undefined' && typeof item.text !== 'undefined') {
+ item.label = item.text.replace(/\(\d\)/, '');
+ }
+ return '
' + item.label + '
';
+ },
+ item: function (item, escape) {
+ if (typeof item.label !== 'undefined' && typeof item.text === 'undefined') {
+ item.text = item.label;
+ }
+ else {
+ item.text = item.text.replace(/\(\d\)/, '');
+ }
+ return '' + item.text + '
';
+ }
+ };
+ config.load = function (query, callback) {
+ if (!query.length) return callback();
+ $.ajax({
+ url: url,
+ data: {
+ q: query,
+ }
+ })
+ .fail(function () {
+ callback();
+ })
+ .done(function (response) {
+ return callback(response);
+ });
+ };
+ return config;
}
};
diff --git a/selectize.install b/selectize.install
index 0e999ae..278e30d 100644
--- a/selectize.install
+++ b/selectize.install
@@ -18,7 +18,8 @@ function selectize_requirements($phase) {
if ($file_exists) {
$message = t('Selectize.js plugin detected in %path.', ['%path' => '/libraries/selectize']);
- } else {
+ }
+ else {
$message = t('The Selectize.js plugin was not found. Please download it into the libraries folder in the root (/libraries/selectize).', [':repository_url' => 'https://github.com/selectize/selectize.js']);
}
@@ -32,4 +33,4 @@ function selectize_requirements($phase) {
}
return $requirements;
-}
\ No newline at end of file
+}
diff --git a/selectize.libraries.yml b/selectize.libraries.yml
index 71c9788..1493608 100644
--- a/selectize.libraries.yml
+++ b/selectize.libraries.yml
@@ -9,4 +9,4 @@ core:
drupal:
version: VERSION
js:
- js/selectize.drupal.js: {}
\ No newline at end of file
+ js/selectize.drupal.js: {}
diff --git a/selectize.module b/selectize.module
index 603a115..a35cba0 100644
--- a/selectize.module
+++ b/selectize.module
@@ -2,4 +2,4 @@
/**
* @file Module file left intentionally blank.
- */
\ No newline at end of file
+ */
diff --git a/src/Element/Selectize.php b/src/Element/Selectize.php
index d48000f..27bd450 100644
--- a/src/Element/Selectize.php
+++ b/src/Element/Selectize.php
@@ -2,55 +2,32 @@
namespace Drupal\selectize\Element;
-use \Drupal\Core\Render\Element\FormElement;
-use \Drupal\Core\Render\Element;
-use \Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element\Select;
+use Drupal\selectize\SelectizeElementTrait;
/**
* Provides a selectized form element.
*
* @FormElement("selectize")
*/
-class Selectize extends FormElement {
+class Selectize extends Select {
+
+ use SelectizeElementTrait;
+
/**
* {@inheritdoc}
*/
public function getInfo() {
+ $info = parent::getInfo();
$class = get_class($this);
- return array(
- '#input' => TRUE,
- '#multiple' => FALSE,
- '#autocomplete_route_name' => FALSE,
- '#process' => array(
- array($class, 'processSelectize'),
- array($class, 'processAjaxForm'),
- ),
- '#pre_render' => array(
- array($class, 'preRenderSelectize'),
- ),
- '#theme' => 'select',
- '#theme_wrappers' => array('form_element'),
- '#options' => array(),
- '#settings' => self::settings(),
- );
- }
+ // Apply default form element properties.
+ $info['#settings'] = self::settings();
+ array_unshift($info['#process'], [$class, 'processSelectize']);
+ array_unshift($info['#pre_render'], [$class, 'preRenderSelectize']);
- /**
- * Prepares a #type 'selectize' render element for input.html.twig.
- *
- * @param array $element
- * An associative array containing the properties of the element.
- * Properties used: #title, #value, #description, #size, #maxlength,
- * #placeholder, #required, #attributes.
- *
- * @return array
- * The $element with prepared variables ready for input.html.twig.
- */
- public static function preRenderSelectize($element) {
- Element::setAttributes($element, array('id', 'name', 'size'));
- static::setAttributes($element, array('form-select'));
- return $element;
+ return $info;
}
/**
@@ -58,69 +35,29 @@ class Selectize extends FormElement {
*/
public static function processSelectize(&$element, FormStateInterface $form_state, &$complete_form) {
if (isset($element['#settings'])) {
+ if (isset($element['#settings']['sortField']) && isset($element['#settings']['sortDirection'])) {
+ $element['#settings']['sortField'] = [
+ [
+ 'field' => $element['#settings']['sortField'],
+ 'direction' => $element['#settings']['sortDirection'],
+ ],
+ ];
+ }
$element['#attached']['drupalSettings']['selectize'][$element['#id']] = json_encode($element['#settings']);
- // if drag_drop plugin is requested, we need to load the sortable plugin.
+ // If drag_drop plugin is requested, we need to load the sortable plugin.
if (isset($element['#settings']['plugins']) && in_array('drag_drop', $element['#settings']['plugins'])) {
$complete_form['#attached']['library'][] = 'core/jquery.ui.sortable';
}
-
- // inject the selectize library and CSS assets.
- $complete_form['#attached']['library'][] = 'selectize/core';
- $complete_form['#attached']['library'][] = 'selectize/drupal';
}
- // #multiple select fields need a special #name.
- if ($element['#multiple']) {
- $element['#attributes']['multiple'] = 'multiple';
- $element['#attributes']['name'] = $element['#name'] . '[]';
- }
- // A non-#multiple select needs special handling to prevent user agents from
- // preselecting the first option without intention. #multiple select lists do
- // not get an empty option, as it would not make sense, user interface-wise.
- else {
- // If the element is set to #required through #states, override the
- // element's #required setting.
- $required = isset($element['#states']['required']) ? TRUE : $element['#required'];
- // If the element is required and there is no #default_value, then add an
- // empty option that will fail validation, so that the user is required to
- // make a choice. Also, if there's a value for #empty_value or
- // #empty_option, then add an option that represents emptiness.
- if (($required && !isset($element['#default_value'])) || isset($element['#empty_value']) || isset($element['#empty_option'])) {
- $element += array(
- '#empty_value' => '',
- '#empty_option' => $required ? t('- Select -') : t('- None -'),
- );
- // The empty option is prepended to #options and purposively not merged
- // to prevent another option in #options mistakenly using the same value
- // as #empty_value.
- $empty_option = array($element['#empty_value'] => $element['#empty_option']);
- $element['#options'] = $empty_option + $element['#options'];
- }
- }
+ // Inject the selectize library and CSS assets.
+ $complete_form['#attached']['library'][] = 'selectize/core';
+ $complete_form['#attached']['library'][] = 'selectize/drupal';
return $element;
}
- /**
- * Return default settings for Selectize. Pass in values to override defaults.
- * @param $values
- * @return array
- */
- public static function settings(array $values = array()) {
- $settings = array(
- 'create' => FALSE,
- 'sortField' => 'text',
- 'plugins' => NULL,
- 'highlight' => TRUE,
- 'maxItems' => 10,
- 'delimiter' => NULL,
- 'persist' => FALSE,
- );
-
- return array_merge($settings, $values);
- }
-
/**
* {@inheritdoc}
*/
diff --git a/src/Element/SelectizeEntityAutocomplete.php b/src/Element/SelectizeEntityAutocomplete.php
new file mode 100644
index 0000000..e090f72
--- /dev/null
+++ b/src/Element/SelectizeEntityAutocomplete.php
@@ -0,0 +1,257 @@
+ ';',
+ 'delimiter_warning' => FALSE,
+ ]);
+ return $info;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
+ $delimiter = NestedArray::getValue($element, ['#settings', 'delimiter']);
+ if (empty($delimiter)) {
+ throw new \InvalidArgumentException('The #settings[delimiter] property is not specified.');
+ }
+ // Process the #default_value property.
+ if ($input === FALSE && isset($element['#default_value']) && $element['#process_default_value']) {
+ if (is_array($element['#default_value']) && $element['#tags'] !== TRUE) {
+ throw new \InvalidArgumentException('The #default_value property is an array but the form element does not allow multiple values.');
+ }
+ elseif (!empty($element['#default_value']) && !is_array($element['#default_value'])) {
+ // Convert the default value into an array for easier processing in
+ // static::getEntityLabels().
+ $element['#default_value'] = [$element['#default_value']];
+ }
+ if ($element['#default_value']) {
+ if (!(reset($element['#default_value']) instanceof EntityInterface)) {
+ throw new \InvalidArgumentException('The #default_value property has to be an entity object or an array of entity objects.');
+ }
+
+ // Extract the labels from the passed-in entity objects, taking access
+ // checks into account.
+ return static::getEntityLabels($element['#default_value'], $delimiter);
+ }
+ }
+
+ // Potentially the #value is set directly, so it contains the 'target_id'
+ // array structure instead of a string.
+ if ($input !== FALSE && is_array($input)) {
+ $entity_ids = array_map(function (array $item) {
+ return $item['target_id'];
+ }, $input);
+
+ $entities = \Drupal::entityTypeManager()->getStorage($element['#target_type'])->loadMultiple($entity_ids);
+
+ return static::getEntityLabels($entities, $delimiter);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function processAutocomplete(&$element, FormStateInterface $form_state, &$complete_form) {
+ $url = NULL;
+ $access = FALSE;
+ if (!empty($element['#autocomplete_route_name'])) {
+ $parameters = isset($element['#autocomplete_route_parameters']) ? $element['#autocomplete_route_parameters'] : [];
+ $url = Url::fromRoute($element['#autocomplete_route_name'], $parameters)
+ ->toString(TRUE);
+ /** @var \Drupal\Core\Access\AccessManagerInterface $access_manager */
+ $access_manager = \Drupal::service('access_manager');
+ $access = $access_manager->checkNamedRoute($element['#autocomplete_route_name'], $parameters, \Drupal::currentUser(), TRUE);
+ }
+ if ($access) {
+ // Process selectize settings.
+ if (isset($element['#settings'])) {
+ if (isset($element['#settings']['sortField']) && isset($element['#settings']['sortDirection'])) {
+ $element['#settings']['sortField'] = [
+ [
+ 'field' => $element['#settings']['sortField'],
+ 'direction' => $element['#settings']['sortDirection'],
+ ],
+ ];
+ }
+ // If drag_drop plugin is requested, we need to load the sortable plugin.
+ if (isset($element['#settings']['plugins']) && in_array('drag_drop', $element['#settings']['plugins'])) {
+ $libraries = ['core/jquery.ui.sortable'];
+ }
+ // Inject the selectize library and CSS assets.
+ $libraries[] = 'selectize/core';
+ $libraries[] = 'selectize/drupal';
+ }
+ $element['#settings']['field_type'] = 'entity_reference';
+ $element['#attached']['drupalSettings']['selectize'][$element['#id']] = json_encode($element['#settings']);
+ $metadata = BubbleableMetadata::createFromRenderArray($element);
+ if ($access->isAllowed()) {
+ $element['#attributes']['class'][] = 'form-selectize-autocomplete';
+ $element['#attributes']['class'][] = 'selectize-widget';
+ $metadata->addAttachments(['library' => $libraries]);
+ // Provide a data attribute for the JavaScript behavior to bind to.
+ $element['#attributes']['data-selectize-autocomplete-path'] = $url->getGeneratedUrl();
+ $metadata = $metadata->merge($url);
+ }
+ $metadata
+ ->merge(BubbleableMetadata::createFromObject($access))
+ ->applyTo($element);
+ }
+
+ return $element;
+ }
+
+ /**
+ * Form element validation handler for entity_autocomplete elements.
+ */
+ public static function validateEntityAutocomplete(array &$element, FormStateInterface $form_state, array &$complete_form) {
+ $value = NULL;
+
+ if (!empty($element['#value'])) {
+ $options = [
+ 'target_type' => $element['#target_type'],
+ 'handler' => $element['#selection_handler'],
+ 'handler_settings' => $element['#selection_settings'],
+ ];
+
+ /** @var /Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $handler */
+ $handler = \Drupal::service('plugin.manager.entity_reference_selection')->getInstance($options);
+ $autocreate = (bool) $element['#autocreate'] && $handler instanceof SelectionWithAutocreateInterface;
+
+ // GET forms might pass the validated data around on the next request, in
+ // which case it will already be in the expected format.
+ if (is_array($element['#value'])) {
+ $value = $element['#value'];
+ }
+ else {
+ // Override delimeter processing.
+ $input_values = explode($element['#settings']['delimiter'], $element['#value']);
+
+ foreach ($input_values as $input) {
+ $match = static::extractEntityIdFromAutocompleteInput($input);
+ if ($match === NULL) {
+ // Try to get a match from the input string when the user didn't use
+ // the autocomplete but filled in a value manually.
+ $match = static::matchEntityByTitle($handler, $input, $element, $form_state, !$autocreate);
+ }
+
+ if ($match !== NULL) {
+ $value[] = [
+ 'target_id' => $match,
+ ];
+ }
+ elseif ($autocreate) {
+ /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionWithAutocreateInterface $handler */
+ // Auto-create item. See an example of how this is handled in
+ // \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::presave().
+ $value[] = [
+ 'entity' => $handler->createNewEntity($element['#target_type'], $element['#autocreate']['bundle'], $input, $element['#autocreate']['uid']),
+ ];
+ }
+ }
+ }
+
+ // Check that the referenced entities are valid, if needed.
+ if ($element['#validate_reference'] && !empty($value)) {
+ // Validate existing entities.
+ $ids = array_reduce($value, function ($return, $item) {
+ if (isset($item['target_id'])) {
+ $return[] = $item['target_id'];
+ }
+ return $return;
+ });
+
+ if ($ids) {
+ $valid_ids = $handler->validateReferenceableEntities($ids);
+ if ($invalid_ids = array_diff($ids, $valid_ids)) {
+ foreach ($invalid_ids as $invalid_id) {
+ $form_state->setError($element, t('The referenced entity (%type: %id) does not exist.', ['%type' => $element['#target_type'], '%id' => $invalid_id]));
+ }
+ }
+ }
+
+ // Validate newly created entities.
+ $new_entities = array_reduce($value, function ($return, $item) {
+ if (isset($item['entity'])) {
+ $return[] = $item['entity'];
+ }
+ return $return;
+ });
+
+ if ($new_entities) {
+ if ($autocreate) {
+ $valid_new_entities = $handler->validateReferenceableNewEntities($new_entities);
+ $invalid_new_entities = array_diff_key($new_entities, $valid_new_entities);
+ }
+ else {
+ // If the selection handler does not support referencing newly
+ // created entities, all of them should be invalidated.
+ $invalid_new_entities = $new_entities;
+ }
+
+ foreach ($invalid_new_entities as $entity) {
+ /** @var \Drupal\Core\Entity\EntityInterface $entity */
+ $form_state->setError($element, t('This entity (%type: %label) cannot be referenced.', ['%type' => $element['#target_type'], '%label' => $entity->label()]));
+ }
+ }
+ }
+ }
+
+ $form_state->setValueForElement($element, $value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getEntityLabels(array $entities, $delimeter = ';') {
+ /** @var \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository */
+ $entity_repository = \Drupal::service('entity.repository');
+
+ $entity_labels = [];
+ foreach ($entities as $entity) {
+ // Set the entity in the correct language for display.
+ $entity = $entity_repository->getTranslationFromContext($entity);
+
+ // Use the special view label, since some entities allow the label to be
+ // viewed, even if the entity is not allowed to be viewed.
+ $label = ($entity->access('view label')) ? $entity->label() : t('- Restricted access -');
+
+ // Take into account "autocreated" entities.
+ if (!$entity->isNew()) {
+ $label .= ' (' . $entity->id() . ')';
+ }
+ // Override delimeter processing.
+ $entity_labels[] = $label;
+ }
+
+ return implode($delimeter, $entity_labels);
+ }
+
+}
diff --git a/src/Plugin/Field/FieldWidget/SelectizeEntityReferenceWidget.php b/src/Plugin/Field/FieldWidget/SelectizeEntityReferenceWidget.php
new file mode 100644
index 0000000..e43ce92
--- /dev/null
+++ b/src/Plugin/Field/FieldWidget/SelectizeEntityReferenceWidget.php
@@ -0,0 +1,43 @@
+fieldDefinition->getFieldStorageDefinition()->getCardinality();
+ $settings = $this->getSettings();
+ $settings['maxItems'] = ($cardinality >= 1) ? $cardinality : NULL;
+ $element['target_id']['#type'] = 'selectize_entity_autocomplete';
+ if ($this->getSelectionHandlerSetting('auto_create') && ($bundle = $this->getAutocreateBundle())) {
+ $settings['create'] = TRUE;
+ }
+ $element['target_id']['#settings'] = $settings;
+
+ return $element;
+ }
+
+}
diff --git a/src/Plugin/Field/FieldWidget/SelectizeWidget.php b/src/Plugin/Field/FieldWidget/SelectizeWidget.php
index 8a14ece..6d70b9b 100644
--- a/src/Plugin/Field/FieldWidget/SelectizeWidget.php
+++ b/src/Plugin/Field/FieldWidget/SelectizeWidget.php
@@ -2,11 +2,14 @@
namespace Drupal\selectize\Plugin\Field\FieldWidget;
+use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Field\FieldItemListInterface;
-use Drupal\Core\Field\Plugin\Field\FieldWidget\OptionsWidgetBase;
+use Drupal\Core\Field\Plugin\Field\FieldWidget\OptionsSelectWidget;
use Drupal\Core\Form\FormStateInterface;
use Drupal\selectize\Element\Selectize;
use Drupal\Component\Utility\Html;
+use Drupal\Core\Form\OptGroup;
+use Drupal\selectize\SelectizeWidgetTrait;
/**
* Plugin implementation of the 'selectize_widget' widget.
@@ -21,77 +24,17 @@ use Drupal\Component\Utility\Html;
* plural = @Translation("selects"),
* ),
* field_types = {
+ * "entity_reference",
+ * "list_integer",
+ * "list_float",
* "list_string"
* },
* multiple_values = TRUE
* )
*/
-class SelectizeWidget extends OptionsWidgetBase {
+class SelectizeWidget extends OptionsSelectWidget {
- /**
- * {@inheritdoc}
- */
- public static function defaultSettings() {
- return [
- 'create' => FALSE,
- 'sortField' => 'text',
- 'allowEmptyOption' => TRUE,
- 'plugins' => ['remove_button'],
- 'highlight' => TRUE,
- 'persist' => FALSE,
- 'diacritics' => FALSE,
- 'closeAfterSelect' => FALSE,
- ] + parent::defaultSettings();
- }
-
- /**
- * {@inheritdoc}
- */
- public function settingsForm(array $form, FormStateInterface $form_state) {
- $element['plugins'] = array(
- '#type' => 'checkboxes',
- '#title' => $this->t('Enabled Plugins'),
- '#description' => $this->t('For a demonstration of what these plugins do, visit the demo site.', ['@url' => 'https://selectize.github.io/selectize.js/']),
- '#default_value' => $this->getSetting('plugins'),
- '#options' => [
- 'remove_button' => $this->t('Remove Button'),
- 'drag_drop' => $this->t('Drag & Drop'),
- 'restore_on_backspace' => $this->t('Restore on Backspace'),
- 'optgroup_columns' => $this->t('Optgroup Columns'),
- 'dropdown_header' => $this->t('Dropdown Headers')
- ]
- );
-
- $element['closeAfterSelect'] = array(
- '#type' => 'checkbox',
- '#title' => $this->t('Close after selecting an option'),
- '#description' => $this->t('This will close the Selectized display after choosing a value.'),
- '#default_value' => $this->getSetting('closeAfterSelect'),
- );
-
- $element['diacritics'] = array(
- '#type' => 'checkbox',
- '#title' => $this->t('Diacritics Support'),
- '#description' => $this->t('Enable or disable international character support'),
- '#default_value' => $this->getSetting('diacritics'),
- );
-
- return $element;
- }
-
- /**
- * {@inheritdoc}
- */
- public function settingsSummary() {
- $summary = [];
- $plugins = $this->getSetting('plugins');
-
- if (!empty($plugins)) {
- $summary[] = $this->t('Enabled plugins: @plugins', array('@plugins' => implode(', ', $plugins)));
- }
-
- return $summary;
- }
+ use SelectizeWidgetTrait;
/**
* {@inheritdoc}
@@ -101,23 +44,67 @@ class SelectizeWidget extends OptionsWidgetBase {
$cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
$settings = $this->getSettings();
- $settings['plugins'] = array_keys(array_filter($settings['plugins']));
$settings['maxItems'] = ($cardinality >= 1) ? $cardinality : 999;
- $settings['items'] = $this->getSelectedOptions($items, $delta);
+ $settings['items'] = $this->getSelectedOptions($items);
+ $settings['field_type'] = 'list_string';
- $element += array(
+ $element = array_merge($element, [
'#type' => 'selectize',
'#settings' => $settings,
'#options' => $this->getOptions($items->getEntity()),
- '#default_value' => $this->getSelectedOptions($items, $delta),
+ '#default_value' => $this->getSelectedOptions($items),
'#multiple' => $this->multiple && count($this->options) > 1,
- );
-
- $element['#attributes']['placeholder'] = $this->t('Start typing to see a list of options...');
+ ]);
return $element;
}
+ /**
+ * {@inheritdoc}
+ */
+ public static function validateElement(array $element, FormStateInterface $form_state) {
+ if ($element['#required'] && $element['#value'] == '') {
+ $form_state->setError($element, t('@name field is required.', ['@name' => $element['#title']]));
+ }
+ parent::validateElement($element, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getOptions(FieldableEntityInterface $entity) {
+ if (!isset($this->options)) {
+ // Limit the settable options for the current user account.
+ $options = $this->fieldDefinition
+ ->getFieldStorageDefinition()
+ ->getOptionsProvider($this->column, $entity)
+ ->getSettableOptions(\Drupal::currentUser());
+
+ // Add an empty option if the widget needs one.
+ if ($empty_label = $this->getEmptyLabel()) {
+ $options = ['' => $empty_label] + $options;
+ }
+
+ $module_handler = \Drupal::moduleHandler();
+ $context = [
+ 'fieldDefinition' => $this->fieldDefinition,
+ 'entity' => $entity,
+ ];
+ $module_handler->alter('options_list', $options, $context);
+
+ array_walk_recursive($options, [$this, 'sanitizeLabel']);
+
+ // Options might be nested ("optgroups"). If the widget does not support
+ // nested options, flatten the list.
+ if (!$this->supportsGroups()) {
+ $options = OptGroup::flattenOptions($options);
+ }
+
+ $this->options = $options;
+ }
+ return $this->options;
+ }
+
/**
* {@inheritdoc}
*/
diff --git a/src/SelectizeElementTrait.php b/src/SelectizeElementTrait.php
new file mode 100644
index 0000000..2b108eb
--- /dev/null
+++ b/src/SelectizeElementTrait.php
@@ -0,0 +1,65 @@
+ FALSE,
+ 'createOnBlur' => FALSE,
+ 'hideTermId' => FALSE,
+ 'createFilter' => NULL,
+ 'delimiter' => ',',
+ 'delimiter_warning' => FALSE,
+ 'sortField' => 'text',
+ 'sortDirection' => 'asc',
+ 'allowEmptyOption' => FALSE,
+ 'plugins' => [],
+ 'highlight' => TRUE,
+ 'maxItems' => 10,
+ 'persist' => FALSE,
+ 'diacritics' => FALSE,
+ 'closeAfterSelect' => FALSE,
+ 'placeholder' => '',
+ ];
+
+ return array_merge($settings, $values);
+ }
+
+ /**
+ * Prepares a #type 'selectize' render element for input.html.twig.
+ *
+ * @param array $element
+ * An associative array containing the properties of the element.
+ * Properties used: #title, #value, #description, #size, #maxlength,
+ * #placeholder, #required, #attributes.
+ *
+ * @return array
+ * The $element with prepared variables ready for input.html.twig.
+ */
+ public static function preRenderSelectize(array $element) {
+ if (NestedArray::getValue($element, ['#settings', 'delimiter_warning'])) {
+ $description = t('Attention: "@delimiter" character used as delimiter!', ['@delimiter' => $element['#settings']['delimiter']]);
+ $element['#description'] = $element['#description'] ? $element['#description'] . ' ' . $description : $description;
+ }
+
+ return $element;
+ }
+
+}
+
diff --git a/src/SelectizeWidgetTrait.php b/src/SelectizeWidgetTrait.php
new file mode 100644
index 0000000..a5eab42
--- /dev/null
+++ b/src/SelectizeWidgetTrait.php
@@ -0,0 +1,209 @@
+ FALSE,
+ // If true, when user exits the field (clicks outside of input),
+ // a new option is created and selected (if create setting is enabled).
+ 'createOnBlur' => FALSE,
+ 'hideTermId' => FALSE,
+ // Specifies a RegExp or a string containing a regular expression that
+ // the current search filter must match to be allowed to be created. May
+ // also be a predicate function that takes the filter text and returns
+ // whether it is allowed.
+ // string|NULL
+ 'createFilter' => NULL,
+ 'delimiter' => $delimiter,
+ 'delimiter_warning' => FALSE,
+ 'sortField' => NULL,
+ 'sortDirection' => 'asc',
+ 'allowEmptyOption' => FALSE,
+ 'plugins' => ['remove_button'],
+ 'highlight' => TRUE,
+ 'persist' => FALSE,
+ 'diacritics' => FALSE,
+ 'closeAfterSelect' => FALSE,
+ 'placeholder' => 'Start typing to see a list of options...',
+ ] + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsForm(array $form, FormStateInterface $form_state) {
+ $element = parent::settingsForm($form, $form_state);
+ $field_name = $this->fieldDefinition->getName();
+ $element['plugins'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Enabled Plugins'),
+ '#description' => $this->t('For a demonstration of what these plugins do, visit the demo site.', ['@url' => 'https://selectize.github.io/selectize.js/']),
+ '#default_value' => $this->getSetting('plugins'),
+ '#multiple' => TRUE,
+ '#options' => $this->pluginsOptions(),
+ ];
+ $element['sortField'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Sort field'),
+ '#description' => $this->t('Sort Settings'),
+ '#default_value' => $this->getSetting('sortField'),
+ '#options' => $this->sortOptions(),
+ ];
+ $element['sortDirection'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Sort direction'),
+ '#description' => $this->t('Direction can be set to "asc" or "desc". The order of the array defines the sort precedence.'),
+ '#default_value' => $this->getSetting('sortDirection'),
+ '#options' => $this->sortOptions(TRUE),
+ '#states' => [
+ 'visible' => [
+ 'select[name="fields[' . $field_name . '][settings_edit_form][settings][sortField]"]' => [
+ ['value' => 'text'],
+ ['value' => 'value'],
+ ],
+ ],
+ ],
+ ];
+ $element['closeAfterSelect'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Close after selecting an option'),
+ '#description' => $this->t('This will close the Selectized display after choosing a value.'),
+ '#default_value' => $this->getSetting('closeAfterSelect'),
+ ];
+ $element['diacritics'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Diacritics Support'),
+ '#description' => $this->t('Enable or disable international character support'),
+ '#default_value' => $this->getSetting('diacritics'),
+ ];
+ $element['placeholder'] = [
+ '#type' => 'textfield',
+ '#title' => t('Placeholder'),
+ '#default_value' => $this->getSetting('placeholder'),
+ '#description' => t('Text that will be shown inside the field until a value is entered. This hint is usually a sample value or a brief description of the expected format.'),
+ ];
+ if ($this->getPluginId() == 'selectize_entity_reference_widget' && $this->getSelectionHandlerSetting('auto_create')) {
+ $element['createOnBlur'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Create On Blur'),
+ '#description' => $this->t('If true, when user exits the field (clicks outside of input), a new option is created and selected (if create setting is enabled)'),
+ '#default_value' => $this->getSetting('createOnBlur'),
+ ];
+ $element['hideTermId'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Hide term ID'),
+ '#description' => $this->t('If true, the term ID will be hide for each element. This should be done to prevent possible value duplicates when using autocomplete and manual creation.'),
+ '#default_value' => $this->getSetting('hideTermId'),
+ ];
+ $element['createFilter'] = [
+ '#type' => 'textfield',
+ '#title' => t('Create filter(REGEX)'),
+ '#default_value' => $this->getSetting('createFilter'),
+ '#description' => t('Specifies a string containing a regular expression that the current search filter must match to be allowed to be created.'),
+ ];
+ $element['delimiter'] = [
+ '#type' => 'textfield',
+ '#title' => t('Delimiter'),
+ '#default_value' => $this->getSetting('delimiter'),
+ '#description' => t('The string to separate items by. When typing an item in a multi-selection control allowing creation, then the delimiter, the item is added. If you paste delimiter-separated items in such control, the items are added at once.'),
+ ];
+ $element['delimiter_warning'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Add Delimiter Warning'),
+ '#description' => $this->t('If true, add a delimiter warning message to the field description.'),
+ '#default_value' => $this->getSetting('delimiter_warning'),
+ ];
+ }
+ else {
+ $element['placeholder'] = [
+ '#type' => 'textfield',
+ '#title' => t('Placeholder'),
+ '#default_value' => $this->getSetting('placeholder'),
+ '#description' => t('Text that will be shown inside the field until a value is entered. This hint is usually a sample value or a brief description of the expected format.'),
+ ];
+ }
+
+ return $element;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsSummary() {
+ $summary = parent::settingsSummary();
+ $settings = $this->getSettings();
+
+ if ($plugins = $settings['plugins']) {
+ $plugins = array_intersect_key($this->pluginsOptions(), array_flip($settings['plugins']));
+ $summary[] = $this->t('Enabled plugins: @plugins', ['@plugins' => implode(', ', $plugins)]);
+ }
+ if ($settings['sortField']) {
+ $summary[] = $this->t('@sort_field: @sort_direction', [
+ '@sort_field' => $this->sortOptions()[$settings['sortField']],
+ '@sort_direction' => $this->sortOptions(TRUE)[$settings['sortDirection']],
+ ]);
+ }
+ if ($settings['closeAfterSelect']) {
+ $summary[] = $this->t('Closed the Selectized display after choosing a value.');
+ }
+ if ($settings['diacritics']) {
+ $summary[] = $this->t('Enabled international character support.');
+ }
+
+ return $summary;
+ }
+
+ /**
+ * Helper method for getting sort options.
+ *
+ * @param bool $direction
+ * Indicates to return available sort direction options.
+ *
+ * @return array
+ * The array of sort options.
+ */
+ public function sortOptions($direction = FALSE) {
+ if ($direction) {
+ return [
+ 'asc' => $this->t('Ascending'),
+ 'desc' => $this->t('Descending'),
+ ];
+ }
+ else {
+ return [
+ '' => $this->t('Default sorting'),
+ 'value' => $this->t('Sort by value'),
+ 'text' => $this->t('Sort by Text'),
+ ];
+ }
+ }
+
+ /**
+ * Helper method for getting available Plugins options.
+ *
+ * @return array
+ * The array of plugins options.
+ */
+ public function pluginsOptions() {
+ return [
+ 'remove_button' => $this->t('Remove Button'),
+ 'drag_drop' => $this->t('Drag & Drop'),
+ 'restore_on_backspace' => $this->t('Restore on Backspace'),
+ 'optgroup_columns' => $this->t('Optgroup Columns'),
+ 'dropdown_header' => $this->t('Dropdown Headers'),
+ ];
+ }
+
+}