diff --git a/config/schema/entity_browser.schema.yml b/config/schema/entity_browser.schema.yml
index 67a1d79..3f203ea 100644
--- a/config/schema/entity_browser.schema.yml
+++ b/config/schema/entity_browser.schema.yml
@@ -191,6 +191,23 @@ field.widget.settings.entity_browser_entity_reference:
type: string
label: 'Selection mode'
+field.widget.settings.entity_browser_entity_reference_autocomplete:
+ type: field.widget.settings.entity_reference_autocomplete
+ label: 'Entity browser entity reference autocomplete widget'
+ mapping:
+ entity_browser:
+ type: string
+ label: 'Entity Browser'
+ selection_mode:
+ type: string
+ label: 'Selection mode'
+ preview_view_mode:
+ type: string
+ label: 'Preview view mode'
+ preview_remove_button:
+ type: boolean
+ label: 'Preview remove button'
+
entity_browser.field_widget_display.label:
type: mapping
label: 'Entity label display config'
diff --git a/css/entity_browser.entity_browser_entity_reference_autocomplete.css b/css/entity_browser.entity_browser_entity_reference_autocomplete.css
new file mode 100644
index 0000000..e7211d0
--- /dev/null
+++ b/css/entity_browser.entity_browser_entity_reference_autocomplete.css
@@ -0,0 +1,4 @@
+.entity-browser-entity-reference-autocomplete-entity-browser-wrapper > div,
+.entity-browser-entity-reference-autocomplete-entity-browser-wrapper > input {
+ display: inline-block;
+}
diff --git a/entity_browser.libraries.yml b/entity_browser.libraries.yml
index bf6d05d..6d80265 100644
--- a/entity_browser.libraries.yml
+++ b/entity_browser.libraries.yml
@@ -50,6 +50,14 @@ entity_reference:
- core/drupal
- core/jquery
+entity_browser_entity_reference_autocomplete:
+ version: VERSION
+ css:
+ theme:
+ css/entity_browser.entity_browser_entity_reference_autocomplete.css: {}
+ js:
+ js/entity_browser.entity_browser_entity_reference_autocomplete.js: { }
+
file_browser:
css:
component:
diff --git a/js/entity_browser.entity_browser_entity_reference_autocomplete.js b/js/entity_browser.entity_browser_entity_reference_autocomplete.js
new file mode 100644
index 0000000..f7c98a5
--- /dev/null
+++ b/js/entity_browser.entity_browser_entity_reference_autocomplete.js
@@ -0,0 +1,73 @@
+/**
+ * @file entity_browser.entity_browser_entity_reference_autocomplete.js
+ *
+ * Defines the behavior related to entity browser link inside autocomplete.
+ */
+
+(function ($, Drupal, drupalSettings, once) {
+
+ 'use strict';
+
+ /**
+ * Registers behaviours related to entity browser link inside autocomplete.
+ */
+ Drupal.behaviors.linkEntityBrowserInsideAutocomplete = {
+ attach: function (context) {
+
+ $(once('register-entity-browser-link', 'input.form-autocomplete.entity-browser-link', context))
+ .autocomplete(Drupal.autocomplete.options)
+ .each(function () {
+ var $this = $(this);
+
+ // Add link to advanced search at the end of autocomplete suggestions.
+ // It opens entity browser.
+ $this.on('autocompleteresponse', function (event, ui) {
+ var has_entity_browser_link = $.grep(ui.content, function (e) {
+ return e.value === 'open-entity-browser';
+ }).length;
+
+ if (!has_entity_browser_link) {
+ ui.content.push({
+ value: 'open-entity-browser',
+ label: '' + drupalSettings.entity_browser_entity_reference_autocomplete.entity_browser_link_label + '',
+ });
+ }
+ });
+
+ // Make sure that we skip all actions that might be triggered by
+ // "autocompleteclose" event. The only thing that should happen is to
+ // open entity browser.
+ $this.on('autocompleteclose', function (event, ui) {
+ if (event.target.value === 'open-entity-browser' && event.target.hasAttribute('data-entity-browser-uuid')) {
+ var entity_browser = document.querySelector('[data-uuid="' + $(event.target).attr('data-entity-browser-uuid') + '"]');
+ if (entity_browser) {
+ event.target.value = '';
+ entity_browser.click();
+ event.target.preventDefault();
+ }
+ }
+ });
+ });
+ }
+ };
+
+ /**
+ * Overridden from core/misc/autocomplete.js to select new item.
+ */
+ function selectHandler(event, ui) {
+ // Open entity browser.
+ if (ui.item.value === 'open-entity-browser') {
+ event.target.value = ui.item.value
+ }
+ else {
+ Drupal.autocomplete.options.__select(event, ui);
+ }
+
+ return false;
+ }
+
+ // Override the select handler from core.
+ Drupal.autocomplete.options.__select = Drupal.autocomplete.options.select;
+ Drupal.autocomplete.options.select = selectHandler;
+
+}(jQuery, Drupal, drupalSettings, once));
diff --git a/src/Plugin/Field/FieldWidget/EntityReferenceAutocompleteBrowserWidget.php b/src/Plugin/Field/FieldWidget/EntityReferenceAutocompleteBrowserWidget.php
new file mode 100644
index 0000000..ea93252
--- /dev/null
+++ b/src/Plugin/Field/FieldWidget/EntityReferenceAutocompleteBrowserWidget.php
@@ -0,0 +1,836 @@
+entityTypeManager = $entity_type_manager;
+ $this->selectionStorage = $selection_storage;
+ $this->displayManager = $display_manager;
+ $this->entityRepository = $entity_repository;
+ $this->entityDisplayRepository = $entity_display_repository;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $plugin_id,
+ $plugin_definition,
+ $configuration['field_definition'],
+ $configuration['settings'],
+ $configuration['third_party_settings'],
+ $container->get('entity_type.manager'),
+ $container->get('entity_browser.selection_storage'),
+ $container->get('plugin.manager.entity_browser.display'),
+ $container->get('entity.repository'),
+ $container->get('entity_display.repository')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function defaultSettings() {
+ return [
+ 'entity_browser' => NULL,
+ 'selection_mode' => EntityBrowserElement::SELECTION_MODE_APPEND,
+ 'preview_view_mode' => '',
+ 'preview_remove_button' => FALSE,
+ ] + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsForm(array $form, FormStateInterface $form_state) {
+ $element = parent::settingsForm($form, $form_state);
+
+ $element['entity_browser'] = [
+ '#title' => $this->t('Entity browser'),
+ '#type' => 'select',
+ '#default_value' => $this->getSetting('entity_browser'),
+ '#options' => [],
+ ];
+ /** @var \Drupal\entity_browser\EntityBrowserInterface[] $entity_browsers */
+ $entity_browsers = $this
+ ->entityTypeManager
+ ->getStorage('entity_browser')
+ ->loadMultiple();
+ foreach ($entity_browsers as $entity_browser) {
+ $element['entity_browser']['#options'][$entity_browser->id()] = $entity_browser->label();
+ }
+
+ $element['selection_mode'] = [
+ '#title' => $this->t('Selection mode'),
+ '#description' => $this->t('Determines how selection in entity browser will be handled. Will selection be appended/prepended or it will be replaced in case of editing.'),
+ '#type' => 'select',
+ '#options' => EntityBrowserElement::getSelectionModeOptions(),
+ '#default_value' => $this->getSetting('selection_mode'),
+ ];
+
+ $element['#element_validate'][] = [$this, 'validateSettingsForm'];
+
+ $options = [];
+ $target_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type');
+ $entity_type = $this->entityTypeManager->getStorage($target_type)->getEntityType();
+ foreach ($this->entityDisplayRepository->getViewModeOptions($entity_type->id()) as $id => $view_mode_label) {
+ $options[$id] = $view_mode_label;
+ }
+
+ $element['preview_view_mode'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Preview view mode'),
+ '#description' => $this->t('Select view mode to be used preview.'),
+ '#default_value' => $this->getSetting('preview_view_mode'),
+ '#options' => array_merge(['' => $this->t('Disabled')], $options),
+ ];
+
+ $element['preview_remove_button'] = [
+ '#title' => $this->t('Display Remove button in preview'),
+ '#type' => 'checkbox',
+ '#default_value' => $this->getSetting('preview_remove_button'),
+ ];
+
+ return $element;
+ }
+
+ /**
+ * Validate the settings form.
+ *
+ * @param array $element
+ * An associative array containing the structure of the element.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ * @param array $complete_form
+ * The complete form structure.
+ */
+ public function validateSettingsForm(array $element, FormStateInterface $form_state, array $complete_form) {
+ $values = NestedArray::getValue($form_state->getValues(), $element['#parents']);
+
+ /** @var \Drupal\entity_browser\Entity\EntityBrowser $entity_browser */
+ $entity_browser = $this
+ ->entityTypeManager
+ ->getStorage('entity_browser')
+ ->load($values['entity_browser']);
+
+ if ($values['selection_mode'] == EntityBrowserElement::SELECTION_MODE_EDIT && $entity_browser->getSelectionDisplay()->supportsPreselection() === FALSE) {
+ $tparams = [
+ '%selection_mode' => EntityBrowserElement::getSelectionModeOptions()[EntityBrowserElement::SELECTION_MODE_EDIT],
+ '@browser_link' => $entity_browser->toLink($entity_browser->label(), 'edit-form')->toString(),
+ ];
+ $form_state->setError($element['entity_browser'], $this->t('This widget requires the %selection_mode selection mode. Either choose another entity browser or update the @browser_link entity browser to use a selection display plugin that supports preselection.', $tparams));
+ }
+ elseif ($entity_browser->getDisplay()->getPluginId() !== 'modal') {
+ $modal_definition = $this->displayManager->getDefinition('modal');
+ $tparams = [
+ '%display' => $modal_definition['label'],
+ '@browser_link' => $entity_browser->toLink($entity_browser->label(), 'edit-form')->toString(),
+ ];
+ $form_state->setError($element['entity_browser'], $this->t('This widget requires the %display display. Either choose another entity browser or update the @browser_link entity browser to use the %display display.', $tparams));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsSummary() {
+ $summary = parent::settingsSummary();
+
+ $entity_browser_id = $this->getSetting('entity_browser');
+ if (empty($entity_browser_id)) {
+ return [$this->t('No entity browser selected.')];
+ }
+ else {
+ $entity_browser = $this
+ ->entityTypeManager
+ ->getStorage('entity_browser')
+ ->load($entity_browser_id);
+ if ($entity_browser) {
+ $summary[] = $this->t('Entity browser: @browser', ['@browser' => $entity_browser->label()]);
+ }
+ else {
+ $this->messenger->addError($this->t('Missing entity browser!'));
+ return [$this->t('Missing entity browser!')];
+ }
+ }
+
+ return $summary;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL) {
+ // If entities were selected in the entity browser, make sure they are
+ // applied to the entity reference autocomplete field.
+ // @todo Is this the correct place to do this?
+ $this->applyEntityBrowserSelection($form['#parents'], $this->fieldDefinition->getName(), $form_state);
+
+ return parent::form($items, $form, $form_state, $get_delta);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+ $element = parent::formElement($items, $delta, $element, $form, $form_state);
+
+ $cardinality = $this
+ ->fieldDefinition
+ ->getFieldStorageDefinition()
+ ->getCardinality();
+ if ($cardinality !== 1) {
+ return $element;
+ }
+
+ $field_name = $this->fieldDefinition->getName();
+ $id_prefix = implode('-', array_merge($form['#parents'], [$field_name]));
+ $element['#id'] = Html::getUniqueId($id_prefix . '-entity-browser-wrapper');
+ $element['#type'] = 'container';
+ $element['#attributes']['class'][] = 'entity-browser-entity-reference-autocomplete-entity-browser-wrapper';
+
+ $this->formElementPreview($items, $delta, $element, $form_state);
+ $this->addEntityBrowserElement($element);
+
+ return $element;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) {
+ $elements = parent::formMultipleElements($items, $form, $form_state);
+
+ $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
+ if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && !$form_state->isProgrammed()) {
+ $field_name = $this->fieldDefinition->getName();
+ $id_prefix = implode('-', array_merge($form['#parents'], [$field_name]));
+ $elements['#id'] = Html::getUniqueId($id_prefix . '-entity-browser-wrapper');
+ if ($this->getSetting('preview_view_mode')) {
+ for ($delta = 0; $delta <= $elements['#max_delta']; $delta++) {
+ $element = &$elements[$delta];
+ $element['#wrapper_id'] = $elements['#id'];
+ $element['#type'] = 'container';
+ $element['#attributes']['class'][] = 'entity-browser-entity-reference-autocomplete-entity-browser-wrapper';
+ $this->formElementPreview($items, $delta, $element, $form_state);
+ }
+ }
+ $this->addEntityBrowserElement($elements);
+ $elements['#prefix'] = '
' . $elements['#prefix'];
+ $elements['#suffix'] .= '
';
+ $elements['#pre_render'][] = [$this, 'alignButtons'];
+ }
+
+ for ($delta = 0; $delta <= $elements['#max_delta']; $delta++) {
+ $elements[$delta]['target_id']['#attributes']['class'][] = 'entity-browser-link';
+ }
+
+ return $elements;
+ }
+
+ /**
+ * Adds preview to the form element.
+ *
+ * @param \Drupal\Core\Field\FieldItemListInterface $items
+ * Array of default values for this field.
+ * @param int $delta
+ * The order of item in the array of sub-elements.
+ * @param array $element
+ * The form element array containing basic properties for the widget.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ protected function formElementPreview(FieldItemListInterface $items, $delta, array &$element, FormStateInterface $form_state) {
+ // We can stop here, if preview is not set on the widget configuration.
+ if (empty($this->getSetting('preview_view_mode'))) {
+ return $element;
+ }
+
+ $element['target_id']['#field_name'] = $this->fieldDefinition->getName();
+ $element['target_id']['#ajax'] = [
+ 'callback' => [static::class, 'updateWidgetCallback'],
+ 'wrapper' => $element['#wrapper_id'] ?? $element['#id'],
+ 'event' => 'change autocompleteclose paste',
+ ];
+
+ $cardinality = $this
+ ->fieldDefinition
+ ->getFieldStorageDefinition()
+ ->getCardinality();
+ $element['entity_preview'] = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'class' => ['entity-preview'],
+ ],
+ '#weight' => $cardinality === 1 ? 0 : $element['target_id']['#weight'] + 1,
+ '#tree' => FALSE,
+ ];
+
+ // Get current value of the widget. First, check user input. If nothing
+ // found, check if default value exists.
+ $user_input = $form_state->getUserInput();
+ $values = NestedArray::getValue($user_input, array_merge($element['target_id']['#field_parents'], [$element['target_id']['#field_name']]));
+ $target_id = NULL;
+ if (empty($values)) {
+ $target_id = $items[$delta]->getValue()['target_id'] ?? NULL;
+ }
+ elseif (!empty($values[$delta]['target_id'])) {
+ $target_id = EntityAutocomplete::extractEntityIdFromAutocompleteInput($values[$delta]['target_id']);
+ }
+
+ if ($target_id) {
+ $target_entity = $this
+ ->entityTypeManager
+ ->getStorage($element['target_id']['#target_type'])
+ ->load($target_id);
+
+ if ($target_entity) {
+ $element['entity_preview']['preview'] = [
+ '#type' => 'fieldgroup',
+ 'value' => $this
+ ->entityTypeManager
+ ->getViewBuilder($element['target_id']['#target_type'])
+ ->view($target_entity, $this->getSetting('preview_view_mode')),
+ ];
+
+ // For multivalue fields we have remove ("x") button, no need to add
+ // second one just for preview.
+ if ($this->getSetting('preview_remove_button') && $cardinality === 1) {
+ $element['entity_preview']['preview']['preview_remove_button'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Remove'),
+ '#ajax' => [
+ 'callback' => [static::class, 'updateWidgetCallback'],
+ 'wrapper' => $element['#wrapper_id'] ?? $element['#id'],
+ ],
+ '#submit' => [[static::class, 'removeItemSubmit']],
+ '#name' => "{$element['target_id']['#field_name']}_{$delta}_preview_remove_button",
+ '#limit_validation_errors' => [array_merge($element['target_id']['#field_parents'], [$this->fieldDefinition->getName()])],
+ '#attributes' => ['class' => ['remove-button']],
+ '#access' => TRUE,
+ ];
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds the entity browser element to the widget form.
+ *
+ * @param array $elements
+ * An associative array containing the structure of the element.
+ */
+ protected function addEntityBrowserElement(array &$elements) {
+ $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
+ $persistent_data = $this->getPersistentData();
+ $selection_mode = $this->getSetting('selection_mode');
+
+ $elements['entity_browser'] = [
+ '#type' => 'entity_browser',
+ '#entity_browser' => $this->getSetting('entity_browser'),
+ '#selection_mode' => $selection_mode,
+ '#cardinality' => $cardinality,
+ '#process' => [
+ [EntityBrowserElement::class, 'processEntityBrowser'],
+ [$this, 'processEntityBrowser'],
+ ],
+ '#wrapper_id' => $elements['#wrapper_id'] ?? $elements['#id'],
+ '#entity_browser_validators' => $persistent_data['validators'],
+ '#widget_context' => $persistent_data['widget_context'],
+ '#weight' => 0,
+ ];
+ $elements['#attached']['library'][] = 'entity_browser/entity_browser_entity_reference_autocomplete';
+ $elements['#attached']['drupalSettings']['entity_browser_entity_reference_autocomplete'] = [
+ 'entity_browser_link_label' => $this->t("Didn't find what you were looking for? Advanced search"),
+ ];
+ }
+
+ /**
+ * Gets data that should persist across Entity Browser renders.
+ *
+ * @return array
+ * Data that should persist after the Entity Browser is rendered.
+ */
+ protected function getPersistentData() {
+ $settings = $this->fieldDefinition->getSettings();
+ $handler = $settings['handler_settings'];
+ return [
+ 'validators' => [
+ 'entity_type' => ['type' => $settings['target_type']],
+ ],
+ 'widget_context' => [
+ 'target_bundles' => !empty($handler['target_bundles']) ? $handler['target_bundles'] : [],
+ 'target_entity_type' => $settings['target_type'],
+ 'cardinality' => $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(),
+ ],
+ ];
+ }
+
+ /**
+ * Process callback for the entity browser form element.
+ *
+ * @param array $element
+ * The form element to process.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ * @param array $complete_form
+ * The complete form structure.
+ *
+ * @return array
+ * The form element.
+ */
+ public function processEntityBrowser(array &$element, FormStateInterface $form_state, array &$complete_form) {
+ // Trigger the AJAX callback whenever the entity browser is submitted.
+ // #ajax is officially not supported for hidden elements but if we specify
+ // the event manually it works.
+ $element['entity_ids']['#ajax'] = [
+ 'callback' => [static::class, 'updateFieldWidgetAjaxCallback'],
+ 'wrapper' => $element['#wrapper_id'],
+ 'event' => 'entity_browser_value_updated',
+ ];
+
+ // Whenever the entity browser button is opened, we update the default
+ // selected entities with the ones referenced through the autocomplete
+ // textfields.
+ $element['entity_browser']['open_modal']['#element_validate'][] = [
+ $this,
+ 'updateEntityBrowserDefaultSelectedEntities',
+ ];
+
+ // Store the entity browser instance UUID as a hidden value. This way we
+ // can identify which instance is being opened (since the instance always
+ // changes on form rebuild).
+ $entity_browser_uuid = $element['entity_browser']['open_modal']['#attributes']['data-uuid'];
+ $element['entity_browser']['uuid'] = [
+ '#type' => 'hidden',
+ // @todo How can we retrieve the instance UUID in a more sane way?
+ '#value' => $entity_browser_uuid,
+ ];
+
+ $parents = array_slice($element['#array_parents'], 0, -1);
+ if (end($parents) === 0) {
+ $parents = array_slice($parents, 0, -1);
+ }
+ $complete_form_element = &NestedArray::getValue($complete_form, $parents);
+ for ($delta = 0; $delta <= $complete_form_element['#max_delta']; $delta++) {
+ $complete_form_element[$delta]['target_id']['#attributes']['data-entity-browser-uuid'] = $entity_browser_uuid;
+ }
+
+ return $element;
+ }
+
+ /**
+ * Ajax callback for the entity browser button.
+ *
+ * This returns the updated field widget content, with the entities selected
+ * in the entity browser taken into account.
+ *
+ * @param array $form
+ * The form where the settings form is being included in.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @return array
+ * The part of the form that needs to be updated (eg. the field widget
+ * element).
+ */
+ public static function updateFieldWidgetAjaxCallback(array &$form, FormStateInterface $form_state) {
+ $triggering_element = $form_state->getTriggeringElement();
+ $parents = array_slice($triggering_element['#array_parents'], 0, -2);
+ return NestedArray::getValue($form, $parents);
+ }
+
+ /**
+ * Update the default selected entities of an entity browsers instance.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function updateEntityBrowserDefaultSelectedEntities(array $form, FormStateInterface $form_state) {
+ // Only update the default selected entities for the field the entity
+ // browser is being opened for.
+ $triggering_element = $form_state->getTriggeringElement();
+ if ($form['#name'] !== $triggering_element['#name']) {
+ return;
+ }
+
+ // Get the entity browser instance UUID.
+ $user_input = $form_state->getUserInput();
+ $uuid = NestedArray::getValue($user_input, array_merge(array_slice($form['#parents'], 0, -1), ['uuid']));
+ if (!$uuid) {
+ return;
+ }
+
+ // Save the selected entities (within the entity reference autocomplete
+ // fields) in the entity browsers persistent data.
+ $persistent_data = $this->selectionStorage->get($uuid);
+ $field_cardinality = $this
+ ->fieldDefinition
+ ->getFieldStorageDefinition()
+ ->getCardinality();
+ // If the field cardinality is 1, the entity browser was added to the first
+ // child of the widget. In any other case, the entity browser was added to
+ // the widget itself.
+ // @see \Drupal\entity_browser\Plugin\Field\FieldWidget\EntityReferenceAutocompleteBrowserWidget::formElement()
+ // @see \Drupal\entity_browser\Plugin\Field\FieldWidget\EntityReferenceAutocompleteBrowserWidget::formMultipleElements()
+ $length = $field_cardinality === 1 ? -4 : -3;
+ $parents = array_slice($form['#parents'], 0, $length);
+ $field_name = array_pop($parents);
+ $persistent_data['selected_entities'] = $this->getSelectedEntities($parents, $field_name, $form_state);
+ $this->selectionStorage->set($uuid, $persistent_data);
+ }
+
+ /**
+ * Pre-render callback for aligning the 'add more' and entity browser buttons.
+ *
+ * The default template explicitly repositions the 'add more' button. In order
+ * to align both, we transform the 'add more' button into a container element
+ * that contains the 'add more' and entity browser button.
+ *
+ * @param array $element
+ * An associative array containing the structure of the element.
+ *
+ * @return array
+ * The altered elements array.
+ *
+ * @see template_preprocess_field_multiple_value_form()
+ * @see field-multiple-value-form.html.twig
+ */
+ public function alignButtons(array $element) {
+ $element['add_more'] = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'class' => ['entity-browser-entity-reference-autocomplete-entity-browser-wrapper'],
+ ],
+ 'entity_browser' => $element['entity_browser'],
+ 'add_more' => $element['add_more'],
+ ];
+
+ unset($element['entity_browser']);
+ return $element;
+ }
+
+ /**
+ * Apply the entity browser selection to the entity reference field.
+ *
+ * @param array $parents
+ * The array of #parents where the field lives in the form.
+ * @param string $field_name
+ * The field name.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ */
+ protected function applyEntityBrowserSelection(array $parents, $field_name, FormStateInterface $form_state) {
+ // If no entity browser was used, we can stop right here.
+ $triggering_element = $form_state->getTriggeringElement();
+ if (!$triggering_element || empty($triggering_element['#parents']) || !in_array($field_name, $triggering_element['#parents'], TRUE) || end($triggering_element['#parents']) !== 'entity_ids') {
+ return;
+ }
+
+ $user_input = $form_state->getUserInput();
+ $values = NestedArray::getValue($user_input, array_merge($parents, [$field_name]));
+ // First we remove all the existing delta's from the input.
+ $values = array_filter($values, function ($value) {
+ return !is_array($value) || !array_key_exists('target_id', $value);
+ });
+ // Now add the selected entities to the input.
+ $selected_entities = $this->getSelectedEntities($parents, $field_name, $form_state);
+ foreach ($selected_entities as $delta => $entity) {
+ $values[$delta] = [
+ 'target_id' => $this->getAutocompleteMatcherKey($entity),
+ '_weight' => $delta,
+ ];
+ }
+ NestedArray::setValue($user_input, array_merge($parents, [$field_name]), $values);
+ $form_state->setUserInput($user_input);
+
+ // The amount of selected entities may have changed. Therefore we need to
+ // update the 'items_count' property in the widgets state. Based on this
+ // property the amount of autocomplete textfields to be displayed is
+ // decided.
+ // @see \Drupal\Core\Field\WidgetBase::formMultipleElements()
+ $field_state = static::getWidgetState($parents, $field_name, $form_state);
+ if (!$field_state) {
+ $field_state = [
+ 'items_count' => 0,
+ 'array_parents' => [],
+ ];
+ }
+ $field_state['items_count'] = count($selected_entities);
+ static::setWidgetState($parents, $field_name, $form_state, $field_state);
+ }
+
+ /**
+ * Gets the entity autocomplete matcher key for an entity.
+ *
+ * The key is what's submitted through the form when an entity is referenced
+ * through the entity reference autocomplete field. It contains the
+ * (translated) entity label and its IDs in the following structure:
+ * "$label ($entity_id)". This structure is required for validation to work
+ * in the entity autocomplete element.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity object.
+ *
+ * @return string
+ * A key that can be interpreted by the entity autocomplete matcher.
+ *
+ * @see \Drupal\Core\Entity\EntityAutocompleteMatcher::getMatches()
+ * @see \Drupal\Core\Entity\Element\EntityAutocomplete::validateEntityAutocomplete()
+ */
+ protected function getAutocompleteMatcherKey(EntityInterface $entity) {
+ $label = Html::escape($this->entityRepository->getTranslationFromContext($entity)->label());
+ $key = "{$label} ({$entity->id()})";
+ // Strip things like starting/trailing white spaces, line breaks and
+ // tags.
+ $key = preg_replace('/\s\s+/', ' ', str_replace("\n", '', trim(Html::decodeEntities(strip_tags($key)))));
+ // Names containing commas or quotes must be wrapped in quotes.
+ $key = Tags::encode($key);
+
+ return $key;
+ }
+
+ /**
+ * Retrieves the selected entities.
+ *
+ * @param array $parents
+ * The array of #parents where the field lives in the form.
+ * @param string $field_name
+ * The field name.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ * @param \Drupal\Core\Field\FieldItemListInterface|null $items
+ * An array of the field values. When creating a new entity this may be NULL
+ * or an empty array to use default values.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface[]
+ * An array of entity objects, if any.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+ * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+ */
+ public function getSelectedEntities(array $parents, $field_name, FormStateInterface $form_state, FieldItemListInterface $items = NULL) {
+ $triggering_element = $form_state->getTriggeringElement();
+ if ($triggering_element && !empty($triggering_element['#parents']) && in_array($field_name, $triggering_element['#parents'], TRUE)) {
+ $parent = end($triggering_element['#parents']);
+ if ($parent === 'entity_ids') {
+ return $form_state->getValue(array_merge(array_slice($triggering_element['#parents'], 0, -1), ['entities']));
+ }
+ }
+
+ $user_input = $form_state->getUserInput();
+ $values = NestedArray::getValue($user_input, array_merge($parents, [$field_name]));
+ if (!empty($values)) {
+ $values = array_filter($values, function ($value) {
+ return is_array($value) && array_key_exists('target_id', $value);
+ });
+
+ usort($values, function ($a, $b) {
+ return SortArray::sortByKeyInt($a, $b, '_weight');
+ });
+
+ $entity_ids = array_map(function ($item) {
+ return is_array($item) && !empty($item['target_id'])
+ ? EntityAutocomplete::extractEntityIdFromAutocompleteInput($item['target_id'])
+ : NULL;
+ }, $values);
+
+ $filtered_entity_ids = array_unique(array_filter($entity_ids));
+
+ if (!empty($filtered_entity_ids)) {
+ $entity_type_id = $this
+ ->fieldDefinition
+ ->getFieldStorageDefinition()
+ ->getSetting('target_type');
+
+ $entities = $this
+ ->entityTypeManager
+ ->getStorage($entity_type_id)
+ ->loadMultiple($filtered_entity_ids);
+
+ // Selection can contain same entity multiple times. Since
+ // \Drupal\Core\Entity\EntityStorageInterface::loadMultiple() returns a
+ // unique list of entities, it's necessary to recreate list of entities
+ // in order to preserve selection of duplicated entities.
+ return array_filter(array_map(function ($entity_id) use ($entities) {
+ return $entities[$entity_id] ?? NULL;
+ }, array_values($entity_ids)));
+ }
+ }
+
+ // We are loading for for the first time so we need to load any existing
+ // values that might already exist on the entity. Also, remove any leftover
+ // data from removed entity references.
+ if ($items) {
+ return array_filter($items->referencedEntities());
+ }
+
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
+ $values = array_filter($values, function ($value) {
+ return empty($value['_original_delta']) || $value['_original_delta'] !== 'entity_browser';
+ });
+
+ return parent::massageFormValues($values, $form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function trustedCallbacks() {
+ return [
+ 'alignButtons',
+ ];
+ }
+
+ /**
+ * AJAX callback for the selected element preview.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @return array
+ * An associative array containing the structure of the element to replace
+ * the wrapper with.
+ */
+ public static function updateWidgetCallback(array &$form, FormStateInterface $form_state) {
+ $triggering_element = $form_state->getTriggeringElement();
+ $depth = end($triggering_element['#array_parents']) === 'preview_remove_button'
+ ? -3
+ : -2;
+
+ $element = NestedArray::getValue($form, array_merge(
+ array_slice($triggering_element['#array_parents'], 0, $depth),
+ ));
+ unset($element['_weight']);
+ unset($element['_actions']);
+
+ return $element;
+ }
+
+ /**
+ * Submit callback for remove buttons.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public static function removeItemSubmit(&$form, FormStateInterface $form_state) {
+ $triggering_element = $form_state->getTriggeringElement();
+ if (end($triggering_element['#array_parents']) !== 'preview_remove_button') {
+ return;
+ }
+
+ $target_id_element = &NestedArray::getValue($form, array_merge(array_slice($triggering_element['#array_parents'], 0, -3), ['target_id']));
+ $form_state->setValueForElement($target_id_element, '');
+ NestedArray::setValue($form_state->getUserInput(), $target_id_element['#parents'], '');
+ $form_state->setRebuild();
+ }
+
+}