3 namespace Drupal\entity_browser\Plugin\Field\FieldWidget;
5 use Drupal\Core\Entity\EntityInterface;
6 use Drupal\entity_browser\Element\EntityBrowserElement;
7 use Symfony\Component\Validator\ConstraintViolationInterface;
8 use Drupal\Component\Utility\Html;
9 use Drupal\Component\Utility\NestedArray;
10 use Drupal\Core\Entity\ContentEntityInterface;
11 use Drupal\Core\Entity\EntityTypeManagerInterface;
12 use Drupal\Core\Field\FieldDefinitionInterface;
13 use Drupal\Core\Field\FieldItemListInterface;
14 use Drupal\Core\Field\WidgetBase;
15 use Drupal\Core\Form\FormStateInterface;
16 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
18 use Drupal\Core\Validation\Plugin\Validation\Constraint\NotNullConstraint;
19 use Drupal\entity_browser\FieldWidgetDisplayManager;
20 use Symfony\Component\DependencyInjection\ContainerInterface;
21 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
22 use Symfony\Component\Validator\ConstraintViolation;
23 use Symfony\Component\Validator\ConstraintViolationListInterface;
24 use Drupal\Core\Extension\ModuleHandlerInterface;
27 * Plugin implementation of the 'entity_reference' widget for entity browser.
30 * id = "entity_browser_entity_reference",
31 * label = @Translation("Entity browser"),
32 * description = @Translation("Uses entity browser to select entities."),
33 * multiple_values = TRUE,
39 class EntityReferenceBrowserWidget extends WidgetBase implements ContainerFactoryPluginInterface {
42 * Entity type manager service.
44 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
46 protected $entityTypeManager;
49 * Field widget display plugin manager.
51 * @var \Drupal\entity_browser\FieldWidgetDisplayManager
53 protected $fieldDisplayManager;
56 * The depth of the delete button.
58 * This property exists so it can be changed if subclasses.
62 protected static $deleteDepth = 4;
65 * The module handler interface.
67 * @var \Drupal\Core\Extension\ModuleHandlerInterface
69 protected $moduleHandler;
72 * Constructs widget plugin.
74 * @param string $plugin_id
75 * The plugin_id for the plugin instance.
76 * @param mixed $plugin_definition
77 * The plugin implementation definition.
78 * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
79 * The definition of the field to which the widget is associated.
80 * @param array $settings
81 * The widget settings.
82 * @param array $third_party_settings
83 * Any third party settings.
84 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
85 * Entity type manager service.
86 * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
88 * @param \Drupal\entity_browser\FieldWidgetDisplayManager $field_display_manager
89 * Field widget display plugin manager.
90 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
91 * The module handler service.
93 public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, EventDispatcherInterface $event_dispatcher, FieldWidgetDisplayManager $field_display_manager, ModuleHandlerInterface $module_handler) {
94 parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
95 $this->entityTypeManager = $entity_type_manager;
96 $this->fieldDisplayManager = $field_display_manager;
97 $this->moduleHandler = $module_handler;
103 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
107 $configuration['field_definition'],
108 $configuration['settings'],
109 $configuration['third_party_settings'],
110 $container->get('entity_type.manager'),
111 $container->get('event_dispatcher'),
112 $container->get('plugin.manager.entity_browser.field_widget_display'),
113 $container->get('module_handler')
120 public static function defaultSettings() {
122 'entity_browser' => NULL,
124 'field_widget_display' => 'label',
125 'field_widget_edit' => TRUE,
126 'field_widget_remove' => TRUE,
127 'field_widget_display_settings' => [],
128 'selection_mode' => EntityBrowserElement::SELECTION_MODE_APPEND,
129 ) + parent::defaultSettings();
135 public function settingsForm(array $form, FormStateInterface $form_state) {
136 $element = parent::settingsForm($form, $form_state);
139 /** @var \Drupal\entity_browser\EntityBrowserInterface $browser */
140 foreach ($this->entityTypeManager->getStorage('entity_browser')->loadMultiple() as $browser) {
141 $browsers[$browser->id()] = $browser->label();
144 $element['entity_browser'] = [
145 '#title' => $this->t('Entity browser'),
147 '#default_value' => $this->getSetting('entity_browser'),
148 '#options' => $browsers,
151 $target_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type');
152 $entity_type = $this->entityTypeManager->getStorage($target_type)->getEntityType();
155 foreach ($this->fieldDisplayManager->getDefinitions() as $id => $definition) {
156 if ($this->fieldDisplayManager->createInstance($id)->isApplicable($entity_type)) {
157 $displays[$id] = $definition['label'];
161 $id = Html::getUniqueId('field-' . $this->fieldDefinition->getName() . '-display-settings-wrapper');
162 $element['field_widget_display'] = [
163 '#title' => $this->t('Entity display plugin'),
165 '#default_value' => $this->getSetting('field_widget_display'),
166 '#options' => $displays,
168 'callback' => array($this, 'updateSettingsAjax'),
173 $edit_button_access = TRUE;
174 if ($entity_type->id() == 'file') {
175 // For entities of type "file", it only makes sense to have the edit
176 // button if the module "file_entity" is present.
177 $edit_button_access = $this->moduleHandler->moduleExists('file_entity');
179 $element['field_widget_edit'] = [
180 '#title' => $this->t('Display Edit button'),
181 '#type' => 'checkbox',
182 '#default_value' => $this->getSetting('field_widget_edit'),
183 '#access' => $edit_button_access,
186 $element['field_widget_remove'] = [
187 '#title' => $this->t('Display Remove button'),
188 '#type' => 'checkbox',
189 '#default_value' => $this->getSetting('field_widget_remove'),
193 '#title' => $this->t('Show widget details as open by default'),
194 '#description' => $this->t('If marked, the fieldset container that wraps the browser on the entity form will be loaded initially expanded.'),
195 '#type' => 'checkbox',
196 '#default_value' => $this->getSetting('open'),
199 $element['selection_mode'] = [
200 '#title' => $this->t('Selection mode'),
201 '#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.'),
203 '#options' => EntityBrowserElement::getSelectionModeOptions(),
204 '#default_value' => $this->getSetting('selection_mode'),
207 $element['field_widget_display_settings'] = [
208 '#type' => 'fieldset',
209 '#title' => $this->t('Entity display plugin configuration'),
211 '#prefix' => '<div id="' . $id . '">',
212 '#suffix' => '</div>',
215 if ($this->getSetting('field_widget_display')) {
216 $element['field_widget_display_settings'] += $this->fieldDisplayManager
218 $form_state->getValue(
219 ['fields', $this->fieldDefinition->getName(), 'settings_edit_form', 'settings', 'field_widget_display'],
220 $this->getSetting('field_widget_display')
222 $form_state->getValue(
223 ['fields', $this->fieldDefinition->getName(), 'settings_edit_form', 'settings', 'field_widget_display_settings'],
224 $this->getSetting('field_widget_display_settings')
225 ) + ['entity_type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')]
227 ->settingsForm($form, $form_state);
234 * Ajax callback that updates field widget display settings fieldset.
236 public function updateSettingsAjax(array $form, FormStateInterface $form_state) {
237 return $form['fields'][$this->fieldDefinition->getName()]['plugin']['settings_edit_form']['settings']['field_widget_display_settings'];
243 public function settingsSummary() {
244 $summary = $this->summaryBase();
245 $field_widget_display = $this->getSetting('field_widget_display');
247 if (!empty($field_widget_display)) {
248 $plugin = $this->fieldDisplayManager->getDefinition($field_widget_display);
249 $summary[] = $this->t('Entity display: @name', ['@name' => $plugin['label']]);
257 public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
258 if ($violations->count() > 0) {
259 /** @var \Symfony\Component\Validator\ConstraintViolation $violation */
260 foreach ($violations as $offset => $violation) {
261 // The value of the required field is checked through the "not null"
262 // constraint, whose message is not very useful. We override it here for
264 if ($violation->getConstraint() instanceof NotNullConstraint) {
265 $violations->set($offset, new ConstraintViolation(
266 $this->t('@name field is required.', ['@name' => $items->getFieldDefinition()->getLabel()]),
269 $violation->getRoot(),
270 $violation->getPropertyPath(),
271 $violation->getInvalidValue(),
272 $violation->getPlural(),
273 $violation->getCode(),
274 $violation->getConstraint(),
275 $violation->getCause()
281 parent::flagErrors($items, $violations, $form, $form_state);
285 * Returns a key used to store the previously loaded entity.
287 * @param \Drupal\Core\Field\FieldItemListInterface $items
291 * A key for form state storage.
293 protected function getFormStateKey(FieldItemListInterface $items) {
294 return $items->getEntity()->uuid() . ':' . $items->getFieldDefinition()->getName();
300 public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
301 $entity_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type');
302 $entities = $this->formElementEntities($items, $element, $form_state);
304 // Get correct ordered list of entity IDs.
306 function (EntityInterface $entity) {
307 return $entity->id();
312 // We store current entity IDs as we might need them in future requests. If
313 // some other part of the form triggers an AJAX request with
314 // #limit_validation_errors we won't have access to the value of the
315 // target_id element and won't be able to build the form as a result of
316 // that. This will cause missing submit (Remove, Edit, ...) elements, which
317 // might result in unpredictable results.
318 $form_state->set(['entity_browser_widget', $this->getFormStateKey($items)], $ids);
320 $hidden_id = Html::getUniqueId('edit-' . $this->fieldDefinition->getName() . '-target-id');
321 $details_id = Html::getUniqueId('edit-' . $this->fieldDefinition->getName());
324 '#id' => $details_id,
325 '#type' => 'details',
326 '#open' => !empty($entities) || $this->getSetting('open'),
327 '#required' => $this->fieldDefinition->isRequired(),
328 // We are not using Entity browser's hidden element since we maintain
329 // selected entities in it during entire process.
333 // We need to repeat ID here as it is otherwise skipped when rendering.
334 '#attributes' => ['id' => $hidden_id],
335 '#default_value' => implode(' ', array_map(
336 function (EntityInterface $item) {
337 return $item->getEntityTypeId() . ':' . $item->id();
341 // #ajax is officially not supported for hidden elements but if we
342 // specify event manually it works.
344 'callback' => [get_class($this), 'updateWidgetCallback'],
345 'wrapper' => $details_id,
346 'event' => 'entity_browser_value_updated',
351 // Get configuration required to check entity browser availability.
352 $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
353 $selection_mode = $this->getSetting('selection_mode');
355 // Enable entity browser if requirements for that are fulfilled.
356 if (EntityBrowserElement::isEntityBrowserAvailable($selection_mode, $cardinality, count($ids))) {
357 $persistentData = $this->getPersistentData();
359 $element['entity_browser'] = [
360 '#type' => 'entity_browser',
361 '#entity_browser' => $this->getSetting('entity_browser'),
362 '#cardinality' => $cardinality,
363 '#selection_mode' => $selection_mode,
364 '#default_value' => $entities,
365 '#entity_browser_validators' => $persistentData['validators'],
366 '#widget_context' => $persistentData['widget_context'],
367 '#custom_hidden_id' => $hidden_id,
369 ['\Drupal\entity_browser\Element\EntityBrowserElement', 'processEntityBrowser'],
370 [get_called_class(), 'processEntityBrowser'],
375 $element['#attached']['library'][] = 'entity_browser/entity_reference';
377 $field_parents = $element['#field_parents'];
379 $element['current'] = $this->displayCurrentSelection($details_id, $field_parents, $entities);
385 * Render API callback: Processes the entity browser element.
387 public static function processEntityBrowser(&$element, FormStateInterface $form_state, &$complete_form) {
388 $uuid = key($element['#attached']['drupalSettings']['entity_browser']);
389 $element['#attached']['drupalSettings']['entity_browser'][$uuid]['selector'] = '#' . $element['#custom_hidden_id'];
396 public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
397 $entities = empty($values['target_id']) ? [] : explode(' ', trim($values['target_id']));
399 foreach ($entities as $entity) {
400 $return[]['target_id'] = explode(':', $entity)[1];
407 * AJAX form callback.
409 public static function updateWidgetCallback(array &$form, FormStateInterface $form_state) {
410 $trigger = $form_state->getTriggeringElement();
411 // AJAX requests can be triggered by hidden "target_id" element when
412 // entities are added or by one of the "Remove" buttons. Depending on that
413 // we need to figure out where root of the widget is in the form structure
414 // and use this information to return correct part of the form.
415 if (!empty($trigger['#ajax']['event']) && $trigger['#ajax']['event'] == 'entity_browser_value_updated') {
416 $parents = array_slice($trigger['#array_parents'], 0, -1);
418 elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], '_remove_')) {
419 $parents = array_slice($trigger['#array_parents'], 0, -static::$deleteDepth);
422 return NestedArray::getValue($form, $parents);
428 public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, FormStateInterface $form_state) {
429 if (($trigger = $form_state->getTriggeringElement())) {
430 // Can be triggered by "Remove" button.
431 if (end($trigger['#parents']) === 'remove_button') {
435 return parent::errorElement($element, $violation, $form, $form_state);
439 * Submit callback for remove buttons.
441 public static function removeItemSubmit(&$form, FormStateInterface $form_state) {
442 $triggering_element = $form_state->getTriggeringElement();
443 if (!empty($triggering_element['#attributes']['data-entity-id']) && isset($triggering_element['#attributes']['data-row-id'])) {
444 $id = $triggering_element['#attributes']['data-entity-id'];
445 $row_id = $triggering_element['#attributes']['data-row-id'];
446 $parents = array_slice($triggering_element['#parents'], 0, -static::$deleteDepth);
447 $array_parents = array_slice($triggering_element['#array_parents'], 0, -static::$deleteDepth);
449 // Find and remove correct entity.
450 $values = explode(' ', $form_state->getValue(array_merge($parents, ['target_id'])));
451 foreach ($values as $index => $item) {
452 if ($item == $id && $index == $row_id) {
453 array_splice($values, $index, 1);
458 $target_id_value = implode(' ', $values);
460 // Set new value for this widget.
461 $target_id_element = &NestedArray::getValue($form, array_merge($array_parents, ['target_id']));
462 $form_state->setValueForElement($target_id_element, $target_id_value);
463 NestedArray::setValue($form_state->getUserInput(), $target_id_element['#parents'], $target_id_value);
466 $form_state->setRebuild();
471 * Builds the render array for displaying the current results.
473 * @param string $details_id
474 * The ID for the details element.
475 * @param string[] $field_parents
477 * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
478 * Array of referenced entities.
481 * The render array for the current selection.
483 protected function displayCurrentSelection($details_id, $field_parents, $entities) {
485 $field_widget_display = $this->fieldDisplayManager->createInstance(
486 $this->getSetting('field_widget_display'),
487 $this->getSetting('field_widget_display_settings') + ['entity_type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')]
491 '#theme_wrappers' => ['container'],
492 '#attributes' => ['class' => ['entities-list']],
493 'items' => array_map(
494 function (ContentEntityInterface $entity, $row_id) use ($field_widget_display, $details_id, $field_parents) {
495 $display = $field_widget_display->view($entity);
496 $edit_button_access = $this->getSetting('field_widget_edit');
497 if ($entity->getEntityTypeId() == 'file') {
498 // On file entities, the "edit" button shouldn't be visible unless
499 // the module "file_entity" is present, which will allow them to be
500 // edited on their own form.
501 $edit_button_access &= $this->moduleHandler->moduleExists('file_entity');
503 if (is_string($display)) {
504 $display = ['#markup' => $display];
507 '#theme_wrappers' => ['container'],
509 'class' => ['item-container', Html::getClass($field_widget_display->getPluginId())],
510 'data-entity-id' => $entity->getEntityTypeId() . ':' . $entity->id(),
511 'data-row-id' => $row_id,
513 'display' => $display,
516 '#value' => $this->t('Remove'),
518 'callback' => [get_class($this), 'updateWidgetCallback'],
519 'wrapper' => $details_id,
521 '#submit' => [[get_class($this), 'removeItemSubmit']],
522 '#name' => $this->fieldDefinition->getName() . '_remove_' . $entity->id() . '_' . $row_id . '_' . md5(json_encode($field_parents)),
523 '#limit_validation_errors' => [array_merge($field_parents, [$this->fieldDefinition->getName()])],
525 'data-entity-id' => $entity->getEntityTypeId() . ':' . $entity->id(),
526 'data-row-id' => $row_id,
528 '#access' => (bool) $this->getSetting('field_widget_remove'),
532 '#value' => $this->t('Edit'),
534 'url' => Url::fromRoute(
535 'entity_browser.edit_form', [
536 'entity_type' => $entity->getEntityTypeId(),
537 'entity' => $entity->id(),
542 'details_id' => $details_id,
546 '#access' => $edit_button_access,
551 empty($entities) ? [] : range(0, count($entities) - 1)
557 * Gets data that should persist across Entity Browser renders.
560 * Data that should persist after the Entity Browser is rendered.
562 protected function getPersistentData() {
565 'entity_type' => ['type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')],
567 'widget_context' => [],
572 * Gets options that define where newly added entities are inserted.
575 * Mode labels indexed by key.
577 protected function selectionModeOptions() {
578 return ['append' => $this->t('Append'), 'prepend' => $this->t('Prepend')];
582 * Provides base for settings summary shared by all EB widgets.
585 * A short summary of the widget settings.
587 protected function summaryBase() {
590 $entity_browser_id = $this->getSetting('entity_browser');
591 if (empty($entity_browser_id)) {
592 return [$this->t('No entity browser selected.')];
595 if ($browser = $this->entityTypeManager->getStorage('entity_browser')->load($entity_browser_id)) {
596 $summary[] = $this->t('Entity browser: @browser', ['@browser' => $browser->label()]);
599 drupal_set_message($this->t('Missing entity browser!'), 'error');
600 return [$this->t('Missing entity browser!')];
604 $selection_mode = $this->getSetting('selection_mode');
605 $selection_mode_options = EntityBrowserElement::getSelectionModeOptions();
606 if (isset($selection_mode_options[$selection_mode])) {
607 $summary[] = $this->t('Selection mode: @selection_mode', ['@selection_mode' => $selection_mode_options[$selection_mode]]);
610 $summary[] = $this->t('Undefined selection mode.');
617 * Determines the entities used for the form element.
619 * @param \Drupal\Core\Field\FieldItemListInterface $items
620 * The field item to extract the entities from.
621 * @param array $element
623 * @param \Drupal\Core\Form\FormStateInterface $form_state
626 * @return \Drupal\Core\Entity\EntityInterface[]
627 * The list of entities for the form element.
629 protected function formElementEntities(FieldItemListInterface $items, array $element, FormStateInterface $form_state) {
631 $entity_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type');
632 $entity_storage = $this->entityTypeManager->getStorage($entity_type);
634 // Find IDs from target_id element (it stores selected entities in form).
635 // This was added to help solve a really edge casey bug in IEF.
636 if (($target_id_entities = $this->getEntitiesByTargetId($element, $form_state)) !== FALSE) {
637 return $target_id_entities;
640 // Determine if we're submitting and if submit came from this widget.
641 $is_relevant_submit = FALSE;
642 if (($trigger = $form_state->getTriggeringElement())) {
643 // Can be triggered by hidden target_id element or "Remove" button.
644 if (end($trigger['#parents']) === 'target_id' || (end($trigger['#parents']) === 'remove_button')) {
645 $is_relevant_submit = TRUE;
647 // In case there are more instances of this widget on the same page we
648 // need to check if submit came from this instance.
649 $field_name_key = end($trigger['#parents']) === 'target_id' ? 2 : static::$deleteDepth + 1;
650 $field_name_key = count($trigger['#parents']) - $field_name_key;
651 $is_relevant_submit &= ($trigger['#parents'][$field_name_key] === $this->fieldDefinition->getName()) &&
652 (array_slice($trigger['#parents'], 0, count($element['#field_parents'])) == $element['#field_parents']);
656 if ($is_relevant_submit) {
657 // Submit was triggered by hidden "target_id" element when entities were
658 // added via entity browser.
659 if (!empty($trigger['#ajax']['event']) && $trigger['#ajax']['event'] == 'entity_browser_value_updated') {
660 $parents = $trigger['#parents'];
662 // Submit was triggered by one of the "Remove" buttons. We need to walk
663 // few levels up to read value of "target_id" element.
664 elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], $this->fieldDefinition->getName() . '_remove_') === 0) {
665 $parents = array_merge(array_slice($trigger['#parents'], 0, -static::$deleteDepth), ['target_id']);
668 if (isset($parents) && $value = $form_state->getValue($parents)) {
669 $entities = EntityBrowserElement::processEntityIds($value);
674 // IDs from a previous request might be saved in the form state.
675 elseif ($form_state->has([
676 'entity_browser_widget',
677 $this->getFormStateKey($items),
680 $stored_ids = $form_state->get([
681 'entity_browser_widget',
682 $this->getFormStateKey($items),
684 $indexed_entities = $entity_storage->loadMultiple($stored_ids);
686 // Selection can contain same entity multiple times. Since loadMultiple()
687 // returns unique list of entities, it's necessary to recreate list of
688 // entities in order to preserve selection of duplicated entities.
689 foreach ($stored_ids as $entity_id) {
690 if (isset($indexed_entities[$entity_id])) {
691 $entities[] = $indexed_entities[$entity_id];
696 // We are loading for for the first time so we need to load any existing
697 // values that might already exist on the entity. Also, remove any leftover
698 // data from removed entity references.
700 foreach ($items as $item) {
701 if (isset($item->target_id)) {
702 $entity = $entity_storage->load($item->target_id);
703 if (!empty($entity)) {
704 $entities[] = $entity;
715 public function calculateDependencies() {
716 $dependencies = parent::calculateDependencies();
718 // If an entity browser is being used in this widget, add it as a config
720 if ($browser_name = $this->getSetting('entity_browser')) {
721 $dependencies['config'][] = 'entity_browser.browser.' . $browser_name;
724 return $dependencies;
728 * Get selected elements from target_id element on form.
730 * @param array $element
732 * @param \Drupal\Core\Form\FormStateInterface $form_state
735 * @return \Drupal\Core\Entity\EntityInterface[]|false
736 * Return list of entities if they are available or false.
738 protected function getEntitiesByTargetId(array $element, FormStateInterface $form_state) {
739 $target_id_element_path = array_merge(
740 $element['#field_parents'],
741 [$this->fieldDefinition->getName(), 'target_id']
744 if (!NestedArray::keyExists($form_state->getUserInput(), $target_id_element_path)) {
748 // TODO Figure out how to avoid using raw user input.
749 $current_user_input = NestedArray::getValue($form_state->getUserInput(), $target_id_element_path);
750 if (!is_array($current_user_input)) {
751 $entities = EntityBrowserElement::processEntityIds($current_user_input);