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'), + ]; + } + +}