class MediaLibrary in Media Library Form API Element 2.x
Same name and namespace in other branches
- 8 src/Element/MediaLibrary.php \Drupal\media_library_form_element\Element\MediaLibrary
Provides a Media library form element.
The #default_value accepted by this element is an ID of a media object.
@FormElement("media_library")
Usage can include the following components:
$element['image'] = [ '#type' => 'media_library', '#allowed_bundles' => ['image'], '#title' => t('Upload your image'), '#default_value' => NULL|'1'|'2,3,1', '#description' => t('Upload or select your profile image.'), '#cardinality' => -1|1, ];
Hierarchy
- class \Drupal\Component\Plugin\PluginBase implements DerivativeInspectionInterface, PluginInspectionInterface
- class \Drupal\Core\Plugin\PluginBase uses DependencySerializationTrait, MessengerTrait, StringTranslationTrait
- class \Drupal\Core\Render\Element\RenderElement implements ElementInterface
- class \Drupal\Core\Render\Element\FormElement implements FormElementInterface
- class \Drupal\media_library_form_element\Element\MediaLibrary
- class \Drupal\Core\Render\Element\FormElement implements FormElementInterface
- class \Drupal\Core\Render\Element\RenderElement implements ElementInterface
- class \Drupal\Core\Plugin\PluginBase uses DependencySerializationTrait, MessengerTrait, StringTranslationTrait
Expanded class hierarchy of MediaLibrary
File
- src/
Element/ MediaLibrary.php, line 33
Namespace
Drupal\media_library_form_element\ElementView source
class MediaLibrary extends FormElement {
/**
* Expand the media_library_element into it's required sub-elements.
*
* @param array $element
* The base form element render array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
* @param array $complete_form
* The complete form render array.
*
* @return array
* The form element render array.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public static function processMediaLibrary(array &$element, FormStateInterface $form_state, array &$complete_form) : array {
$default_value = NULL;
$referenced_entities = [];
if (!empty($element['#value'])) {
$default_value = $element['#value'];
}
if (!empty($default_value['media_selection_id'])) {
$entity_ids = [
$default_value['media_selection_id'],
];
}
else {
$entity_ids = array_filter(explode(',', $default_value));
}
if (!empty($entity_ids)) {
foreach ($entity_ids as $entity_id) {
$referenced_entities[] = \Drupal::entityTypeManager()
->getStorage('media')
->load($entity_id);
}
}
$view_builder = \Drupal::entityTypeManager()
->getViewBuilder('media');
$allowed_media_type_ids = $element['#allowed_bundles'];
$parents = $element['#parents'];
$field_name = array_pop($parents);
$attributes = $element['#attributes'] ?? [];
// Create an ID suffix from the parents to make sure each widget is unique.
$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,
]),
];
$element = array_merge($element, [
'#target_bundles' => !empty($allowed_media_type_ids) ? $allowed_media_type_ids : FALSE,
'#cardinality' => $element['#cardinality'] ?? 1,
'#attributes' => [
'id' => $wrapper_id,
'class' => [
'media-library-form-element',
],
],
'#modal_selector' => '#modal-media-library',
'#attached' => [
'library' => [
'media_library_form_element/media_library_form_element',
'media_library/view',
],
],
]);
if (empty($referenced_entities)) {
$element['empty_selection'] = [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => t('No media item selected.'),
'#attributes' => [
'class' => [
'media-library-form-element-empty-text',
],
],
];
}
$element['selection'] = [
'#type' => 'container',
'#attributes' => [
'class' => [
'js-media-library-selection',
'media-library-selection',
],
],
];
foreach ($referenced_entities as $delta => $referenced_entity) {
$element['selection'][$delta] = [
'#type' => 'container',
'#attributes' => [
'class' => [
'media-library-item',
'media-library-item--grid',
'js-media-library-item',
],
// Add the tabindex '-1' to allow the focus to be shifted to the next
// media item when an item is removed. We set focus to the container
// because we do not want to set focus to the remove button
// automatically.
// @see ::updateFormElement()
'tabindex' => '-1',
// Add a data attribute containing the delta to allow us to easily
// shift the focus to a specific media item.
// @see ::updateFormElement()
'data-media-library-item-delta' => $delta,
],
'preview' => [
'#type' => 'container',
'remove_button' => [
'#type' => 'submit',
'#name' => $field_name . '-' . $delta . '-media-library-remove-button' . $id_suffix,
'#value' => t('Remove'),
'#media_id' => $referenced_entity
->id(),
'#attributes' => [
'class' => [
'media-library-item__remove',
],
'aria-label' => t('Remove @label', [
'@label' => $referenced_entity
->label(),
]),
],
'#ajax' => [
'callback' => [
static::class,
'updateFormElement',
],
'wrapper' => $wrapper_id,
'progress' => [
'type' => 'throbber',
'message' => t('Removing @label.', [
'@label' => $referenced_entity
->label(),
]),
],
],
'#submit' => [
[
static::class,
'removeItem',
],
],
// Prevent errors in other widgets from preventing removal.
'#limit_validation_errors' => $limit_validation_errors,
],
// @todo Make the view mode configurable in https://www.drupal.org/project/drupal/issues/2971209
'rendered_entity' => $view_builder
->view($referenced_entity, 'media_library'),
'target_id' => [
'#type' => 'hidden',
'#value' => $referenced_entity
->id(),
],
],
'weight' => [
'#type' => 'number',
'#theme' => 'input__number__media_library_item_weight',
'#title' => \Drupal::translation()
->translate('Weight'),
'#default_value' => $delta,
'#attributes' => [
'class' => [
'js-media-library-item-weight',
],
],
],
];
}
$cardinality_unlimited = $element['#cardinality'] === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED;
$remaining = $element['#cardinality'] - count($referenced_entities);
// Inform the user of how many items are remaining.
if (!$cardinality_unlimited) {
if ($remaining) {
$cardinality_message = \Drupal::translation()
->formatPlural($remaining, 'One media item remaining.', '@count media items remaining.');
}
else {
$cardinality_message = \Drupal::translation()
->translate('The maximum number of media items have been selected.');
}
// Add a line break between the field message and the cardinality message.
if (!empty($element['#description'])) {
$element['#description'] .= '<br />' . $cardinality_message;
}
else {
$element['#description'] = $cardinality_message;
}
}
// Create a new media library URL with the correct state parameters.
$selected_type_id = reset($allowed_media_type_ids);
$remaining = $cardinality_unlimited ? FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED : $remaining;
// This particular media library opener needs some extra metadata for its
// \Drupal\media_library\MediaLibraryOpenerInterface::getSelectionResponse()
// to be able to target the element
// whose 'data-media-library-form-element-value'
// attribute is the same as $field_widget_id. The entity ID, entity type ID,
// bundle, field name are used for access checking.
$opener_parameters = [
'field_widget_id' => $field_widget_id,
'field_name' => $field_name,
];
$state = MediaLibraryState::create('media_library.opener.form_element', $allowed_media_type_ids, $selected_type_id, $remaining, $opener_parameters);
// Add a button that will load the Media library in a modal using AJAX.
$element['media_library_open_button'] = [
'#type' => 'button',
'#value' => t('Add media'),
'#name' => $field_name . '-media-library-open-button' . $id_suffix,
'#attributes' => [
'class' => [
'media-library-open-button',
'js-media-library-open-button',
],
// The jQuery UI dialog automatically moves focus to the first :tabbable
// element of the modal, so we need to disable refocus on the button.
'data-disable-refocus' => 'true',
],
'#media_library_state' => $state,
'#ajax' => [
'callback' => [
static::class,
'openMediaLibrary',
],
'progress' => [
'type' => 'throbber',
'message' => t('Opening media library.'),
],
],
// Allow the media library to be opened even if there are form errors.
'#limit_validation_errors' => [],
];
// When the user returns from the modal to the widget, we want to shift the
// focus back to the open button. If the user is not allowed to add more
// items, the button needs to be disabled. Since we can't shift the focus to
// disabled elements, the focus is set back to the open button via
// JavaScript by adding the 'data-disabled-focus' attribute.
// @see Drupal.behaviors.MediaLibraryWidgetDisableButton
if (!$cardinality_unlimited && $remaining === 0) {
$element['media_library_open_button']['#attributes']['data-disabled-focus'] = 'true';
$element['media_library_open_button']['#attributes']['class'][] = 'visually-hidden';
}
// This hidden field and button are used to add new items to the widget.
$element['media_library_selection'] = [
'#type' => 'hidden',
'#attributes' => array_merge([
'data-media-library-form-element-value' => $field_widget_id,
], $attributes),
'#default_value' => $element['#value'],
];
// When a selection is made this hidden button is pressed to add new media
// items based on the "media_library_selection" value.
$element['media_library_update_widget'] = [
'#type' => 'submit',
'#value' => t('Update widget'),
'#name' => $field_name . '-media-library-update' . $id_suffix,
'#ajax' => [
'callback' => [
static::class,
'updateFormElement',
],
'wrapper' => $wrapper_id,
'progress' => [
'type' => 'throbber',
'message' => t('Adding selection.'),
],
],
'#attributes' => [
'data-media-library-form-element-update' => $field_widget_id,
'class' => [
'js-hide',
],
],
'#validate' => [
[
static::class,
'validateItem',
],
],
'#submit' => [
[
static::class,
'updateItem',
],
],
// Prevent errors in other widgets from preventing updates.
// Exclude other validations in case there is no data yet.
'#limit_validation_errors' => !empty($referenced_entities) ? $limit_validation_errors : [],
];
return $element;
}
/**
* Extract the proper portion of our default_value.
*
* @param array $element
* The render element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state object.
* @param array $complete_form
* The complete form render array.
*/
public static function elementValidateMediaLibrary(array &$element, FormStateInterface $form_state, array &$complete_form) {
$value = NULL;
if (!empty($element['#value'])) {
$value = $element['#value'];
if (isset($value['media_library_selection'])) {
$value = $value['media_library_selection'];
}
}
$form_state
->setValueForElement($element, $value);
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
$value = NULL;
// Process the submission of our form element.
if ($input !== FALSE && $input !== NULL && isset($input['media_library_selection'])) {
$value = $input['media_library_selection'];
}
elseif ($input === FALSE) {
if (!empty($element['#default_value'])) {
// Remove the default value propery in case of AJAX removal.
if ($form_state
->isSubmitted() && end($form_state
->getTriggeringElement()['#parents']) === 'remove_button') {
$element['#default_value'] = NULL;
}
$value = $element['#default_value'];
}
}
if (!empty($value)) {
if (isset($value['target_id'])) {
$value = $value['target_id'];
}
// Normalize 0 value.
$value = $value === 0 ? '' : $value;
}
return $value;
}
/**
* AJAX callback to update the widget when the selection changes.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* An AJAX response to update the selection.
*/
public static function updateFormElement(array $form, FormStateInterface $form_state) : array {
$triggering_element = $form_state
->getTriggeringElement();
// This callback is either invoked from the remove button or the update
// button, which have different nesting levels.
$is_remove_button = end($triggering_element['#parents']) === 'remove_button';
$length = $is_remove_button ? -4 : -1;
if (count($triggering_element['#array_parents']) < abs($length)) {
throw new \LogicException('The element that triggered the form element update was at an unexpected depth. Triggering element parents were: ' . implode(',', $triggering_element['#array_parents']));
}
$parents = array_slice($triggering_element['#array_parents'], 0, $length);
return NestedArray::getValue($form, $parents);
}
/**
* Validates that newly selected items can be added to the widget.
*
* Making an invalid selection from the view should not be possible, but we
* still validate in case other selection methods (ex: upload) are valid.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public static function validateItem(array $form, FormStateInterface $form_state) {
$button = $form_state
->getTriggeringElement();
$element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
$media = static::getNewMediaItem($element, $form_state);
if (empty($media)) {
return;
}
// Validate that each selected media is of an allowed bundle.
$all_bundles = \Drupal::service('entity_type.bundle.info')
->getBundleInfo('media');
$bundle_labels = array_map(static function ($bundle) use ($all_bundles) {
return $all_bundles[$bundle]['label'];
}, $element['#target_bundles']);
if ($element['#target_bundles'] && !in_array($media
->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
->label(),
'@types' => implode(', ', $bundle_labels),
]));
}
}
/**
* Gets newly selected media item.
*
* @param array $element
* The wrapping element for this widget.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return array
* An array selected media item.
*/
protected static function getNewMediaItem(array $element, FormStateInterface $form_state) {
// Get the new media IDs passed to our hidden button.
$values = $form_state
->getValues();
$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)) {
/** @var \Drupal\media\MediaInterface[] $media */
return Media::loadMultiple($ids);
}
}
return [];
}
/**
* Flags the form for rebuild.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public static function updateItem(array $form, FormStateInterface $form_state) {
$form_state
->setRebuild();
}
/**
* Submit callback for remove buttons.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return mixed
* The updated form element.
*/
public static function removeItem(array &$form, FormStateInterface $form_state) {
$triggering_element = $form_state
->getTriggeringElement();
// Get the parents required to find the top-level widget element.
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, -4);
$element = NestedArray::getValue($form, $parents);
$user_input_parents = array_slice($triggering_element['#parents'], 0, -4);
$delta = array_slice($triggering_element['#array_parents'], -3, 1)[0];
if (isset($element['selection'][$delta])) {
unset($element['selection'][$delta]);
$items = array_filter($element['selection'], static function ($k) {
return is_numeric($k);
}, ARRAY_FILTER_USE_KEY);
$remaining_items = [];
foreach ($items as $item) {
/** @var \Drupal\media\Entity\Media $media_item */
$media_item = $item['preview']['rendered_entity']['#media'];
if ($media_item instanceof Media) {
$remaining_items[] = $media_item
->id();
}
}
$selection = implode(',', $remaining_items);
// Remove our value.
$element['media_library_selection']['#value'] = $selection;
$element['media_library_selection']['#default_value'] = $selection;
$element['#value'] = $selection;
$element['#default_value'] = $selection;
// Clear the formstate values.
$form_state
->setValueForElement($element, $selection);
// Clear formstate user input.
$user_input = $form_state
->getUserInput();
NestedArray::setValue($user_input, $user_input_parents, $selection);
$form_state
->setUserInput($user_input);
$form_state
->setValue($user_input_parents, $selection);
}
// Refresh the form element.
$form_state
->setRebuild();
}
/**
* AJAX callback to open the library modal.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response to open the media library.
*/
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, NULL, '#modal-media-library'));
}
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#input' => TRUE,
'#tree' => TRUE,
'#cardinality' => 1,
'#allowed_bundles' => [],
'#process' => [
[
$class,
'processAjaxForm',
],
[
$class,
'processMediaLibrary',
],
[
$class,
'processGroup',
],
],
'#pre_render' => [
[
$class,
'preRenderGroup',
],
],
'#element_validate' => [
[
$class,
'elementValidateMediaLibrary',
],
],
'#theme' => 'media_library_element',
];
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
DependencySerializationTrait:: |
protected | property | ||
DependencySerializationTrait:: |
protected | property | ||
DependencySerializationTrait:: |
public | function | 2 | |
DependencySerializationTrait:: |
public | function | 2 | |
FormElement:: |
public static | function | Adds autocomplete functionality to elements. | |
FormElement:: |
public static | function | #process callback for #pattern form element property. | |
FormElement:: |
public static | function | #element_validate callback for #pattern form element property. | |
MediaLibrary:: |
public static | function | Extract the proper portion of our default_value. | |
MediaLibrary:: |
public | function |
Returns the element properties for this element. Overrides ElementInterface:: |
|
MediaLibrary:: |
protected static | function | Gets newly selected media item. | |
MediaLibrary:: |
public static | function | AJAX callback to open the library modal. | |
MediaLibrary:: |
public static | function | Expand the media_library_element into it's required sub-elements. | |
MediaLibrary:: |
public static | function | Submit callback for remove buttons. | |
MediaLibrary:: |
public static | function | AJAX callback to update the widget when the selection changes. | |
MediaLibrary:: |
public static | function | Flags the form for rebuild. | |
MediaLibrary:: |
public static | function | Validates that newly selected items can be added to the widget. | |
MediaLibrary:: |
public static | function |
Determines how user input is mapped to an element's #value property. Overrides FormElement:: |
|
MessengerTrait:: |
protected | property | The messenger. | 27 |
MessengerTrait:: |
public | function | Gets the messenger. | 27 |
MessengerTrait:: |
public | function | Sets the messenger. | |
PluginBase:: |
protected | property | Configuration information passed into the plugin. | 1 |
PluginBase:: |
protected | property | The plugin implementation definition. | 1 |
PluginBase:: |
protected | property | The plugin_id. | |
PluginBase:: |
constant | A string which is used to separate base plugin IDs from the derivative ID. | ||
PluginBase:: |
public | function |
Gets the base_plugin_id of the plugin instance. Overrides DerivativeInspectionInterface:: |
|
PluginBase:: |
public | function |
Gets the derivative_id of the plugin instance. Overrides DerivativeInspectionInterface:: |
|
PluginBase:: |
public | function |
Gets the definition of the plugin implementation. Overrides PluginInspectionInterface:: |
2 |
PluginBase:: |
public | function |
Gets the plugin_id of the plugin instance. Overrides PluginInspectionInterface:: |
|
PluginBase:: |
public | function | Determines if the plugin is configurable. | |
PluginBase:: |
public | function | Constructs a \Drupal\Component\Plugin\PluginBase object. | 98 |
RenderElement:: |
public static | function | Adds Ajax information about an element to communicate with JavaScript. | |
RenderElement:: |
public static | function | Adds members of this group as actual elements for rendering. | |
RenderElement:: |
public static | function | Form element processing handler for the #ajax form property. | 1 |
RenderElement:: |
public static | function | Arranges elements into groups. | |
RenderElement:: |
public static | function |
Sets a form element's class attribute. Overrides ElementInterface:: |
|
StringTranslationTrait:: |
protected | property | The string translation service. | 4 |
StringTranslationTrait:: |
protected | function | Formats a string containing a count of items. | |
StringTranslationTrait:: |
protected | function | Returns the number of plurals supported by a given language. | |
StringTranslationTrait:: |
protected | function | Gets the string translation service. | |
StringTranslationTrait:: |
public | function | Sets the string translation service to use. | 2 |
StringTranslationTrait:: |
protected | function | Translates a string to the current language or to a given language. |