2609bd3e9fd0de774215f61b6f7fe46ad0a552e9
[yaffs-website] / Plugin / EntityBrowser / SelectionDisplay / MultiStepDisplay.php
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  *   js_commands = 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) {
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 }