3 namespace Drupal\entity_browser\Plugin\EntityBrowser\SelectionDisplay;
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;
17 * Show current selection and delivers selected entities.
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,
27 class MultiStepDisplay extends SelectionDisplayBase {
30 * Field widget display plugin manager.
32 * @var \Drupal\entity_browser\FieldWidgetDisplayManager
34 protected $fieldDisplayManager;
37 * Constructs widget plugin.
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.
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;
60 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
65 $container->get('event_dispatcher'),
66 $container->get('entity_type.manager'),
67 $container->get('plugin.manager.entity_browser.field_widget_display')
74 public function defaultConfiguration() {
76 'entity_type' => 'node',
78 'display_settings' => [],
79 'select_text' => 'Use selected',
80 'selection_hidden' => 0,
81 ] + parent::defaultConfiguration();
87 public function getForm(array &$original_form, FormStateInterface $form_state) {
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);
94 $selected_entities = $form_state->get(['entity_browser', 'selected_entities']);
97 $form['#attached']['library'][] = 'entity_browser/multi_step_display';
99 '#theme_wrappers' => ['container'],
100 '#attributes' => ['class' => ['entities-list']],
103 if ($this->configuration['selection_hidden']) {
104 $form['selected']['#attributes']['class'][] = 'hidden';
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']]
111 $display = $display_plugin->view($entity);
112 if (is_string($display)) {
113 $display = ['#markup' => $display];
116 $form['selected']['items_' . $entity->id() . '_' . $id] = [
117 '#theme_wrappers' => ['container'],
119 'class' => ['item-container'],
120 'data-entity-id' => $entity->id(),
122 'display' => $display,
125 '#value' => $this->t('Remove'),
126 '#submit' => [[get_class($this), 'removeItemSubmit']],
127 '#name' => 'remove_' . $entity->id() . '_' . $id,
129 'class' => ['entity-browser-remove-selected-entity'],
130 'data-row-id' => $id,
131 'data-remove-entity' => 'items_' . $entity->id(),
136 '#default_value' => $id,
137 '#attributes' => ['class' => ['weight']],
142 // Add hidden element used to make execution of front-end commands.
143 $form['ajax_commands_handler'] = [
145 '#name' => 'ajax_commands_handler',
146 '#id' => 'ajax_commands_handler',
147 '#attributes' => ['id' => 'ajax_commands_handler'],
149 'callback' => [get_class($this), 'handleAjaxCommand'],
150 'wrapper' => 'edit-selected',
151 'event' => 'execute_js_commands',
153 'type' => 'fullscreen',
158 $form['use_selected'] = [
160 '#value' => $this->t($this->configuration['select_text']),
161 '#name' => 'use_selected',
163 'class' => ['entity-browser-use-selected'],
165 '#access' => empty($selected_entities) ? FALSE : TRUE,
168 $form['show_selection'] = [
170 '#value' => $this->t('Show selected'),
172 'class' => ['entity-browser-show-selection'],
174 '#access' => empty($selected_entities) ? FALSE : TRUE,
181 * Execute command generated by front-end.
183 * @param \Drupal\Core\Form\FormStateInterface $form_state
186 protected function executeJsCommand(FormStateInterface $form_state) {
187 $triggering_element = $form_state->getTriggeringElement();
189 $commands = json_decode($triggering_element['#value'], TRUE);
191 // Process Remove command.
192 if (isset($commands['remove'])) {
193 $entity_ids = $commands['remove'];
195 // Remove weight of entity being removed.
196 foreach ($entity_ids as $entity_info) {
197 $entity_id_info = explode('_', $entity_info['entity_id']);
199 $form_state->unsetValue([
201 $entity_info['entity_id'],
204 // Remove entity itself.
205 $selected_entities = &$form_state->get(['entity_browser', 'selected_entities']);
206 unset($selected_entities[$entity_id_info[2]]);
209 static::saveNewOrder($form_state);
212 // Process Add command.
213 if (isset($commands['add'])) {
214 $entity_ids = $commands['add'];
216 $entities_to_add = [];
217 $added_entities = [];
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']);
223 if (!isset($entities_to_add[$entity_info[0]])) {
224 $entities_to_add[$entity_info[0]] = [];
227 $entities_to_add[$entity_info[0]][] = $entity_info[1];
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);
236 foreach ($indexed_entities as $entity_id => $entity) {
237 $added_entities[implode(':', [
244 // Array is accessed as reference, so that changes are propagated.
245 $selected_entities = &$form_state->get([
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']];
260 * Handler to generate Ajax response, after command is executed.
264 * @param \Drupal\Core\Form\FormStateInterface $form_state
267 * @return \Drupal\Core\Ajax\AjaxResponse
268 * Return Ajax response with commands.
270 public static function handleAjaxCommand(array $form, FormStateInterface $form_state) {
271 $ajax = new AjaxResponse();
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);
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'];
282 $selected_entities = &$form_state->get([
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();
293 $html = $renderer->render($form['selection_display']['selected']['items_' . $last_entity_id . '_' . $selected_entity_keys[$key_index]]);
296 new ReplaceCommand('div[id="' . $entity_pair_info['proxy_id'] . '"]', static::trimSingleHtmlTag($html))
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)) {
307 // Order is important, since commands are executed one after another.
309 new AfterCommand('.entities-list', static::trimSingleHtmlTag($renderer->render($form['selection_display']['show_selection'])))
313 new AfterCommand('.entities-list', static::trimSingleHtmlTag($renderer->render($form['selection_display']['use_selected'])))
318 // Add Invoke command to trigger loading of entities that are queued
319 // during execution of current Ajax request.
321 new InvokeCommand('[name=ajax_commands_handler]', 'trigger', ['execute-commands'])
329 * Make HTML with single tag suitable for Ajax response.
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.
335 * @param string $html
339 * Returns cleaner HTML content, suitable for Ajax responses.
341 protected static function trimSingleHtmlTag($html) {
342 $clearHtml = trim($html);
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];
357 * Submit callback for remove buttons.
361 * @param \Drupal\Core\Form\FormStateInterface $form_state
364 public static function removeItemSubmit(array &$form, FormStateInterface $form_state) {
365 $triggering_element = $form_state->getTriggeringElement();
367 // Remove weight of entity being removed.
368 $form_state->unsetValue([
370 $triggering_element['#attributes']['data-remove-entity'] . '_' . $triggering_element['#attributes']['data-row-id'],
373 // Remove entity itself.
374 $selected_entities = &$form_state->get(['entity_browser', 'selected_entities']);
375 unset($selected_entities[$triggering_element['#attributes']['data-row-id']]);
377 static::saveNewOrder($form_state);
378 $form_state->setRebuild();
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);
392 * Saves new ordering of entities based on weight.
394 * @param FormStateInterface $form_state
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']);
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;
413 $ordered = array_combine($weights, $selected_entities);
415 $form_state->set(['entity_browser', 'selected_entities'], $ordered);
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];
428 if ($form_state->isRebuilding()) {
429 $form['#prefix'] = '<div id="multi-step-form-wrapper">';
431 $form['#prefix'] .= '<div id="multi-step-form-wrapper">';
433 $form['#suffix'] = '</div>';
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();
440 $form['entity_type'] = [
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,
447 'callback' => [$this, 'updateSettingsAjax'],
448 'wrapper' => 'multi-step-form-wrapper',
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'];
460 '#title' => $this->t('Entity display plugin'),
462 '#default_value' => $default_display,
463 '#options' => $displays,
465 'callback' => [$this, 'updateSettingsAjax'],
466 'wrapper' => 'multi-step-form-wrapper',
470 $form['display_settings'] = [
471 '#type' => 'container',
472 '#title' => $this->t('Entity display plugin configuration'),
475 if ($default_display_settings) {
476 $display_plugin = $this->fieldDisplayManager
477 ->createInstance($default_display, $default_display_settings);
479 $form['display_settings'] += $display_plugin->settingsForm($form, $form_state);
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.'),
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.'),
499 * Ajax callback that updates multi-step plugin configuration form on AJAX updates.
501 public function updateSettingsAjax(array $form, FormStateInterface $form_state) {