selectionModeOptions()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Drupal\entity_browser\Plugin\Field\FieldWidget;
4
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;
17
use Drupal\Core\Url;
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;
25
use Drupal\Core\Session\AccountInterface;
26
27
/**
28
 * Plugin implementation of the 'entity_reference' widget for entity browser.
29
 *
30
 * @FieldWidget(
31
 *   id = "entity_browser_entity_reference",
32
 *   label = @Translation("Entity browser"),
33
 *   description = @Translation("Uses entity browser to select entities."),
34
 *   multiple_values = TRUE,
35
 *   field_types = {
36
 *     "entity_reference"
37
 *   }
38
 * )
39
 */
40
class EntityReferenceBrowserWidget extends WidgetBase implements ContainerFactoryPluginInterface {
41
42
  /**
43
   * Entity type manager service.
44
   *
45
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
46
   */
47
  protected $entityTypeManager;
48
49
  /**
50
   * Field widget display plugin manager.
51
   *
52
   * @var \Drupal\entity_browser\FieldWidgetDisplayManager
53
   */
54
  protected $fieldDisplayManager;
55
56
  /**
57
   * The depth of the delete button.
58
   *
59
   * This property exists so it can be changed if subclasses.
60
   *
61
   * @var int
62
   */
63
  protected static $deleteDepth = 4;
64
65
  /**
66
   * The module handler interface.
67
   *
68
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
69
   */
70
  protected $moduleHandler;
71
72
  /**
73
   * The current user.
74
   *
75
   * @var \Drupal\Core\Session\AccountInterface
76
   */
77
  protected $currentUser;
78
79
  /**
80
   * Constructs widget plugin.
81
   *
82
   * @param string $plugin_id
83
   *   The plugin_id for the plugin instance.
84
   * @param mixed $plugin_definition
85
   *   The plugin implementation definition.
86
   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
87
   *   The definition of the field to which the widget is associated.
88
   * @param array $settings
89
   *   The widget settings.
90
   * @param array $third_party_settings
91
   *   Any third party settings.
92
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
93
   *   Entity type manager service.
94
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
95
   *   Event dispatcher.
96
   * @param \Drupal\entity_browser\FieldWidgetDisplayManager $field_display_manager
97
   *   Field widget display plugin manager.
98
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
99
   *   The module handler service.
100
   * @param \Drupal\Core\Session\AccountInterface $current_user
101
   *   The current user.
102
   */
103
  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, AccountInterface $current_user) {
104
    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
105
    $this->entityTypeManager = $entity_type_manager;
106
    $this->fieldDisplayManager = $field_display_manager;
107
    $this->moduleHandler = $module_handler;
108
    $this->currentUser = $current_user;
109
  }
110
111
  /**
112
   * {@inheritdoc}
113
   */
114
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
115
    return new static(
116
      $plugin_id,
117
      $plugin_definition,
118
      $configuration['field_definition'],
119
      $configuration['settings'],
120
      $configuration['third_party_settings'],
121
      $container->get('entity_type.manager'),
122
      $container->get('event_dispatcher'),
123
      $container->get('plugin.manager.entity_browser.field_widget_display'),
124
      $container->get('module_handler'),
125
      $container->get('current_user')
126
    );
127
  }
128
129
  /**
130
   * {@inheritdoc}
131
   */
132
  public static function defaultSettings() {
133
    return [
134
      'entity_browser' => NULL,
135
      'open' => FALSE,
136
      'field_widget_display' => 'label',
137
      'field_widget_edit' => TRUE,
138
      'field_widget_remove' => TRUE,
139
      'field_widget_display_settings' => [],
140
      'selection_mode' => EntityBrowserElement::SELECTION_MODE_APPEND,
141
    ] + parent::defaultSettings();
142
  }
143
144
  /**
145
   * {@inheritdoc}
146
   */
147
  public function settingsForm(array $form, FormStateInterface $form_state) {
148
    $element = parent::settingsForm($form, $form_state);
149
150
    $browsers = [];
151
    /** @var \Drupal\entity_browser\EntityBrowserInterface $browser */
152
    foreach ($this->entityTypeManager->getStorage('entity_browser')->loadMultiple() as $browser) {
153
      $browsers[$browser->id()] = $browser->label();
154
    }
155
156
    $element['entity_browser'] = [
157
      '#title' => $this->t('Entity browser'),
158
      '#type' => 'select',
159
      '#default_value' => $this->getSetting('entity_browser'),
160
      '#options' => $browsers,
161
    ];
162
163
    $target_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type');
164
    $entity_type = $this->entityTypeManager->getStorage($target_type)->getEntityType();
165
166
    $displays = [];
167
    foreach ($this->fieldDisplayManager->getDefinitions() as $id => $definition) {
168
      if ($this->fieldDisplayManager->createInstance($id)->isApplicable($entity_type)) {
169
        $displays[$id] = $definition['label'];
170
      }
171
    }
172
173
    $id = Html::getUniqueId('field-' . $this->fieldDefinition->getName() . '-display-settings-wrapper');
174
    $element['field_widget_display'] = [
175
      '#title' => $this->t('Entity display plugin'),
176
      '#type' => 'select',
177
      '#default_value' => $this->getSetting('field_widget_display'),
178
      '#options' => $displays,
179
      '#ajax' => [
180
        'callback' => [$this, 'updateSettingsAjax'],
181
        'wrapper' => $id,
182
      ],
183
    ];
184
185
    $edit_button_access = TRUE;
186
    if ($entity_type->id() == 'file') {
187
      // For entities of type "file", it only makes sense to have the edit
188
      // button if the module "file_entity" is present.
189
      $edit_button_access = $this->moduleHandler->moduleExists('file_entity');
190
    }
191
    $element['field_widget_edit'] = [
192
      '#title' => $this->t('Display Edit button'),
193
      '#type' => 'checkbox',
194
      '#default_value' => $this->getSetting('field_widget_edit'),
195
      '#access' => $edit_button_access,
196
    ];
197
198
    $element['field_widget_remove'] = [
199
      '#title' => $this->t('Display Remove button'),
200
      '#type' => 'checkbox',
201
      '#default_value' => $this->getSetting('field_widget_remove'),
202
    ];
203
204
    $element['open'] = [
205
      '#title' => $this->t('Show widget details as open by default'),
206
      '#description' => $this->t('If marked, the fieldset container that wraps the browser on the entity form will be loaded initially expanded.'),
207
      '#type' => 'checkbox',
208
      '#default_value' => $this->getSetting('open'),
209
    ];
210
211
    $element['selection_mode'] = [
212
      '#title' => $this->t('Selection mode'),
213
      '#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.'),
214
      '#type' => 'select',
215
      '#options' => EntityBrowserElement::getSelectionModeOptions(),
216
      '#default_value' => $this->getSetting('selection_mode'),
217
    ];
218
219
    $element['field_widget_display_settings'] = [
220
      '#type' => 'fieldset',
221
      '#title' => $this->t('Entity display plugin configuration'),
222
      '#tree' => TRUE,
223
      '#prefix' => '<div id="' . $id . '">',
224
      '#suffix' => '</div>',
225
    ];
226
227
    if ($this->getSetting('field_widget_display')) {
228
      $element['field_widget_display_settings'] += $this->fieldDisplayManager
229
        ->createInstance(
230
          $form_state->getValue(
231
            ['fields', $this->fieldDefinition->getName(), 'settings_edit_form', 'settings', 'field_widget_display'],
232
            $this->getSetting('field_widget_display')
233
          ),
234
          $form_state->getValue(
235
            ['fields', $this->fieldDefinition->getName(), 'settings_edit_form', 'settings', 'field_widget_display_settings'],
236
            $this->getSetting('field_widget_display_settings')
237
          ) + ['entity_type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')]
238
        )
239
        ->settingsForm($form, $form_state);
240
    }
241
242
    return $element;
243
  }
244
245
  /**
246
   * Ajax callback that updates field widget display settings fieldset.
247
   */
248
  public function updateSettingsAjax(array $form, FormStateInterface $form_state) {
249
    return $form['fields'][$this->fieldDefinition->getName()]['plugin']['settings_edit_form']['settings']['field_widget_display_settings'];
250
  }
251
252
  /**
253
   * {@inheritdoc}
254
   */
255
  public function settingsSummary() {
256
    $summary = $this->summaryBase();
257
    $field_widget_display = $this->getSetting('field_widget_display');
258
259
    if (!empty($field_widget_display)) {
260
      $plugin = $this->fieldDisplayManager->getDefinition($field_widget_display);
261
      $summary[] = $this->t('Entity display: @name', ['@name' => $plugin['label']]);
262
    }
263
    return $summary;
264
  }
265
266
  /**
267
   * {@inheritdoc}
268
   */
269
  public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
270
    if ($violations->count() > 0) {
271
      /** @var \Symfony\Component\Validator\ConstraintViolation $violation */
272
      foreach ($violations as $offset => $violation) {
273
        // The value of the required field is checked through the "not null"
274
        // constraint, whose message is not very useful. We override it here for
275
        // better UX.
276
        if ($violation->getConstraint() instanceof NotNullConstraint) {
0 ignored issues
show
Bug introduced by
The class Drupal\Core\Validation\P...raint\NotNullConstraint does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
277
          $violations->set($offset, new ConstraintViolation(
278
            $this->t('@name field is required.', ['@name' => $items->getFieldDefinition()->getLabel()]),
279
            '',
280
            [],
281
            $violation->getRoot(),
282
            $violation->getPropertyPath(),
283
            $violation->getInvalidValue(),
284
            $violation->getPlural(),
285
            $violation->getCode(),
286
            $violation->getConstraint(),
287
            $violation->getCause()
288
          ));
289
        }
290
      }
291
    }
292
293
    parent::flagErrors($items, $violations, $form, $form_state);
294
  }
295
296
  /**
297
   * Returns a key used to store the previously loaded entity.
298
   *
299
   * @param \Drupal\Core\Field\FieldItemListInterface $items
300
   *   The field items.
301
   *
302
   * @return string
303
   *   A key for form state storage.
304
   */
305
  protected function getFormStateKey(FieldItemListInterface $items) {
306
    return $items->getEntity()->uuid() . ':' . $items->getFieldDefinition()->getName();
307
  }
308
309
  /**
310
   * {@inheritdoc}
311
   */
312
  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
313
    $entity_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type');
0 ignored issues
show
Unused Code introduced by
$entity_type is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
314
    $entities = $this->formElementEntities($items, $element, $form_state);
315
316
    // Get correct ordered list of entity IDs.
317
    $ids = array_map(
318
      function (EntityInterface $entity) {
319
        return $entity->id();
320
      },
321
      $entities
322
    );
323
324
    // We store current entity IDs as we might need them in future requests. If
325
    // some other part of the form triggers an AJAX request with
326
    // #limit_validation_errors we won't have access to the value of the
327
    // target_id element and won't be able to build the form as a result of
328
    // that. This will cause missing submit (Remove, Edit, ...) elements, which
329
    // might result in unpredictable results.
330
    $form_state->set(['entity_browser_widget', $this->getFormStateKey($items)], $ids);
331
332
    $hidden_id = Html::getUniqueId('edit-' . $this->fieldDefinition->getName() . '-target-id');
333
    $details_id = Html::getUniqueId('edit-' . $this->fieldDefinition->getName());
334
335
    $element += [
336
      '#id' => $details_id,
337
      '#type' => 'details',
338
      '#open' => !empty($entities) || $this->getSetting('open'),
339
      '#required' => $this->fieldDefinition->isRequired(),
340
      // We are not using Entity browser's hidden element since we maintain
341
      // selected entities in it during entire process.
342
      'target_id' => [
343
        '#type' => 'hidden',
344
        '#id' => $hidden_id,
345
        // We need to repeat ID here as it is otherwise skipped when rendering.
346
        '#attributes' => ['id' => $hidden_id],
347
        '#default_value' => implode(' ', array_map(
348
            function (EntityInterface $item) {
349
              return $item->getEntityTypeId() . ':' . $item->id();
350
            },
351
            $entities
352
        )),
353
        // #ajax is officially not supported for hidden elements but if we
354
        // specify event manually it works.
355
        '#ajax' => [
356
          'callback' => [get_class($this), 'updateWidgetCallback'],
357
          'wrapper' => $details_id,
358
          'event' => 'entity_browser_value_updated',
359
        ],
360
      ],
361
    ];
362
363
    // Get configuration required to check entity browser availability.
364
    $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
365
    $selection_mode = $this->getSetting('selection_mode');
366
367
    // Enable entity browser if requirements for that are fulfilled.
368
    if (EntityBrowserElement::isEntityBrowserAvailable($selection_mode, $cardinality, count($ids))) {
369
      $persistentData = $this->getPersistentData();
370
371
      $element['entity_browser'] = [
372
        '#type' => 'entity_browser',
373
        '#entity_browser' => $this->getSetting('entity_browser'),
374
        '#cardinality' => $cardinality,
375
        '#selection_mode' => $selection_mode,
376
        '#default_value' => $entities,
377
        '#entity_browser_validators' => $persistentData['validators'],
378
        '#widget_context' => $persistentData['widget_context'],
379
        '#custom_hidden_id' => $hidden_id,
380
        '#process' => [
381
          ['\Drupal\entity_browser\Element\EntityBrowserElement', 'processEntityBrowser'],
382
          [get_called_class(), 'processEntityBrowser'],
383
        ],
384
      ];
385
    }
386
387
    $element['#attached']['library'][] = 'entity_browser/entity_reference';
388
389
    $field_parents = $element['#field_parents'];
390
391
    $element['current'] = $this->displayCurrentSelection($details_id, $field_parents, $entities);
392
393
    return $element;
394
  }
395
396
  /**
397
   * Render API callback: Processes the entity browser element.
398
   */
399
  public static function processEntityBrowser(&$element, FormStateInterface $form_state, &$complete_form) {
400
    $uuid = key($element['#attached']['drupalSettings']['entity_browser']);
401
    $element['#attached']['drupalSettings']['entity_browser'][$uuid]['selector'] = '#' . $element['#custom_hidden_id'];
402
    return $element;
403
  }
404
405
  /**
406
   * {@inheritdoc}
407
   */
408
  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
409
    $entities = empty($values['target_id']) ? [] : explode(' ', trim($values['target_id']));
410
    $return = [];
411
    foreach ($entities as $entity) {
412
      $return[]['target_id'] = explode(':', $entity)[1];
413
    }
414
415
    return $return;
416
  }
417
418
  /**
419
   * AJAX form callback.
420
   */
421
  public static function updateWidgetCallback(array &$form, FormStateInterface $form_state) {
422
    $trigger = $form_state->getTriggeringElement();
423
    // AJAX requests can be triggered by hidden "target_id" element when
424
    // entities are added or by one of the "Remove" buttons. Depending on that
425
    // we need to figure out where root of the widget is in the form structure
426
    // and use this information to return correct part of the form.
427 View Code Duplication
    if (!empty($trigger['#ajax']['event']) && $trigger['#ajax']['event'] == 'entity_browser_value_updated') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
428
      $parents = array_slice($trigger['#array_parents'], 0, -1);
429
    }
430
    elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], '_remove_')) {
431
      $parents = array_slice($trigger['#array_parents'], 0, -static::$deleteDepth);
432
    }
433
434
    return NestedArray::getValue($form, $parents);
0 ignored issues
show
Bug introduced by
The variable $parents does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
435
  }
436
437
  /**
438
   * {@inheritdoc}
439
   */
440
  public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, FormStateInterface $form_state) {
441
    if (($trigger = $form_state->getTriggeringElement())) {
442
      // Can be triggered by "Remove" button.
443
      if (end($trigger['#parents']) === 'remove_button') {
444
        return FALSE;
445
      }
446
    }
447
    return parent::errorElement($element, $violation, $form, $form_state);
448
  }
449
450
  /**
451
   * Submit callback for remove buttons.
452
   */
453
  public static function removeItemSubmit(&$form, FormStateInterface $form_state) {
454
    $triggering_element = $form_state->getTriggeringElement();
455
    if (!empty($triggering_element['#attributes']['data-entity-id']) && isset($triggering_element['#attributes']['data-row-id'])) {
456
      $id = $triggering_element['#attributes']['data-entity-id'];
457
      $row_id = $triggering_element['#attributes']['data-row-id'];
458
      $parents = array_slice($triggering_element['#parents'], 0, -static::$deleteDepth);
459
      $array_parents = array_slice($triggering_element['#array_parents'], 0, -static::$deleteDepth);
460
461
      // Find and remove correct entity.
462
      $values = explode(' ', $form_state->getValue(array_merge($parents, ['target_id'])));
463
      foreach ($values as $index => $item) {
464
        if ($item == $id && $index == $row_id) {
465
          array_splice($values, $index, 1);
466
467
          break;
468
        }
469
      }
470
      $target_id_value = implode(' ', $values);
471
472
      // Set new value for this widget.
473
      $target_id_element = &NestedArray::getValue($form, array_merge($array_parents, ['target_id']));
474
      $form_state->setValueForElement($target_id_element, $target_id_value);
475
      NestedArray::setValue($form_state->getUserInput(), $target_id_element['#parents'], $target_id_value);
476
477
      // Rebuild form.
478
      $form_state->setRebuild();
479
    }
480
  }
481
482
  /**
483
   * Builds the render array for displaying the current results.
484
   *
485
   * @param string $details_id
486
   *   The ID for the details element.
487
   * @param string[] $field_parents
488
   *   Field parents.
489
   * @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
490
   *   Array of referenced entities.
491
   *
492
   * @return array
493
   *   The render array for the current selection.
494
   */
495
  protected function displayCurrentSelection($details_id, $field_parents, $entities) {
496
497
    $field_widget_display = $this->fieldDisplayManager->createInstance(
498
      $this->getSetting('field_widget_display'),
499
      $this->getSetting('field_widget_display_settings') + ['entity_type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')]
500
    );
501
502
    $classes = ['entities-list'];
503
    if ($this->fieldDefinition->getFieldStorageDefinition()->getCardinality() != 1) {
504
      $classes[] = 'sortable';
505
    }
506
507
    return [
508
      '#theme_wrappers' => ['container'],
509
      '#attributes' => ['class' => $classes],
510
      'items' => array_map(
511
        function (ContentEntityInterface $entity, $row_id) use ($field_widget_display, $details_id, $field_parents) {
512
          $display = $field_widget_display->view($entity);
513
          $edit_button_access = $this->getSetting('field_widget_edit') && $entity->access('update', $this->currentUser);
514
          if ($entity->getEntityTypeId() == 'file') {
515
            // On file entities, the "edit" button shouldn't be visible unless
516
            // the module "file_entity" is present, which will allow them to be
517
            // edited on their own form.
518
            $edit_button_access &= $this->moduleHandler->moduleExists('file_entity');
519
          }
520
          if (is_string($display)) {
521
            $display = ['#markup' => $display];
522
          }
523
          return [
524
            '#theme_wrappers' => ['container'],
525
            '#attributes' => [
526
              'class' => ['item-container', Html::getClass($field_widget_display->getPluginId())],
527
              'data-entity-id' => $entity->getEntityTypeId() . ':' . $entity->id(),
528
              'data-row-id' => $row_id,
529
            ],
530
            'display' => $display,
531
            'remove_button' => [
532
              '#type' => 'submit',
533
              '#value' => $this->t('Remove'),
534
              '#ajax' => [
535
                'callback' => [get_class($this), 'updateWidgetCallback'],
536
                'wrapper' => $details_id,
537
              ],
538
              '#submit' => [[get_class($this), 'removeItemSubmit']],
539
              '#name' => $this->fieldDefinition->getName() . '_remove_' . $entity->id() . '_' . $row_id . '_' . md5(json_encode($field_parents)),
540
              '#limit_validation_errors' => [array_merge($field_parents, [$this->fieldDefinition->getName()])],
541
              '#attributes' => [
542
                'data-entity-id' => $entity->getEntityTypeId() . ':' . $entity->id(),
543
                'data-row-id' => $row_id,
544
              ],
545
              '#access' => (bool) $this->getSetting('field_widget_remove'),
546
            ],
547
            'edit_button' => [
548
              '#type' => 'submit',
549
              '#value' => $this->t('Edit'),
550
              '#ajax' => [
551
                'url' => Url::fromRoute(
552
                  'entity_browser.edit_form', [
553
                    'entity_type' => $entity->getEntityTypeId(),
554
                    'entity' => $entity->id(),
555
                  ]
556
                ),
557
                'options' => [
558
                  'query' => [
559
                    'details_id' => $details_id,
560
                  ],
561
                ],
562
              ],
563
              '#access' => $edit_button_access,
564
            ],
565
          ];
566
        },
567
        $entities,
568
        empty($entities) ? [] : range(0, count($entities) - 1)
569
      ),
570
    ];
571
  }
572
573
  /**
574
   * Gets data that should persist across Entity Browser renders.
575
   *
576
   * @return array
577
   *   Data that should persist after the Entity Browser is rendered.
578
   */
579
  protected function getPersistentData() {
580
    return [
581
      'validators' => [
582
        'entity_type' => ['type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type')],
583
      ],
584
      'widget_context' => [],
585
    ];
586
  }
587
588
  /**
589
   * Gets options that define where newly added entities are inserted.
590
   *
591
   * @return array
592
   *   Mode labels indexed by key.
593
   */
594
  protected function selectionModeOptions() {
595
    return ['append' => $this->t('Append'), 'prepend' => $this->t('Prepend')];
596
  }
597
598
  /**
599
   * Provides base for settings summary shared by all EB widgets.
600
   *
601
   * @return array
602
   *   A short summary of the widget settings.
603
   */
604
  protected function summaryBase() {
605
    $summary = [];
606
607
    $entity_browser_id = $this->getSetting('entity_browser');
608
    if (empty($entity_browser_id)) {
609
      return [$this->t('No entity browser selected.')];
610
    }
611
    else {
612
      if ($browser = $this->entityTypeManager->getStorage('entity_browser')->load($entity_browser_id)) {
613
        $summary[] = $this->t('Entity browser: @browser', ['@browser' => $browser->label()]);
614
      }
615
      else {
616
        drupal_set_message($this->t('Missing entity browser!'), 'error');
617
        return [$this->t('Missing entity browser!')];
618
      }
619
    }
620
621
    $selection_mode = $this->getSetting('selection_mode');
622
    $selection_mode_options = EntityBrowserElement::getSelectionModeOptions();
623
    if (isset($selection_mode_options[$selection_mode])) {
624
      $summary[] = $this->t('Selection mode: @selection_mode', ['@selection_mode' => $selection_mode_options[$selection_mode]]);
625
    }
626
    else {
627
      $summary[] = $this->t('Undefined selection mode.');
628
    }
629
630
    return $summary;
631
  }
632
633
  /**
634
   * Determines the entities used for the form element.
635
   *
636
   * @param \Drupal\Core\Field\FieldItemListInterface $items
637
   *   The field item to extract the entities from.
638
   * @param array $element
639
   *   The form element.
640
   * @param \Drupal\Core\Form\FormStateInterface $form_state
641
   *   The form state.
642
   *
643
   * @return \Drupal\Core\Entity\EntityInterface[]
644
   *   The list of entities for the form element.
645
   */
646
  protected function formElementEntities(FieldItemListInterface $items, array $element, FormStateInterface $form_state) {
647
    $entities = [];
648
    $entity_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type');
649
    $entity_storage = $this->entityTypeManager->getStorage($entity_type);
650
651
    // Find IDs from target_id element (it stores selected entities in form).
652
    // This was added to help solve a really edge casey bug in IEF.
653
    if (($target_id_entities = $this->getEntitiesByTargetId($element, $form_state)) !== FALSE) {
654
      return $target_id_entities;
655
    }
656
657
    // Determine if we're submitting and if submit came from this widget.
658
    $is_relevant_submit = FALSE;
659
    if (($trigger = $form_state->getTriggeringElement())) {
660
      // Can be triggered by hidden target_id element or "Remove" button.
661
      if (end($trigger['#parents']) === 'target_id' || (end($trigger['#parents']) === 'remove_button')) {
662
        $is_relevant_submit = TRUE;
663
664
        // In case there are more instances of this widget on the same page we
665
        // need to check if submit came from this instance.
666
        $field_name_key = end($trigger['#parents']) === 'target_id' ? 2 : static::$deleteDepth + 1;
667
        $field_name_key = count($trigger['#parents']) - $field_name_key;
668
        $is_relevant_submit &= ($trigger['#parents'][$field_name_key] === $this->fieldDefinition->getName()) &&
669
          (array_slice($trigger['#parents'], 0, count($element['#field_parents'])) == $element['#field_parents']);
670
      }
671
    };
672
673
    if ($is_relevant_submit) {
674
      // Submit was triggered by hidden "target_id" element when entities were
675
      // added via entity browser.
676 View Code Duplication
      if (!empty($trigger['#ajax']['event']) && $trigger['#ajax']['event'] == 'entity_browser_value_updated') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
677
        $parents = $trigger['#parents'];
678
      }
679
      // Submit was triggered by one of the "Remove" buttons. We need to walk
680
      // few levels up to read value of "target_id" element.
681
      elseif ($trigger['#type'] == 'submit' && strpos($trigger['#name'], $this->fieldDefinition->getName() . '_remove_') === 0) {
682
        $parents = array_merge(array_slice($trigger['#parents'], 0, -static::$deleteDepth), ['target_id']);
683
      }
684
685
      if (isset($parents) && $value = $form_state->getValue($parents)) {
686
        $entities = EntityBrowserElement::processEntityIds($value);
687
        return $entities;
688
      }
689
      return $entities;
690
    }
691
    // IDs from a previous request might be saved in the form state.
692
    elseif ($form_state->has([
693
      'entity_browser_widget',
694
      $this->getFormStateKey($items),
695
    ])
696
    ) {
697
      $stored_ids = $form_state->get([
698
        'entity_browser_widget',
699
        $this->getFormStateKey($items),
700
      ]);
701
      $indexed_entities = $entity_storage->loadMultiple($stored_ids);
702
703
      // Selection can contain same entity multiple times. Since loadMultiple()
704
      // returns unique list of entities, it's necessary to recreate list of
705
      // entities in order to preserve selection of duplicated entities.
706
      foreach ($stored_ids as $entity_id) {
707
        if (isset($indexed_entities[$entity_id])) {
708
          $entities[] = $indexed_entities[$entity_id];
709
        }
710
      }
711
      return $entities;
712
    }
713
    // We are loading for for the first time so we need to load any existing
714
    // values that might already exist on the entity. Also, remove any leftover
715
    // data from removed entity references.
716
    else {
717
      foreach ($items as $item) {
718
        if (isset($item->target_id)) {
719
          $entity = $entity_storage->load($item->target_id);
720
          if (!empty($entity)) {
721
            $entities[] = $entity;
722
          }
723
        }
724
      }
725
      return $entities;
726
    }
727
  }
728
729
  /**
730
   * {@inheritdoc}
731
   */
732
  public function calculateDependencies() {
733
    $dependencies = parent::calculateDependencies();
734
735
    // If an entity browser is being used in this widget, add it as a config
736
    // dependency.
737
    if ($browser_name = $this->getSetting('entity_browser')) {
738
      $dependencies['config'][] = 'entity_browser.browser.' . $browser_name;
739
    }
740
741
    return $dependencies;
742
  }
743
744
  /**
745
   * Get selected elements from target_id element on form.
746
   *
747
   * @param array $element
748
   *   The form element.
749
   * @param \Drupal\Core\Form\FormStateInterface $form_state
750
   *   The form state.
751
   *
752
   * @return \Drupal\Core\Entity\EntityInterface[]|false
753
   *   Return list of entities if they are available or false.
754
   */
755
  protected function getEntitiesByTargetId(array $element, FormStateInterface $form_state) {
756
    $target_id_element_path = array_merge(
757
      $element['#field_parents'],
758
      [$this->fieldDefinition->getName(), 'target_id']
759
    );
760
761
    if (!NestedArray::keyExists($form_state->getUserInput(), $target_id_element_path)) {
762
      return FALSE;
763
    }
764
765
    // TODO Figure out how to avoid using raw user input.
766
    $current_user_input = NestedArray::getValue($form_state->getUserInput(), $target_id_element_path);
767
    if (!is_array($current_user_input)) {
768
      $entities = EntityBrowserElement::processEntityIds($current_user_input);
769
      return $entities;
770
    }
771
772
    return FALSE;
773
  }
774
775
}
776