Further Drupal 8.6.4 changes. Some core files were not committed before a commit...
[yaffs-website] / web / core / modules / media_library / src / Plugin / Field / FieldWidget / MediaLibraryWidget.php
1 <?php
2
3 namespace Drupal\media_library\Plugin\Field\FieldWidget;
4
5 use Drupal\Component\Serialization\Json;
6 use Drupal\Component\Utility\NestedArray;
7 use Drupal\Component\Utility\SortArray;
8 use Drupal\Core\Entity\EntityTypeManagerInterface;
9 use Drupal\Core\Field\FieldDefinitionInterface;
10 use Drupal\Core\Field\FieldItemListInterface;
11 use Drupal\Core\Field\FieldStorageDefinitionInterface;
12 use Drupal\Core\Field\WidgetBase;
13 use Drupal\Core\Form\FormStateInterface;
14 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
15 use Drupal\Core\Url;
16 use Drupal\media\Entity\Media;
17 use Drupal\media_library\Form\MediaLibraryUploadForm;
18 use Symfony\Component\DependencyInjection\ContainerInterface;
19 use Symfony\Component\Validator\ConstraintViolationInterface;
20
21 /**
22  * Plugin implementation of the 'media_library_widget' widget.
23  *
24  * @FieldWidget(
25  *   id = "media_library_widget",
26  *   label = @Translation("Media library"),
27  *   description = @Translation("Allows you to select items from the media library."),
28  *   field_types = {
29  *     "entity_reference"
30  *   },
31  *   multiple_values = TRUE,
32  * )
33  *
34  * @internal
35  */
36 class MediaLibraryWidget extends WidgetBase implements ContainerFactoryPluginInterface {
37
38   /**
39    * Entity type manager service.
40    *
41    * @var \Drupal\Core\Entity\EntityTypeManagerInterface
42    */
43   protected $entityTypeManager;
44
45   /**
46    * Indicates whether or not the add button should be shown.
47    *
48    * @var bool
49    */
50   protected $addAccess = FALSE;
51
52   /**
53    * Constructs a MediaLibraryWidget widget.
54    *
55    * @param string $plugin_id
56    *   The plugin_id for the plugin instance.
57    * @param mixed $plugin_definition
58    *   The plugin implementation definition.
59    * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
60    *   The definition of the field to which the widget is associated.
61    * @param array $settings
62    *   The widget settings.
63    * @param array $third_party_settings
64    *   Any third party settings.
65    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
66    *   Entity type manager service.
67    * @param bool $add_access
68    *   Indicates whether or not the add button should be shown.
69    */
70   public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, $add_access) {
71     parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
72     $this->entityTypeManager = $entity_type_manager;
73     $this->addAccess = $add_access;
74   }
75
76   /**
77    * {@inheritdoc}
78    */
79   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
80     $settings = $configuration['field_definition']->getSettings()['handler_settings'];
81     $target_bundles = isset($settings['target_bundles']) ? $settings['target_bundles'] : NULL;
82     return new static(
83       $plugin_id,
84       $plugin_definition,
85       $configuration['field_definition'],
86       $configuration['settings'],
87       $configuration['third_party_settings'],
88       $container->get('entity_type.manager'),
89       // @todo Use URL access in https://www.drupal.org/node/2956747
90       MediaLibraryUploadForm::create($container)->access($target_bundles)->isAllowed()
91     );
92   }
93
94   /**
95    * {@inheritdoc}
96    */
97   public static function isApplicable(FieldDefinitionInterface $field_definition) {
98     return $field_definition->getSetting('target_type') === 'media';
99   }
100
101   /**
102    * {@inheritdoc}
103    */
104   public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL) {
105     // Load the items for form rebuilds from the field state.
106     $field_state = static::getWidgetState($form['#parents'], $this->fieldDefinition->getName(), $form_state);
107     if (isset($field_state['items'])) {
108       usort($field_state['items'], [SortArray::class, 'sortByWeightElement']);
109       $items->setValue($field_state['items']);
110     }
111
112     return parent::form($items, $form, $form_state, $get_delta);
113   }
114
115   /**
116    * {@inheritdoc}
117    */
118   public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
119     /** @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $items */
120     $referenced_entities = $items->referencedEntities();
121     $view_builder = $this->entityTypeManager->getViewBuilder('media');
122     $field_name = $this->fieldDefinition->getName();
123     $parents = $form['#parents'];
124     $id_suffix = '-' . implode('-', $parents);
125     $wrapper_id = $field_name . '-media-library-wrapper' . $id_suffix;
126     $limit_validation_errors = [array_merge($parents, [$field_name])];
127
128     $settings = $this->getFieldSetting('handler_settings');
129     $element += [
130       '#type' => 'fieldset',
131       '#cardinality' => $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(),
132       '#target_bundles' => isset($settings['target_bundles']) ? $settings['target_bundles'] : FALSE,
133       '#attributes' => [
134         'id' => $wrapper_id,
135         'class' => ['media-library-widget'],
136       ],
137       '#attached' => [
138         'library' => ['media_library/widget'],
139       ],
140     ];
141
142     $element['selection'] = [
143       '#type' => 'container',
144       '#attributes' => [
145         'class' => [
146           'js-media-library-selection',
147           'media-library-selection',
148         ],
149       ],
150     ];
151
152     if (empty($referenced_entities)) {
153       $element['empty_selection'] = [
154         '#markup' => $this->t('<p>No media items are selected.</p>'),
155       ];
156     }
157     else {
158       $element['weight_toggle'] = [
159         '#type' => 'html_tag',
160         '#tag' => 'button',
161         '#value' => $this->t('Show media item weights'),
162         '#attributes' => [
163           'class' => [
164             'link',
165             'media-library-widget__toggle-weight',
166             'js-media-library-widget-toggle-weight',
167           ],
168           'title' => $this->t('Re-order media by numerical weight instead of dragging'),
169         ],
170       ];
171     }
172
173     foreach ($referenced_entities as $delta => $media_item) {
174       $element['selection'][$delta] = [
175         '#type' => 'container',
176         '#attributes' => [
177           'class' => [
178             'media-library-item',
179             'js-media-library-item',
180           ],
181         ],
182         'preview' => [
183           '#type' => 'container',
184           // @todo Make the view mode configurable in https://www.drupal.org/project/drupal/issues/2971209
185           'rendered_entity' => $view_builder->view($media_item, 'media_library'),
186           'remove_button' => [
187             '#type' => 'submit',
188             '#name' => $field_name . '-' . $delta . '-media-library-remove-button' . $id_suffix,
189             '#value' => $this->t('Remove'),
190             '#attributes' => [
191               'class' => ['media-library-item__remove'],
192             ],
193             '#ajax' => [
194               'callback' => [static::class, 'updateWidget'],
195               'wrapper' => $wrapper_id,
196             ],
197             '#submit' => [[static::class, 'removeItem']],
198             // Prevent errors in other widgets from preventing removal.
199             '#limit_validation_errors' => $limit_validation_errors,
200           ],
201         ],
202         'target_id' => [
203           '#type' => 'hidden',
204           '#value' => $media_item->id(),
205         ],
206         // This hidden value can be toggled visible for accessibility.
207         'weight' => [
208           '#type' => 'number',
209           '#title' => $this->t('Weight'),
210           '#default_value' => $delta,
211           '#attributes' => [
212             'class' => [
213               'js-media-library-item-weight',
214               'media-library-item__weight',
215             ],
216           ],
217         ],
218       ];
219     }
220
221     $cardinality_unlimited = ($element['#cardinality'] === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
222     $remaining = $element['#cardinality'] - count($referenced_entities);
223
224     // Inform the user of how many items are remaining.
225     if (!$cardinality_unlimited) {
226       if ($remaining) {
227         $cardinality_message = $this->formatPlural($remaining, 'One media item remaining.', '@count media items remaining.');
228       }
229       else {
230         $cardinality_message = $this->t('The maximum number of media items have been selected.');
231       }
232       $element['#description'] .= '<br />' . $cardinality_message;
233     }
234
235     $query = [
236       'media_library_widget_id' => $field_name . $id_suffix,
237       'media_library_allowed_types' => $element['#target_bundles'],
238       'media_library_remaining' => $cardinality_unlimited ? FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED : $remaining,
239     ];
240     $dialog_options = Json::encode([
241       'dialogClass' => 'media-library-widget-modal',
242       'height' => '75%',
243       'width' => '75%',
244       'title' => $this->t('Media library'),
245     ]);
246
247     // Add a button that will load the Media library in a modal using AJAX.
248     $element['media_library_open_button'] = [
249       '#type' => 'link',
250       '#title' => $this->t('Browse media'),
251       '#name' => $field_name . '-media-library-open-button' . $id_suffix,
252       // @todo Make the view configurable in https://www.drupal.org/project/drupal/issues/2971209
253       '#url' => Url::fromRoute('view.media_library.widget', [], [
254         'query' => $query,
255       ]),
256       '#attributes' => [
257         'class' => ['button', 'use-ajax', 'media-library-open-button'],
258         'data-dialog-type' => 'modal',
259         'data-dialog-options' => $dialog_options,
260       ],
261       // Prevent errors in other widgets from preventing addition.
262       '#limit_validation_errors' => $limit_validation_errors,
263       '#access' => $cardinality_unlimited || $remaining > 0,
264     ];
265
266     $element['media_library_add_button'] = [
267       '#type' => 'link',
268       '#title' => $this->t('Add media'),
269       '#name' => $field_name . '-media-library-add-button' . $id_suffix,
270       '#url' => Url::fromRoute('media_library.upload', [], [
271         'query' => $query,
272       ]),
273       '#attributes' => [
274         'class' => ['button', 'use-ajax', 'media-library-add-button'],
275         'data-dialog-type' => 'modal',
276         'data-dialog-options' => $dialog_options,
277       ],
278       // Prevent errors in other widgets from preventing addition.
279       '#limit_validation_errors' => $limit_validation_errors,
280       '#access' => $this->addAccess && ($cardinality_unlimited || $remaining > 0),
281     ];
282
283     // This hidden field and button are used to add new items to the widget.
284     $element['media_library_selection'] = [
285       '#type' => 'hidden',
286       '#attributes' => [
287         // This is used to pass the selection from the modal to the widget.
288         'data-media-library-widget-value' => $field_name . $id_suffix,
289       ],
290     ];
291
292     // When a selection is made this hidden button is pressed to add new media
293     // items based on the "media_library_selection" value.
294     $element['media_library_update_widget'] = [
295       '#type' => 'submit',
296       '#value' => $this->t('Update widget'),
297       '#name' => $field_name . '-media-library-update' . $id_suffix,
298       '#ajax' => [
299         'callback' => [static::class, 'updateWidget'],
300         'wrapper' => $wrapper_id,
301       ],
302       '#attributes' => [
303         'data-media-library-widget-update' => $field_name . $id_suffix,
304         'class' => ['js-hide'],
305       ],
306       '#validate' => [[static::class, 'validateItems']],
307       '#submit' => [[static::class, 'updateItems']],
308       // Prevent errors in other widgets from preventing updates.
309       '#limit_validation_errors' => $limit_validation_errors,
310     ];
311
312     return $element;
313   }
314
315   /**
316    * {@inheritdoc}
317    */
318   public function errorElement(array $element, ConstraintViolationInterface $error, array $form, FormStateInterface $form_state) {
319     return isset($element['target_id']) ? $element['target_id'] : FALSE;
320   }
321
322   /**
323    * {@inheritdoc}
324    */
325   public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
326     if (isset($values['selection'])) {
327       usort($values['selection'], [SortArray::class, 'sortByWeightElement']);
328       return $values['selection'];
329     }
330     return [];
331   }
332
333   /**
334    * AJAX callback to update the widget when the selection changes.
335    *
336    * @param array $form
337    *   The form array.
338    * @param \Drupal\Core\Form\FormStateInterface $form_state
339    *   The form state.
340    *
341    * @return array
342    *   An array representing the updated widget.
343    */
344   public static function updateWidget(array $form, FormStateInterface $form_state) {
345     $triggering_element = $form_state->getTriggeringElement();
346     // This callback is either invoked from the remove button or the update
347     // button, which have different nesting levels.
348     $length = end($triggering_element['#parents']) === 'remove_button' ? -4 : -1;
349     if (count($triggering_element['#array_parents']) < abs($length)) {
350       throw new \LogicException('The element that triggered the widget update was at an unexpected depth. Triggering element parents were: ' . implode(',', $triggering_element['#array_parents']));
351     }
352     $parents = array_slice($triggering_element['#array_parents'], 0, $length);
353     $element = NestedArray::getValue($form, $parents);
354     // Always clear the textfield selection to prevent duplicate additions.
355     $element['media_library_selection']['#value'] = '';
356     return $element;
357   }
358
359   /**
360    * Submit callback for remove buttons.
361    *
362    * @param array $form
363    *   The form array.
364    * @param \Drupal\Core\Form\FormStateInterface $form_state
365    *   The form state.
366    */
367   public static function removeItem(array $form, FormStateInterface $form_state) {
368     $triggering_element = $form_state->getTriggeringElement();
369
370     // Get the parents required to find the top-level widget element.
371     if (count($triggering_element['#array_parents']) < 4) {
372       throw new \LogicException('Expected the remove button to be more than four levels deep in the form. Triggering element parents were: ' . implode(',', $triggering_element['#array_parents']));
373     }
374     $parents = array_slice($triggering_element['#array_parents'], 0, -4);
375     // Get the delta of the item being removed.
376     $delta = array_slice($triggering_element['#array_parents'], -3, 1)[0];
377     $element = NestedArray::getValue($form, $parents);
378
379     // Get the field state.
380     $path = $element['#parents'];
381     $values = NestedArray::getValue($form_state->getValues(), $path);
382     $field_state = static::getFieldState($element, $form_state);
383
384     // Remove the item from the field state and update it.
385     if (isset($values['selection'][$delta])) {
386       array_splice($values['selection'], $delta, 1);
387       $field_state['items'] = $values['selection'];
388       static::setFieldState($element, $form_state, $field_state);
389     }
390
391     $form_state->setRebuild();
392   }
393
394   /**
395    * Validates that newly selected items can be added to the widget.
396    *
397    * Making an invalid selection from the view should not be possible, but we
398    * still validate in case other selection methods (ex: upload) are valid.
399    *
400    * @param array $form
401    *   The form array.
402    * @param \Drupal\Core\Form\FormStateInterface $form_state
403    *   The form state.
404    */
405   public static function validateItems(array $form, FormStateInterface $form_state) {
406     $button = $form_state->getTriggeringElement();
407     $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
408
409     $field_state = static::getFieldState($element, $form_state);
410     $media = static::getNewMediaItems($element, $form_state);
411     if (empty($media)) {
412       return;
413     }
414
415     // Check if more items were selected than we allow.
416     $cardinality_unlimited = ($element['#cardinality'] === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
417     $selection = count($field_state['items']) + count($media);
418     if (!$cardinality_unlimited && ($selection > $element['#cardinality'])) {
419       $form_state->setError($element, \Drupal::translation()->formatPlural($element['#cardinality'], 'Only one item can be selected.', 'Only @count items can be selected.'));
420     }
421
422     // Validate that each selected media is of an allowed bundle.
423     $all_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo('media');
424     $bundle_labels = array_map(function ($bundle) use ($all_bundles) {
425       return $all_bundles[$bundle]['label'];
426     }, $element['#target_bundles']);
427     foreach ($media as $media_item) {
428       if ($element['#target_bundles'] && !in_array($media_item->bundle(), $element['#target_bundles'], TRUE)) {
429         $form_state->setError($element, t('The media item "@label" is not of an accepted type. Allowed types: @types', [
430           '@label' => $media_item->label(),
431           '@types' => implode(', ', $bundle_labels),
432         ]));
433       }
434     }
435   }
436
437   /**
438    * Updates the field state and flags the form for rebuild.
439    *
440    * @param array $form
441    *   The form array.
442    * @param \Drupal\Core\Form\FormStateInterface $form_state
443    *   The form state.
444    */
445   public static function updateItems(array $form, FormStateInterface $form_state) {
446     $button = $form_state->getTriggeringElement();
447     $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
448
449     $field_state = static::getFieldState($element, $form_state);
450
451     $media = static::getNewMediaItems($element, $form_state);
452     if (!empty($media)) {
453       $weight = count($field_state['items']);
454       foreach ($media as $media_item) {
455         // Any ID can be passed to the widget, so we have to check access.
456         if ($media_item->access('view')) {
457           $field_state['items'][] = [
458             'target_id' => $media_item->id(),
459             'weight' => $weight++,
460           ];
461         }
462       }
463       static::setFieldState($element, $form_state, $field_state);
464     }
465
466     $form_state->setRebuild();
467   }
468
469   /**
470    * Gets newly selected media items.
471    *
472    * @param array $element
473    *   The wrapping element for this widget.
474    * @param \Drupal\Core\Form\FormStateInterface $form_state
475    *   The current state of the form.
476    *
477    * @return \Drupal\media\MediaInterface[]
478    *   An array of selected media items.
479    */
480   protected static function getNewMediaItems(array $element, FormStateInterface $form_state) {
481     // Get the new media IDs passed to our hidden button.
482     $values = $form_state->getValues();
483     $path = $element['#parents'];
484     $value = NestedArray::getValue($values, $path);
485
486     if (!empty($value['media_library_selection'])) {
487       $ids = explode(',', $value['media_library_selection']);
488       $ids = array_filter($ids, 'is_numeric');
489       if (!empty($ids)) {
490         /** @var \Drupal\media\MediaInterface[] $media */
491         return Media::loadMultiple($ids);
492       }
493     }
494     return [];
495   }
496
497   /**
498    * Gets the field state for the widget.
499    *
500    * @param array $element
501    *   The wrapping element for this widget.
502    * @param \Drupal\Core\Form\FormStateInterface $form_state
503    *   The current state of the form.
504    *
505    * @return array[]
506    *   An array of arrays with the following key/value pairs:
507    *   - items: (array) An array of selections.
508    *     - target_id: (int) A media entity ID.
509    *     - weight: (int) A weight for the selection.
510    */
511   protected static function getFieldState(array $element, FormStateInterface $form_state) {
512     // Default to using the current selection if the form is new.
513     $path = $element['#parents'];
514     $values = NestedArray::getValue($form_state->getValues(), $path);
515     $selection = isset($values['selection']) ? $values['selection'] : [];
516
517     $widget_state = static::getWidgetState($element['#field_parents'], $element['#field_name'], $form_state);
518     $widget_state['items'] = isset($widget_state['items']) ? $widget_state['items'] : $selection;
519     return $widget_state;
520   }
521
522   /**
523    * Sets the field state for the widget.
524    *
525    * @param array $element
526    *   The wrapping element for this widget.
527    * @param \Drupal\Core\Form\FormStateInterface $form_state
528    *   The current state of the form.
529    * @param array[] $field_state
530    *   An array of arrays with the following key/value pairs:
531    *   - items: (array) An array of selections.
532    *     - target_id: (int) A media entity ID.
533    *     - weight: (int) A weight for the selection.
534    */
535   protected static function setFieldState(array $element, FormStateInterface $form_state, array $field_state) {
536     static::setWidgetState($element['#field_parents'], $element['#field_name'], $form_state, $field_state);
537   }
538
539 }