View source
<?php
namespace Drupal\media_library\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\SortArray;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AnnounceCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\field_ui\FieldUI;
use Drupal\media\Entity\Media;
use Drupal\media_library\MediaLibraryUiBuilder;
use Drupal\media_library\MediaLibraryState;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;
class MediaLibraryWidget extends WidgetBase implements ContainerFactoryPluginInterface, TrustedCallbackInterface {
protected $entityTypeManager;
protected $currentUser;
protected $moduleHandler;
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user = NULL, ModuleHandlerInterface $module_handler = NULL) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
$this->entityTypeManager = $entity_type_manager;
if (!$current_user) {
@trigger_error('The current_user service must be passed to MediaLibraryWidget::__construct(), it is required before Drupal 9.0.0.', E_USER_DEPRECATED);
$current_user = \Drupal::currentUser();
}
$this->currentUser = $current_user;
if (!$module_handler) {
@trigger_error('The module_handler service must be passed to MediaLibraryWidget::__construct(), it is required before Drupal 9.0.0.', E_USER_DEPRECATED);
$module_handler = \Drupal::moduleHandler();
}
$this->moduleHandler = $module_handler;
}
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($plugin_id, $plugin_definition, $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings'], $container
->get('entity_type.manager'), $container
->get('current_user'), $container
->get('module_handler'));
}
public static function isApplicable(FieldDefinitionInterface $field_definition) {
return $field_definition
->getSetting('target_type') === 'media';
}
public static function defaultSettings() {
return [
'media_types' => [],
] + parent::defaultSettings();
}
protected function getAllowedMediaTypeIdsSorted() {
$sorted_media_type_ids = $this
->getSetting('media_types');
$handler_settings = $this
->getFieldSetting('handler_settings');
$allowed_media_type_ids = isset($handler_settings['target_bundles']) ? $handler_settings['target_bundles'] : NULL;
if ($allowed_media_type_ids === []) {
return $allowed_media_type_ids;
}
if ($allowed_media_type_ids === NULL) {
$allowed_media_type_ids = $this->entityTypeManager
->getStorage('media_type')
->getQuery()
->execute();
}
if (empty($sorted_media_type_ids)) {
return $allowed_media_type_ids;
}
$new_media_type_ids = array_diff($allowed_media_type_ids, $sorted_media_type_ids);
$sorted_media_type_ids = array_merge($sorted_media_type_ids, array_values($new_media_type_ids));
$sorted_media_type_ids = array_intersect($sorted_media_type_ids, $allowed_media_type_ids);
return array_values($sorted_media_type_ids);
}
public function settingsForm(array $form, FormStateInterface $form_state) {
$media_type_ids = $this
->getAllowedMediaTypeIdsSorted();
if (count($media_type_ids) <= 1) {
return $form;
}
$form['media_types'] = [
'#type' => 'table',
'#header' => [
$this
->t('Tab order'),
$this
->t('Weight'),
],
'#tabledrag' => [
[
'action' => 'order',
'relationship' => 'sibling',
'group' => 'weight',
],
],
'#value_callback' => [
static::class,
'setMediaTypesValue',
],
];
$media_types = $this->entityTypeManager
->getStorage('media_type')
->loadMultiple($media_type_ids);
$weight = 0;
foreach ($media_types as $media_type_id => $media_type) {
$label = $media_type
->label();
$form['media_types'][$media_type_id] = [
'label' => [
'#markup' => $label,
],
'weight' => [
'#type' => 'weight',
'#title' => t('Weight for @title', [
'@title' => $label,
]),
'#title_display' => 'invisible',
'#default_value' => $weight,
'#attributes' => [
'class' => [
'weight',
],
],
],
'#weight' => $weight,
'#attributes' => [
'class' => [
'draggable',
],
],
];
$weight++;
}
return $form;
}
public static function setMediaTypesValue(array &$element, $input, FormStateInterface $form_state) {
if ($input === FALSE) {
return isset($element['#default_value']) ? $element['#default_value'] : [];
}
uasort($input, 'Drupal\\Component\\Utility\\SortArray::sortByWeightElement');
$sorted_media_type_ids = array_keys($input);
$form_state
->setValue($element['#parents'], $sorted_media_type_ids);
foreach ($sorted_media_type_ids as $media_type_id) {
unset($element[$media_type_id]);
}
return $sorted_media_type_ids;
}
public function settingsSummary() {
$summary = [];
$media_type_labels = [];
$media_types = $this->entityTypeManager
->getStorage('media_type')
->loadMultiple($this
->getAllowedMediaTypeIdsSorted());
if (count($media_types) !== 1) {
foreach ($media_types as $media_type) {
$media_type_labels[] = $media_type
->label();
}
$summary[] = t('Tab order: @order', [
'@order' => implode(', ', $media_type_labels),
]);
}
return $summary;
}
public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL) {
$field_state = static::getWidgetState($form['#parents'], $this->fieldDefinition
->getName(), $form_state);
if (isset($field_state['items'])) {
usort($field_state['items'], [
SortArray::class,
'sortByWeightElement',
]);
$items
->setValue($field_state['items']);
}
return parent::form($items, $form, $form_state, $get_delta);
}
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$referenced_entities = $items
->referencedEntities();
$view_builder = $this->entityTypeManager
->getViewBuilder('media');
$field_name = $this->fieldDefinition
->getName();
$parents = $form['#parents'];
$id_suffix = $parents ? '-' . implode('-', $parents) : '';
$field_widget_id = implode(':', array_filter([
$field_name,
$id_suffix,
]));
$wrapper_id = $field_name . '-media-library-wrapper' . $id_suffix;
$limit_validation_errors = [
array_merge($parents, [
$field_name,
]),
];
$settings = $this
->getFieldSetting('handler_settings');
$element += [
'#type' => 'fieldset',
'#cardinality' => $this->fieldDefinition
->getFieldStorageDefinition()
->getCardinality(),
'#target_bundles' => isset($settings['target_bundles']) ? $settings['target_bundles'] : FALSE,
'#attributes' => [
'id' => $wrapper_id,
'class' => [
'js-media-library-widget',
],
],
'#pre_render' => [
[
$this,
'preRenderWidget',
],
],
'#attached' => [
'library' => [
'media_library/widget',
],
],
'#theme_wrappers' => [
'fieldset__media_library_widget',
],
];
$allowed_media_type_ids = $this
->getAllowedMediaTypeIdsSorted();
if (!$allowed_media_type_ids) {
$element['no_types_message'] = [
'#markup' => $this
->getNoMediaTypesAvailableMessage(),
];
return $element;
}
if (empty($referenced_entities)) {
$element['#field_prefix']['empty_selection'] = [
'#markup' => $this
->t('No media items are selected.'),
];
}
else {
$element['#field_prefix']['weight_toggle'] = [
'#type' => 'html_tag',
'#tag' => 'button',
'#value' => $this
->t('Show media item weights'),
'#attributes' => [
'class' => [
'link',
'js-media-library-widget-toggle-weight',
],
],
];
}
$element['selection'] = [
'#type' => 'container',
'#theme_wrappers' => [
'container__media_library_widget_selection',
],
'#attributes' => [
'class' => [
'js-media-library-selection',
],
],
];
foreach ($referenced_entities as $delta => $media_item) {
$element['selection'][$delta] = [
'#theme' => 'media_library_item__widget',
'#attributes' => [
'class' => [
'js-media-library-item',
],
'tabindex' => '-1',
'data-media-library-item-delta' => $delta,
],
'remove_button' => [
'#type' => 'submit',
'#name' => $field_name . '-' . $delta . '-media-library-remove-button' . $id_suffix,
'#value' => $this
->t('Remove'),
'#media_id' => $media_item
->id(),
'#attributes' => [
'aria-label' => $this
->t('Remove @label', [
'@label' => $media_item
->label(),
]),
],
'#ajax' => [
'callback' => [
static::class,
'updateWidget',
],
'wrapper' => $wrapper_id,
'progress' => [
'type' => 'throbber',
'message' => $this
->t('Removing @label.', [
'@label' => $media_item
->label(),
]),
],
],
'#submit' => [
[
static::class,
'removeItem',
],
],
'#limit_validation_errors' => $limit_validation_errors,
],
'rendered_entity' => $view_builder
->view($media_item, 'media_library'),
'target_id' => [
'#type' => 'hidden',
'#value' => $media_item
->id(),
],
'weight' => [
'#type' => 'number',
'#theme' => 'input__number__media_library_item_weight',
'#title' => $this
->t('Weight'),
'#default_value' => $delta,
'#attributes' => [
'class' => [
'js-media-library-item-weight',
],
],
],
];
}
$cardinality_unlimited = $element['#cardinality'] === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED;
$remaining = $element['#cardinality'] - count($referenced_entities);
if (!$cardinality_unlimited) {
if ($remaining) {
$cardinality_message = $this
->formatPlural($remaining, 'One media item remaining.', '@count media items remaining.');
}
else {
$cardinality_message = $this
->t('The maximum number of media items have been selected.');
}
if (!empty($element['#description'])) {
$element['#description'] .= '<br />';
}
$element['#description'] .= $cardinality_message;
}
$selected_type_id = reset($allowed_media_type_ids);
$remaining = $cardinality_unlimited ? FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED : $remaining;
$entity = $items
->getEntity();
$opener_parameters = [
'field_widget_id' => $field_widget_id,
'entity_type_id' => $entity
->getEntityTypeId(),
'bundle' => $entity
->bundle(),
'field_name' => $field_name,
];
if (!$entity
->isNew()) {
$opener_parameters['entity_id'] = (string) $entity
->id();
if ($entity
->getEntityType()
->isRevisionable()) {
$opener_parameters['revision_id'] = (string) $entity
->getRevisionId();
}
}
$state = MediaLibraryState::create('media_library.opener.field_widget', $allowed_media_type_ids, $selected_type_id, $remaining, $opener_parameters);
$element['open_button'] = [
'#type' => 'button',
'#value' => $this
->t('Add media'),
'#name' => $field_name . '-media-library-open-button' . $id_suffix,
'#attributes' => [
'class' => [
'js-media-library-open-button',
],
'data-disable-refocus' => 'true',
],
'#media_library_state' => $state,
'#ajax' => [
'callback' => [
static::class,
'openMediaLibrary',
],
'progress' => [
'type' => 'throbber',
'message' => $this
->t('Opening media library.'),
],
],
'#limit_validation_errors' => [],
];
if (!$cardinality_unlimited && $remaining === 0) {
$triggering_element = $form_state
->getTriggeringElement();
if ($triggering_element && ($trigger_parents = $triggering_element['#array_parents']) && end($trigger_parents) === 'media_library_update_widget') {
$element['open_button']['#attributes']['data-disabled-focus'] = 'true';
$element['open_button']['#attributes']['class'][] = 'visually-hidden';
}
else {
$element['open_button']['#disabled'] = TRUE;
$element['open_button']['#attributes']['class'][] = 'visually-hidden';
}
}
$element['media_library_selection'] = [
'#type' => 'hidden',
'#attributes' => [
'data-media-library-widget-value' => $field_widget_id,
],
];
$element['media_library_update_widget'] = [
'#type' => 'submit',
'#value' => $this
->t('Update widget'),
'#name' => $field_name . '-media-library-update' . $id_suffix,
'#ajax' => [
'callback' => [
static::class,
'updateWidget',
],
'wrapper' => $wrapper_id,
'progress' => [
'type' => 'throbber',
'message' => $this
->t('Adding selection.'),
],
],
'#attributes' => [
'data-media-library-widget-update' => $field_widget_id,
'class' => [
'js-hide',
],
],
'#validate' => [
[
static::class,
'validateItems',
],
],
'#submit' => [
[
static::class,
'addItems',
],
],
'#limit_validation_errors' => !empty($referenced_entities) ? $limit_validation_errors : [],
];
return $element;
}
public static function trustedCallbacks() {
return [
'preRenderWidget',
];
}
public function preRenderWidget(array $element) {
if (isset($element['open_button'])) {
$element['#field_suffix']['open_button'] = $element['open_button'];
unset($element['open_button']);
}
return $element;
}
protected function getNoMediaTypesAvailableMessage() {
$entity_type_id = $this->fieldDefinition
->getTargetEntityTypeId();
$default_message = $this
->t('There are no allowed media types configured for this field. Please contact the site administrator.');
if (!$this->currentUser
->hasPermission("administer {$entity_type_id} fields")) {
return $default_message;
}
if (!$this->moduleHandler
->moduleExists('field_ui')) {
return $this
->t('There are no allowed media types configured for this field. Edit the field settings to select the allowed media types.');
}
$route_parameters = FieldUI::getRouteBundleParameter($this->entityTypeManager
->getDefinition($entity_type_id), $this->fieldDefinition
->getTargetBundle());
$route_parameters['field_config'] = $this->fieldDefinition
->id();
$url = Url::fromRoute('entity.field_config.' . $entity_type_id . '_field_edit_form', $route_parameters);
if ($url
->access($this->currentUser)) {
return $this
->t('There are no allowed media types configured for this field. <a href=":url">Edit the field settings</a> to select the allowed media types.', [
':url' => $url
->toString(),
]);
}
return $default_message;
}
public function errorElement(array $element, ConstraintViolationInterface $error, array $form, FormStateInterface $form_state) {
return isset($element['target_id']) ? $element['target_id'] : FALSE;
}
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
if (isset($values['selection'])) {
usort($values['selection'], [
SortArray::class,
'sortByWeightElement',
]);
return $values['selection'];
}
return [];
}
public static function updateWidget(array $form, FormStateInterface $form_state) {
$triggering_element = $form_state
->getTriggeringElement();
$wrapper_id = $triggering_element['#ajax']['wrapper'];
$is_remove_button = end($triggering_element['#parents']) === 'remove_button';
$length = $is_remove_button ? -3 : -1;
if (count($triggering_element['#array_parents']) < abs($length)) {
throw new \LogicException('The element that triggered the widget update was at an unexpected depth. Triggering element parents were: ' . implode(',', $triggering_element['#array_parents']));
}
$parents = array_slice($triggering_element['#array_parents'], 0, $length);
$element = NestedArray::getValue($form, $parents);
$element['media_library_selection']['#value'] = '';
$field_state = static::getFieldState($element, $form_state);
if ($is_remove_button) {
$announcement = t('@label has been removed.', [
'@label' => Media::load($field_state['removed_item_id'])
->label(),
]);
}
else {
$new_items = count(static::getNewMediaItems($element, $form_state));
$announcement = \Drupal::translation()
->formatPlural($new_items, 'Added one media item.', 'Added @count media items.');
}
$response = new AjaxResponse();
$response
->addCommand(new ReplaceCommand("#{$wrapper_id}", $element));
$response
->addCommand(new AnnounceCommand($announcement));
$removed_last = $is_remove_button && !count($field_state['items']);
if ($is_remove_button && !$removed_last) {
$removed_item_weight = $field_state['removed_item_weight'];
$delta_to_focus = 0;
foreach ($field_state['items'] as $delta => $item_fields) {
$delta_to_focus = $delta;
if ($item_fields['weight'] > $removed_item_weight) {
$delta_to_focus--;
break;
}
}
$response
->addCommand(new InvokeCommand("#{$wrapper_id} [data-media-library-item-delta={$delta_to_focus}]", 'focus'));
}
elseif ($removed_last || !$is_remove_button && !isset($element['open_button']['#attributes']['data-disabled-focus'])) {
$response
->addCommand(new InvokeCommand("#{$wrapper_id} .js-media-library-open-button", 'focus'));
}
return $response;
}
public static function removeItem(array $form, FormStateInterface $form_state) {
$triggering_element = $form_state
->getTriggeringElement();
if (count($triggering_element['#array_parents']) < 4) {
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']));
}
$parents = array_slice($triggering_element['#array_parents'], 0, -3);
$element = NestedArray::getValue($form, $parents);
$path = $element['#parents'];
$values = NestedArray::getValue($form_state
->getValues(), $path);
$field_state = static::getFieldState($element, $form_state);
$delta = array_slice($triggering_element['#array_parents'], -2, 1)[0];
if (isset($values['selection'][$delta])) {
$field_state['removed_item_weight'] = $values['selection'][$delta]['weight'];
$field_state['removed_item_id'] = $triggering_element['#media_id'];
unset($values['selection'][$delta]);
$field_state['items'] = $values['selection'];
static::setFieldState($element, $form_state, $field_state);
}
$form_state
->setRebuild();
}
public static function openMediaLibrary(array $form, FormStateInterface $form_state) {
$triggering_element = $form_state
->getTriggeringElement();
$library_ui = \Drupal::service('media_library.ui_builder')
->buildUi($triggering_element['#media_library_state']);
$dialog_options = MediaLibraryUiBuilder::dialogOptions();
return (new AjaxResponse())
->addCommand(new OpenModalDialogCommand($dialog_options['title'], $library_ui, $dialog_options));
}
public static function validateItems(array $form, FormStateInterface $form_state) {
$button = $form_state
->getTriggeringElement();
$element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
$field_state = static::getFieldState($element, $form_state);
$media = static::getNewMediaItems($element, $form_state);
if (empty($media)) {
return;
}
$cardinality_unlimited = $element['#cardinality'] === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED;
$selection = count($field_state['items']) + count($media);
if (!$cardinality_unlimited && $selection > $element['#cardinality']) {
$form_state
->setError($element, \Drupal::translation()
->formatPlural($element['#cardinality'], 'Only one item can be selected.', 'Only @count items can be selected.'));
}
$all_bundles = \Drupal::service('entity_type.bundle.info')
->getBundleInfo('media');
$bundle_labels = array_map(function ($bundle) use ($all_bundles) {
return $all_bundles[$bundle]['label'];
}, $element['#target_bundles']);
foreach ($media as $media_item) {
if ($element['#target_bundles'] && !in_array($media_item
->bundle(), $element['#target_bundles'], TRUE)) {
$form_state
->setError($element, t('The media item "@label" is not of an accepted type. Allowed types: @types', [
'@label' => $media_item
->label(),
'@types' => implode(', ', $bundle_labels),
]));
}
}
}
public static function addItems(array $form, FormStateInterface $form_state) {
$button = $form_state
->getTriggeringElement();
$element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
$field_state = static::getFieldState($element, $form_state);
$media = static::getNewMediaItems($element, $form_state);
if (!empty($media)) {
$last_element = end($field_state['items']);
$weight = $last_element ? $last_element['weight'] : 0;
foreach ($media as $media_item) {
if ($media_item
->access('view')) {
$field_state['items'][] = [
'target_id' => $media_item
->id(),
'weight' => ++$weight,
];
}
}
static::setFieldState($element, $form_state, $field_state);
}
$form_state
->setRebuild();
}
protected static function getNewMediaItems(array $element, FormStateInterface $form_state) {
$values = $form_state
->getUserInput();
$path = $element['#parents'];
$value = NestedArray::getValue($values, $path);
if (!empty($value['media_library_selection'])) {
$ids = explode(',', $value['media_library_selection']);
$ids = array_filter($ids, 'is_numeric');
if (!empty($ids)) {
return Media::loadMultiple($ids);
}
}
return [];
}
protected static function getFieldState(array $element, FormStateInterface $form_state) {
$path = $element['#parents'];
$values = NestedArray::getValue($form_state
->getUserInput(), $path);
$selection = isset($values['selection']) ? $values['selection'] : [];
$widget_state = static::getWidgetState($element['#field_parents'], $element['#field_name'], $form_state);
$widget_state['items'] = isset($widget_state['items']) ? $widget_state['items'] : $selection;
return $widget_state;
}
protected static function setFieldState(array $element, FormStateInterface $form_state, array $field_state) {
static::setWidgetState($element['#field_parents'], $element['#field_name'], $form_state, $field_state);
}
}