Completed
Pull Request — 8.x-1.x (#153)
by
unknown
02:46
created

MultiStepDisplay::handleAjaxCommand()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 57
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 26
nc 4
nop 2
dl 0
loc 57
rs 7.6759
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Drupal\entity_browser\Plugin\EntityBrowser\SelectionDisplay;
4
5
use Drupal\Core\Ajax\AfterCommand;
6
use Drupal\Core\Ajax\AjaxResponse;
7
use Drupal\Core\Ajax\InvokeCommand;
8
use Drupal\Core\Ajax\ReplaceCommand;
9
use Drupal\Core\Entity\EntityTypeManagerInterface;
10
use Drupal\Core\Form\FormStateInterface;
11
use Drupal\entity_browser\FieldWidgetDisplayManager;
12
use Drupal\entity_browser\SelectionDisplayBase;
13
use Symfony\Component\DependencyInjection\ContainerInterface;
14
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
15
16
/**
17
 * Show current selection and delivers selected entities.
18
 *
19
 * @EntityBrowserSelectionDisplay(
20
 *   id = "multi_step_display",
21
 *   label = @Translation("Multi step selection display"),
22
 *   description = @Translation("Shows the current selection display, allowing to mix elements selected through different widgets in several steps."),
23
 *   acceptPreselection = TRUE,
24
 *   jsCommands = TRUE
25
 * )
26
 */
27
class MultiStepDisplay extends SelectionDisplayBase {
28
29
  /**
30
   * Field widget display plugin manager.
31
   *
32
   * @var \Drupal\entity_browser\FieldWidgetDisplayManager
33
   */
34
  protected $fieldDisplayManager;
35
36
  /**
37
   * Constructs widget plugin.
38
   *
39
   * @param array $configuration
40
   *   A configuration array containing information about the plugin instance.
41
   * @param string $plugin_id
42
   *   The plugin_id for the plugin instance.
43
   * @param mixed $plugin_definition
44
   *   The plugin implementation definition.
45
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
46
   *   Event dispatcher service.
47
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
48
   *   The entity type manager service.
49
   * @param \Drupal\entity_browser\FieldWidgetDisplayManager $field_display_manager
50
   *   Field widget display plugin manager.
51
   */
52
  public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, EntityTypeManagerInterface $entity_type_manager, FieldWidgetDisplayManager $field_display_manager) {
53
    parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $entity_type_manager);
54
    $this->fieldDisplayManager = $field_display_manager;
55
  }
56
57
  /**
58
   * {@inheritdoc}
59
   */
60
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
61
    return new static(
62
      $configuration,
63
      $plugin_id,
64
      $plugin_definition,
65
      $container->get('event_dispatcher'),
66
      $container->get('entity_type.manager'),
67
      $container->get('plugin.manager.entity_browser.field_widget_display')
68
    );
69
  }
70
71
  /**
72
   * {@inheritdoc}
73
   */
74
  public function defaultConfiguration() {
75
    return [
76
      'entity_type' => 'node',
77
      'display' => 'label',
78
      'display_settings' => [],
79
      'select_text' => 'Use selected',
80
      'selection_hidden' => 0,
81
    ] + parent::defaultConfiguration();
82
  }
83
84
  /**
85
   * {@inheritdoc}
86
   */
87
  public function getForm(array &$original_form, FormStateInterface $form_state) {
88
89
    // Check if trigger element is dedicated to handle front-end commands.
90
    if (($triggering_element = $form_state->getTriggeringElement()) && $triggering_element['#name'] === 'ajax_commands_handler' && !empty($triggering_element['#value'])) {
91
      $this->executeJsCommand($form_state);
92
    }
93
94
    $selected_entities = $form_state->get(['entity_browser', 'selected_entities']);
95
96
    $form = [];
97
    $form['#attached']['library'][] = 'entity_browser/multi_step_display';
98
    $form['selected'] = [
99
      '#theme_wrappers' => ['container'],
100
      '#attributes' => ['class' => ['entities-list']],
101
      '#tree' => TRUE,
102
    ];
103
    if ($this->configuration['selection_hidden']) {
104
      $form['selected']['#attributes']['class'][] = 'hidden';
105
    }
106
    foreach ($selected_entities as $id => $entity) {
107
      $display_plugin = $this->fieldDisplayManager->createInstance(
108
        $this->configuration['display'],
109
        $this->configuration['display_settings'] + ['entity_type' => $this->configuration['entity_type']]
110
      );
111
      $display = $display_plugin->view($entity);
112
      if (is_string($display)) {
113
        $display = ['#markup' => $display];
114
      }
115
116
      $form['selected']['items_' . $entity->id() . '_' . $id] = [
117
        '#theme_wrappers' => ['container'],
118
        '#attributes' => [
119
          'class' => ['item-container'],
120
          'data-entity-id' => $entity->id(),
121
        ],
122
        'display' => $display,
123
        'remove_button' => [
124
          '#type' => 'submit',
125
          '#value' => $this->t('Remove'),
126
          '#submit' => [[get_class($this), 'removeItemSubmit']],
127
          '#name' => 'remove_' . $entity->id() . '_' . $id,
128
          '#attributes' => [
129
            'class' => ['entity-browser-remove-selected-entity'],
130
            'data-row-id' => $id,
131
            'data-remove-entity' => 'items_' . $entity->id(),
132
          ],
133
        ],
134
        'weight' => [
135
          '#type' => 'hidden',
136
          '#default_value' => $id,
137
          '#attributes' => ['class' => ['weight']],
138
        ],
139
      ];
140
    }
141
142
    // Add hidden element used to make execution of front-end commands.
143
    $form['ajax_commands_handler'] = [
144
      '#type' => 'hidden',
145
      '#name' => 'ajax_commands_handler',
146
      '#id' => 'ajax_commands_handler',
147
      '#attributes' => ['id' => 'ajax_commands_handler'],
148
      '#ajax' => [
149
        'callback' => [get_class($this), 'handleAjaxCommand'],
150
        'wrapper' => 'edit-selected',
151
        'event' => 'execute_js_commands',
152
        'progress' => [
153
          'type' => 'fullscreen',
154
        ],
155
      ],
156
    ];
157
158
    $form['use_selected'] = [
159
      '#type' => 'submit',
160
      '#value' => $this->t($this->configuration['select_text']),
161
      '#name' => 'use_selected',
162
      '#attributes' => [
163
        'class' => ['entity-browser-use-selected'],
164
      ],
165
      '#access' => empty($selected_entities) ? FALSE : TRUE,
166
    ];
167
168
    $form['show_selection'] = [
169
      '#type' => 'button',
170
      '#value' => $this->t('Show selected'),
171
      '#attributes' => [
172
        'class' => ['entity-browser-show-selection'],
173
      ],
174
      '#access' => empty($selected_entities) ? FALSE : TRUE,
175
    ];
176
177
    return $form;
178
  }
179
180
  /**
181
   * Execute command generated by front-end.
182
   *
183
   * @param \Drupal\Core\Form\FormStateInterface $form_state
184
   *   Form state object.
185
   */
186
  protected function executeJsCommand(FormStateInterface $form_state) {
187
    $triggering_element = $form_state->getTriggeringElement();
188
189
    $commands = json_decode($triggering_element['#value'], TRUE);
190
191
    // Process Remove command.
192
    if (isset($commands['remove'])) {
193
      $entity_ids = $commands['remove'];
194
195
      // Remove weight of entity being removed.
196
      foreach ($entity_ids as $entity_info) {
197
        $entity_id_info = explode('_', $entity_info['entity_id']);
198
199
        $form_state->unsetValue([
200
          'selected',
201
          $entity_info['entity_id'],
202
        ]);
203
204
        // Remove entity itself.
205
        $selected_entities = &$form_state->get(['entity_browser', 'selected_entities']);
206
        unset($selected_entities[$entity_id_info[2]]);
207
      }
208
209
      static::saveNewOrder($form_state);
210
    }
211
212
    // Process Add command.
213
    if (isset($commands['add'])) {
214
      $entity_ids = $commands['add'];
215
216
      $entities_to_add = [];
217
      $added_entities = [];
218
219
      // Generate list of entities grouped by type, to speed up loadMultiple.
220
      foreach ($entity_ids as $entity_pair_info) {
221
        $entity_info = explode(':', $entity_pair_info['entity_id']);
222
223
        if (!isset($entities_to_add[$entity_info[0]])) {
224
          $entities_to_add[$entity_info[0]] = [];
225
        }
226
227
        $entities_to_add[$entity_info[0]][] = $entity_info[1];
228
      }
229
230
      // Load Entities and add into $added_entities, so that we have list of
231
      // entities with key - "type:id".
232
      foreach ($entities_to_add as $entity_type => $entity_type_ids) {
233
        $indexed_entities = $this->entityTypeManager->getStorage($entity_type)
234
          ->loadMultiple($entity_type_ids);
235
236
        foreach ($indexed_entities as $entity_id => $entity) {
237
          $added_entities[implode(':', [
238
            $entity_type,
239
            $entity_id,
240
          ])] = $entity;
241
        }
242
      }
243
244
      // Array is accessed as reference, so that changes are propagated.
245
      $selected_entities = &$form_state->get([
246
        'entity_browser',
247
        'selected_entities',
248
      ]);
249
250
      // Fill list of selected entities in correct order with loaded entities.
251
      // In this case, order is preserved and multiple entities with same ID
252
      // can be selected properly.
253
      foreach ($entity_ids as $entity_pair_info) {
254
        $selected_entities[] = $added_entities[$entity_pair_info['entity_id']];
255
      }
256
    }
257
  }
258
259
  /**
260
   * Handler to generate Ajax response, after command is executed.
261
   *
262
   * @param array $form
263
   *   Form.
264
   * @param \Drupal\Core\Form\FormStateInterface $form_state
265
   *   Form state object.
266
   *
267
   * @return \Drupal\Core\Ajax\AjaxResponse
268
   *   Return Ajax response with commands.
269
   */
270
  public static function handleAjaxCommand(array $form, FormStateInterface $form_state) {
271
    $ajax = new AjaxResponse();
272
273
    if (($triggering_element = $form_state->getTriggeringElement()) && $triggering_element['#name'] === 'ajax_commands_handler' && !empty($triggering_element['#value'])) {
274
      $commands = json_decode($triggering_element['#value'], TRUE);
275
276
      // Entity IDs that are affected by this command.
277
      if (isset($commands['add'])) {
278
        /** @var \Drupal\Core\Render\RendererInterface $renderer */
279
        $renderer = \Drupal::service('renderer');
280
        $entity_ids = $commands['add'];
281
282
        $selected_entities = &$form_state->get([
283
          'entity_browser',
284
          'selected_entities',
285
        ]);
286
287
        // Get entities added by this command and generate JS commands for them.
288
        $selected_entity_keys = array_keys($selected_entities);
289
        $key_index = count($selected_entity_keys) - count($entity_ids);
290
        foreach ($entity_ids as $entity_pair_info) {
291
          $last_entity_id = $selected_entities[$selected_entity_keys[$key_index]]->id();
292
293
          $html = $renderer->render($form['selection_display']['selected']['items_' . $last_entity_id . '_' . $selected_entity_keys[$key_index]]);
294
295
          $ajax->addCommand(
296
            new ReplaceCommand('div[id="' . $entity_pair_info['proxy_id'] . '"]', static::trimSingleHtmlTag($html))
297
          );
298
299
          $key_index++;
300
        }
301
302
        // Check if action buttons should be added to form. When number of added
303
        // entities is equal to number of selected entities. Then form buttons
304
        // should be also rendered: use_selected and show_selection.
305
        if (count($selected_entities) === count($entity_ids)) {
306
307
          // Order is important, since commands are executed one after another.
308
          $ajax->addCommand(
309
            new AfterCommand('.entities-list', static::trimSingleHtmlTag($renderer->render($form['selection_display']['show_selection'])))
310
          );
311
312
          $ajax->addCommand(
313
            new AfterCommand('.entities-list', static::trimSingleHtmlTag($renderer->render($form['selection_display']['use_selected'])))
314
          );
315
        }
316
      }
317
318
      // Add Invoke command to trigger loading of entities that are queued
319
      // during execution of current Ajax request.
320
      $ajax->addCommand(
321
        new InvokeCommand('[name=ajax_commands_handler]', 'trigger', ['execute-commands'])
322
      );
323
    }
324
325
    return $ajax;
326
  }
327
328
  /**
329
   * Make HTML with single tag suitable for Ajax response.
330
   *
331
   * Comments will be removed and also whitespace characters, because Ajax JS
332
   * "insert" command handling checks number of base elements in response and
333
   * wraps it in a "div" tag if there are more then one base element.
334
   *
335
   * @param string $html
336
   *   HTML content.
337
   *
338
   * @return string
339
   *   Returns cleaner HTML content, suitable for Ajax responses.
340
   */
341
  protected static function trimSingleHtmlTag($html) {
342
    $clearHtml = trim($html);
343
344
    // Remove comments around main single HTML tag. RegEx flag 's' is there to
345
    // allow matching on whitespaces too. That's needed, because generated HTML
346
    // contains a lot newlines.
347
    if (preg_match_all('/(<(?!(!--)).+((\\/)|(<\\/[a-z]+))>)/is', $clearHtml, $matches)) {
348
      if (!empty($matches) && !empty($matches[0])) {
349
        $clearHtml = $matches[0][0];
350
      }
351
    }
352
353
    return $clearHtml;
354
  }
355
356
  /**
357
   * Submit callback for remove buttons.
358
   *
359
   * @param array $form
360
   *   Form.
361
   * @param \Drupal\Core\Form\FormStateInterface $form_state
362
   *   Form state.
363
   */
364
  public static function removeItemSubmit(array &$form, FormStateInterface $form_state) {
365
    $triggering_element = $form_state->getTriggeringElement();
366
367
    // Remove weight of entity being removed.
368
    $form_state->unsetValue([
369
      'selected',
370
      $triggering_element['#attributes']['data-remove-entity'] . '_' . $triggering_element['#attributes']['data-row-id'],
371
    ]);
372
373
    // Remove entity itself.
374
    $selected_entities = &$form_state->get(['entity_browser', 'selected_entities']);
375
    unset($selected_entities[$triggering_element['#attributes']['data-row-id']]);
376
377
    static::saveNewOrder($form_state);
378
    $form_state->setRebuild();
379
  }
380
381
  /**
382
   * {@inheritdoc}
383
   */
384
  public function submit(array &$form, FormStateInterface $form_state) {
385
    $this->saveNewOrder($form_state);
386
    if ($form_state->getTriggeringElement()['#name'] == 'use_selected') {
387
      $this->selectionDone($form_state);
388
    }
389
  }
390
391
  /**
392
   * Saves new ordering of entities based on weight.
393
   *
394
   * @param FormStateInterface $form_state
395
   *   Form state.
396
   */
397
  public static function saveNewOrder(FormStateInterface $form_state) {
398
    $selected = $form_state->getValue('selected');
399
    if (!empty($selected)) {
400
      $weights = array_column($selected, 'weight');
401
      $selected_entities = $form_state->get(['entity_browser', 'selected_entities']);
402
403
      // If we added new entities to the selection at this step we won't have
404
      // weights for them so we have to fake them.
405
      $diff_selected_size = count($selected_entities) - count($weights);
406
      if ($diff_selected_size > 0) {
407
        $max_weight = (max($weights) + 1);
408
        for ($new_weight = $max_weight; $new_weight < ($max_weight + $diff_selected_size); $new_weight++) {
409
          $weights[] = $new_weight;
410
        }
411
      }
412
413
      $ordered = array_combine($weights, $selected_entities);
414
      ksort($ordered);
415
      $form_state->set(['entity_browser', 'selected_entities'], $ordered);
416
    }
417
  }
418
419
  /**
420
   * {@inheritdoc}
421
   */
422
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
423
    $default_entity_type = $form_state->getValue('entity_type', $this->configuration['entity_type']);
424
    $default_display = $form_state->getValue('display', $this->configuration['display']);
425
    $default_display_settings = $form_state->getValue('display_settings', $this->configuration['display_settings']);
426
    $default_display_settings += ['entity_type' => $default_entity_type];
427
428
    if ($form_state->isRebuilding()) {
429
      $form['#prefix'] = '<div id="multi-step-form-wrapper">';
430
    } else {
431
      $form['#prefix'] .= '<div id="multi-step-form-wrapper">';
432
    }
433
    $form['#suffix'] = '</div>';
434
435
    $entity_types = [];
436
    foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
437
      /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */
438
      $entity_types[$entity_type_id] = $entity_type->getLabel();
439
    }
440
    $form['entity_type'] = [
441
      '#type' => 'select',
442
      '#title' => $this->t('Entity type'),
443
      '#description' => $this->t("Entity browser itself does not need information about entity type being selected. It can actually select entities of different type. However, some of the display plugins need to know which entity type they are operating with. Display plugins that do not need this info will ignore this configuration value."),
444
      '#default_value' => $default_entity_type,
445
      '#options' => $entity_types,
446
      '#ajax' => [
447
        'callback' => [$this, 'updateSettingsAjax'],
448
        'wrapper' => 'multi-step-form-wrapper',
449
      ],
450
    ];
451
452
    $displays = [];
453
    foreach ($this->fieldDisplayManager->getDefinitions() as $display_plugin_id => $definition) {
454
      $entity_type = $this->entityTypeManager->getDefinition($default_entity_type);
455
      if ($this->fieldDisplayManager->createInstance($display_plugin_id)->isApplicable($entity_type)) {
456
        $displays[$display_plugin_id] = $definition['label'];
457
      }
458
    }
459
    $form['display'] = [
460
      '#title' => $this->t('Entity display plugin'),
461
      '#type' => 'select',
462
      '#default_value' => $default_display,
463
      '#options' => $displays,
464
      '#ajax' => [
465
        'callback' => [$this, 'updateSettingsAjax'],
466
        'wrapper' => 'multi-step-form-wrapper',
467
      ],
468
    ];
469
470
    $form['display_settings'] = [
471
      '#type' => 'container',
472
      '#title' => $this->t('Entity display plugin configuration'),
473
      '#tree' => TRUE,
474
    ];
475
    if ($default_display_settings) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $default_display_settings of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
476
      $display_plugin = $this->fieldDisplayManager
477
        ->createInstance($default_display, $default_display_settings);
478
479
      $form['display_settings'] += $display_plugin->settingsForm($form, $form_state);
480
    }
481
    $form['select_text'] = [
482
      '#type' => 'textfield',
483
      '#title' => $this->t('Select button text'),
484
      '#default_value' => $this->configuration['select_text'],
485
      '#description' => $this->t('Text to display on the entity browser select button.'),
486
    ];
487
488
    $form['selection_hidden'] = [
489
      '#type' => 'checkbox',
490
      '#title' => $this->t('Selection hidden by default'),
491
      '#default_value' => $this->configuration['selection_hidden'],
492
      '#description' => $this->t('Whether or not the selection should be hidden by default.'),
493
    ];
494
495
    return $form;
496
  }
497
498
  /**
499
   * Ajax callback that updates multi-step plugin configuration form on AJAX updates.
500
   */
501
  public function updateSettingsAjax(array $form, FormStateInterface $form_state) {
502
    return $form;
503
  }
504
505
}
506