inline_media_form-1.0.0-beta1/src/Plugin/Field/FieldWidget/InlineMediaFormWidget.php
src/Plugin/Field/FieldWidget/InlineMediaFormWidget.php
<?php
namespace Drupal\inline_media_form\Plugin\Field\FieldWidget;
use Drupal\Component\Datetime\Time;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\content_translation\ContentTranslationManagerInterface;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AnnounceCommand;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldFilteredMarkup;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Field\WidgetInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\TypedData\TranslationStatusInterface;
use Drupal\field_group\FormatterHelper;
use Drupal\inline_media_form\MediaItemAdapter;
use Drupal\inline_media_form\MediaItemAdapterInterface;
use Drupal\media\Entity\Media;
use Drupal\media\MediaInterface;
use Drupal\media_library\MediaLibraryState;
use Drupal\media_library\MediaLibraryUiBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\ConstraintViolationListInterface;
/**
* Custom entity browser and edit widget for media file Entity Reference fields.
*
* @FieldWidget(
* id = "inline_media_form",
* label = @Translation("Inline media form"),
* description = @Translation("Uses entity browser to select entities, and then provides a way to edit selected media without leaving the entity form."),
* field_types = {
* "entity_reference"
* }
* )
*/
class InlineMediaFormWidget extends WidgetBase {
// ===========================================================================
// Constants
// ===========================================================================
/**
* Action position is in the add media files place.
*/
const ACTION_POSITION_BASE = 1;
/**
* Action position is in the table header section.
*/
const ACTION_POSITION_HEADER = 2;
/**
* Action position is in the toolbar sub-component of table header section.
*/
const ACTION_POSITION_HEADER_TOOLBAR = 3;
/**
* Action position is a single media file row in the widget.
*/
const ACTION_POSITION_ITEM_ROW = 4;
/**
* Edit more for a widget that is open for editing.
*/
const EDIT_MODE_OPEN = 'open';
/**
* Edit more for a widget that is closed and not actively editable.
*/
const EDIT_MODE_CLOSED = 'closed';
/**
* Edit more for a widget that has been removed/is hidden from view.
*/
const EDIT_MODE_REMOVED = 'removed';
/**
* Option value used when auto-collapse is disabled.
*/
const AUTO_COLLAPSE_DISABLED = 'no';
/**
* Option value used when auto-collapse is enabled.
*/
const AUTO_COLLAPSE_ENABLED = 'yes';
/**
* Feature for when "Collapse all" and "Enable all" are available.
*/
const FEATURE_COLLAPSE_EDIT_ALL = 'collapse_edit_all';
// ===========================================================================
// Member Fields
// ===========================================================================
/**
* Indicates whether the current widget instance is in translation.
*
* @var bool
*/
protected $isTranslating;
/**
* ID to name ajax buttons that includes field parents and field name.
*
* @var string
*/
protected $fieldIdPrefix;
/**
* Wrapper id to identify the media files.
*
* @var string
*/
protected $fieldWrapperId;
/**
* Number of media files item on form.
*
* @var int
*/
protected $realItemCount;
/**
* Parents for the current media file.
*
* @var array
*/
protected $fieldParents;
/**
* The Drupal module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The manager of all entity type information.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Information on the different bundles of each entity type.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityBundleInfo;
/**
* The repository of entity display information.
*
* @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
*/
protected $entityDisplayRepository;
/**
* The entity reference selection plugin manager.
*
* This manages plug-ins that provide users with a way to select which
* entity or entities should be the target of an entity reference field.
*
* @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface
*/
protected $selectionManager;
/**
* The content translation management interface.
*
* @var \Drupal\content_translation\ContentTranslationManagerInterface
*/
protected $translationManager;
/**
* The logged-in user.
*
* @var \Drupal\Core\Session\AccountProxy
*/
protected $user;
/**
* The time service.
*
* @var \Drupal\Component\Datetime\Time
*/
protected $time;
// ===========================================================================
// IoC Constructor
// ===========================================================================
/**
* {@inheritDoc}
*/
public static function create(ContainerInterface $container,
array $configuration,
$plugin_id,
$plugin_definition): WidgetInterface {
/** @var \Drupal\Core\Extension\ModuleHandlerInterface $module_handler */
$module_handler = $container->get('module_handler');
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
$entity_type_manager = $container->get('entity_type.manager');
/** @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_bundle_info */
$entity_bundle_info = $container->get('entity_type.bundle.info');
/** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository */
$entity_display_repository = $container->get('entity_display.repository');
/** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager */
$selection_manager =
$container->get('plugin.manager.entity_reference_selection');
/** @var \Drupal\content_translation\ContentTranslationManagerInterface $translation_manager */
if ($module_handler->moduleExists('content_translation')) {
$translation_manager = $container->get('content_translation.manager');
}
else {
$translation_manager = NULL;
}
/** @var \Drupal\Core\Session\AccountProxyInterface $user */
$user = $container->get('current_user');
/** @var \Drupal\Component\Datetime\Time $time */
$time = $container->get('datetime.time');
return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['third_party_settings'],
$module_handler,
$entity_type_manager,
$entity_bundle_info,
$entity_display_repository,
$selection_manager,
$translation_manager,
$user,
$time
);
}
// ===========================================================================
// Widget Callbacks
// ===========================================================================
/**
* {@inheritdoc}
*/
public static function isApplicable(
FieldDefinitionInterface $field_definition): bool {
$target_type_id = $field_definition->getSetting('target_type');
try {
$target_type =
\Drupal::entityTypeManager()->getDefinition($target_type_id);
return $target_type->entityClassImplements(MediaInterface::class);
}
catch (PluginNotFoundException $ex) {
watchdog_exception(
'inline_media_form',
$ex,
'Failed to retrieve type definition for target entity type %entity_type - %type: @message in %function (line %line of %file).',
['%entity_type' => $target_type_id]
);
return FALSE;
}
}
// ===========================================================================
// AJAX Callbacks
// ===========================================================================
/**
* Dispatches and renders the result of opening the media library modal.
*
* @param array $form
* The entity edit form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the entity edit form.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response to open the media library.
*
* @noinspection PhpUnusedParameterInspection
*/
public static function handleOpenMediaLibraryAjax(
array $form,
FormStateInterface $form_state): AjaxResponse {
$triggering_element = $form_state->getTriggeringElement();
$opener_state = $triggering_element['#media_library_opener_state'];
$library_state = self::buildMediaLibraryState(
$opener_state,
$form['#build_id'],
);
$library_ui =
\Drupal::service('media_library.ui_builder')
->buildUi($library_state);
$dialog_options = MediaLibraryUiBuilder::dialogOptions();
$response = new AjaxResponse();
$response->addCommand(
new OpenModalDialogCommand(
$dialog_options['title'],
$library_ui,
$dialog_options
)
);
return $response;
}
/**
* AJAX callback to update the widget when additional, new media are selected.
*
* @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 update the selection.
*/
public static function handleMediaSelectedAjax(
array $form,
FormStateInterface $form_state): AjaxResponse {
$submit = static::getSubmitElementInfo($form, $form_state);
$wrapper_id = $submit['button']['#ajax']['wrapper'] ?? '';
$element = $submit['element'];
// Clear hidden textfield selection to reduce how often we have to filter
// out duplicate IDs if the user re-opens the media browser.
$element['add_more']['media_library_selection']['#value'] = '';
$response = new AjaxResponse();
$response->addCommand(new ReplaceCommand("#$wrapper_id", $element));
$new_items = static::getNewMediaItems($element, $form_state);
$new_count = count($new_items);
$label = $element['#reference_title'];
$plural_label = $element['#reference_title_plural'];
$announcement =
\Drupal::translation()->formatPlural(
$new_count,
'Added one @title.',
'Added @count @title_plural.',
[
'@title' => $label,
'@title_plural' => $plural_label,
]
);
$response->addCommand(new AnnounceCommand($announcement));
return $response;
}
/**
* Dispatches and renders the result of AJAX actions that affect all deltas.
*
* In other words, this returns an updated copy of the entire widget to the
* browser.
*
* @param array $form
* The entity edit form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the entity edit form.
*
* @return array
* The form element to render back to the client.
*/
public static function handleAllItemAjax(
array $form,
FormStateInterface $form_state): array {
$submit =
static::getSubmitElementInfo(
$form,
$form_state,
static::ACTION_POSITION_HEADER
);
return $submit['element'];
}
/**
* Dispatches and renders the result of toolbar actions like bulk rename.
*
* Returns an updated copy of the entire widget to the browser.
*
* @param array $form
* The entity edit form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the entity edit form.
*
* @return array
* The form element to render back to the client.
*/
public static function handleToolbarAjax(
array $form,
FormStateInterface $form_state): array {
$submit =
static::getSubmitElementInfo(
$form,
$form_state,
static::ACTION_POSITION_HEADER_TOOLBAR
);
return $submit['element'];
}
/**
* Dispatches and renders the result of AJAX actions that affect one delta.
*
* @param array $form
* The entity edit form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the entity edit form.
*
* @return array
* The form element to render back to the client.
*/
public static function handleSingleItemAjax(array $form,
FormStateInterface $form_state): array {
$submit =
static::getSubmitElementInfo(
$form,
$form_state,
static::ACTION_POSITION_ITEM_ROW
);
$element = $submit['element'];
$element['#prefix'] =
'<div class="ajax-new-content">' . ($element['#prefix'] ?? '');
$element['#suffix'] =
($element['#suffix'] ?? '') . '</div>';
return $element;
}
// ===========================================================================
// Submit and Form Process Callbacks
// ===========================================================================
/**
* Value callback for optimizing storage of media type weights.
*
* The tabledrag functionality needs a specific weight field, but we don't
* want to store this extra weight field in our settings.
*
* Adapted from
* \Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget::setMediaTypesValue().
*
* @param array $element
* An associative array containing the properties of the element.
* @param array|bool $input
* Either the incoming input to populate the form element; or FALSE if the
* element's default value should be returned.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return mixed
* The value to assign to the element.
*/
public static function storeMediaTabWeights(array &$element,
$input,
FormStateInterface $form_state) {
$value = $element['#default_value'] ?? [];
if (!empty($input)) {
// Sort media types by 'weight' value.
uasort($input, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
// Update form state with sorted values.
$sorted_media_type_ids = array_keys($input);
$form_state->setValue($element['#parents'], $sorted_media_type_ids);
// Unset child elements containing weights for each media type to avoid
// FormBuilder::doBuildForm() processing and storing the weight fields as
// well.
foreach ($sorted_media_type_ids as $media_type_id) {
unset($element[$media_type_id]);
}
$value = $sorted_media_type_ids;
}
return $value;
}
/**
* 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 validateSelectedMediaItems(
array $form,
FormStateInterface $form_state): void {
$submit = static::getSubmitElementInfo($form, $form_state);
$widget_state = $submit['widget_state'];
$element = $submit['element'];
$new_media = static::getNewMediaItems($element, $form_state);
if (empty($new_media)) {
return;
}
$cardinality = $element['#cardinality'];
$target_bundles = $element['#target_bundles'];
$is_cardinality_unlimited =
($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
$current_item_count = $widget_state['items_count'];
$total_media = $current_item_count + count($new_media);
if (!$is_cardinality_unlimited && ($total_media > $cardinality)) {
$form_state->setError(
$element,
\Drupal::translation()->formatPlural(
$cardinality,
'Only one media file can be selected.',
'Only @count media files can be selected.'
)
);
}
if (!empty($target_bundles)) {
$allowed_media_type_ids = array_keys($target_bundles);
foreach ($new_media as $media_item) {
if (!in_array($media_item->bundle(), $allowed_media_type_ids, TRUE)) {
$label = $element['#reference_title'];
$form_state->setError(
$element,
t('The new @title "@label" is not of an accepted type. Allowed types: @types',
[
'@title' => $label,
'@label' => $media_item->label(),
'@types' => implode(', ', array_values($target_bundles)),
]
)
);
}
}
}
}
/**
* Submit callback for adding selected media to the widget.
*
* @param array $form
* The entity edit form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the entity edit form.
*/
public static function addSelectedMediaItemSubmit(
array $form,
FormStateInterface $form_state): void {
$submit = static::getSubmitElementInfo($form, $form_state);
$widget_state = $submit['widget_state'];
$element = $submit['element'];
$new_media = static::getNewMediaItems($element, $form_state);
if (empty($new_media)) {
return;
}
$field_path =
array_merge($element['#field_parents'], [$element['#field_name']]);
$max_delta = $element['#max_delta'];
$new_delta = $max_delta + 1;
$new_deltas = [];
foreach ($new_media as $media_entity) {
// Any ID can be passed to the widget, so we have to check access.
if (!$media_entity->access('view')) {
continue;
}
static::prepareDeltaPosition(
$widget_state,
$form_state,
$field_path,
$new_delta
);
$wrapped_media = MediaItemAdapter::create($media_entity);
$widget_delta_state =& $widget_state['media'][$new_delta];
$widget_delta_state['wrapper'] = $wrapped_media;
$widget_delta_state['entity'] = $wrapped_media->toMediaEntity();
$new_deltas[] = $new_delta;
++$new_delta;
}
$widget_state = static::applyAutoCollapse($widget_state);
// Ensure all the new files start out open for editing after the
// auto-collapse.
foreach ($new_deltas as $new_delta) {
$widget_state['media'][$new_delta]['mode'] = self::EDIT_MODE_OPEN;
}
static::setWidgetState(
$submit['parents'],
$submit['field_name'],
$form_state,
$widget_state
);
$form_state->setRebuild();
}
/**
* Submit callback for performing an action on a single media file item.
*
* @param array $form
* The entity edit form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the entity edit form.
*/
public static function mediaFileItemSubmit(
array $form,
FormStateInterface $form_state): void {
$submit =
static::getSubmitElementInfo(
$form,
$form_state,
static::ACTION_POSITION_ITEM_ROW
);
$button = $submit['button'];
$widget_state = $submit['widget_state'];
$delta = $submit['delta'];
$new_mode = $button['#edit_mode'];
if ($new_mode === self::EDIT_MODE_OPEN) {
$widget_state = static::applyAutoCollapse($widget_state);
// Close/hide bulk rename toolbar.
$widget_state['show_bulk_rename_options'] = FALSE;
}
$widget_delta_state =& $widget_state['media'][$delta];
$widget_delta_state['mode'] = $new_mode;
static::setWidgetState(
$submit['parents'],
$submit['field_name'],
$form_state,
$widget_state
);
$form_state->setRebuild();
}
/**
* Submit callback for toggling all media files into or out of an edit mode.
*
* @param array $form
* The entity edit form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the entity edit form.
*/
public static function changeAllEditModeSubmit(
array $form,
FormStateInterface $form_state): void {
$submit =
static::getSubmitElementInfo(
$form,
$form_state,
static::ACTION_POSITION_HEADER
);
// Activate edit mode (or deactivate if already activated)
$widget_state = $submit['widget_state'];
foreach ($widget_state['media'] as &$widget_delta_state) {
if ($widget_delta_state['mode'] === self::EDIT_MODE_REMOVED) {
// Skip deltas we've marked for deletion.
continue;
}
$widget_delta_state['mode'] = $submit['button']['#edit_mode'];
}
// Close/hide bulk rename toolbar.
$widget_state['show_bulk_rename_options'] = FALSE;
static::setWidgetState(
$submit['parents'],
$submit['field_name'],
$form_state,
$widget_state
);
$form_state->setRebuild();
}
/**
* Submit callback for the button that shows the bulk rename toolbar.
*
* @param array $form
* The entity edit form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the entity edit form.
*/
public static function showBulkRenameToolbarSubmit(
array $form,
FormStateInterface $form_state): void {
$submit =
static::getSubmitElementInfo(
$form,
$form_state,
static::ACTION_POSITION_HEADER
);
$widget_state = $submit['widget_state'];
$widget_state['show_bulk_rename_options'] = TRUE;
static::setWidgetState(
$submit['parents'],
$submit['field_name'],
$form_state,
$widget_state
);
$form_state->setRebuild();
}
/**
* Submit callback for the button that hides the bulk rename toolbar.
*
* @param array $form
* The entity edit form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the entity edit form.
*/
public static function hideBulkRenameToolbarSubmit(
array $form,
FormStateInterface $form_state): void {
$submit =
static::getSubmitElementInfo(
$form,
$form_state,
static::ACTION_POSITION_HEADER_TOOLBAR
);
$widget_state = $submit['widget_state'];
$widget_state['show_bulk_rename_options'] = FALSE;
static::setWidgetState(
$submit['parents'],
$submit['field_name'],
$form_state,
$widget_state
);
$form_state->setRebuild();
}
/**
* Submit callback for applying bulk renames to all media files.
*
* @param array $form
* The entity edit form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the entity edit form.
*/
public static function bulkRenameAllMediaSubmit(
array $form,
FormStateInterface $form_state): void {
$submit =
static::getSubmitElementInfo(
$form,
$form_state,
static::ACTION_POSITION_HEADER_TOOLBAR
);
$widget_state = $submit['widget_state'];
$button = $submit['button'];
assert(isset($button['#parents']));
// Get our parents relative to the "Rename" button that was clicked, since
// it's part of the same toolbar.
$toolbar_parents = array_slice($button['#parents'], 0, -1);
$raw_input = $form_state->getUserInput();
$raw_toolbar_inputs = NestedArray::getValue($raw_input, $toolbar_parents);
if (isset($raw_toolbar_inputs['name'])) {
$raw_name = $raw_toolbar_inputs['name'];
$is_sequential = $raw_toolbar_inputs['number_sequentially'];
// Ensure that we escape any HTML or unsafe markup. The trim ensures that
// leading or trailing spaces in the name are not preserved.
$trimmed_name = trim($raw_name);
// Number of digits to pad up to (minimum length of 2, so 01, 02, etc.).
$num_digits = max(2, (int) log10(count($widget_state['media'])) + 1);
$file_index = 1;
foreach ($widget_state['media'] as &$widget_delta_state) {
$media_entity = $widget_delta_state['entity'];
$item_mode = $widget_delta_state['mode'] ?? NULL;
if ($item_mode === self::EDIT_MODE_REMOVED) {
continue;
}
if ($is_sequential) {
// Add a padded counter to the end of each media file, renaming it to
// be "Name 001", "Name 002", etc.
$padded_index = str_pad($file_index, $num_digits, '0', STR_PAD_LEFT);
// This trim ensures we don't end up with leading spaces in case the
// name is blank. (Unlikely, but it could happen if requirements for
// the form change).
$new_name = ltrim(sprintf('%s %s', $trimmed_name, $padded_index));
}
else {
// Rename each media file to be "Name", "Name", etc.
$new_name = $trimmed_name;
}
$media_entity->set('name', $new_name);
$file_index++;
}
}
// Close the bulk rename widget, since it's no longer needed.
$widget_state['show_bulk_rename_options'] = FALSE;
static::setWidgetState(
$submit['parents'],
$submit['field_name'],
$form_state,
$widget_state
);
$form_state->setRebuild();
}
/**
* After-build callback for adding the translatability clue from the widget.
*
* Adds a clue about the form element translatability. This replicates the
* logic of ContentTranslationHandler::addTranslatabilityClue().
*
* If the given element does not have a #title attribute, the function is
* recursively applied to child elements.
*
* @param array $element
* The form element to which the clue needs to be added.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The state of the form containing the element.
*
* @return array
* The form element with the translatability clue added.
*/
public static function addTranslatabilityClue(
array $element,
FormStateInterface $form_state): array {
static $suffix,
$fapi_title_elements;
// Widgets could have multiple elements with their own titles, so remove the
// suffix if it exists, do not recurse lower than this to avoid going into
// nested media or similar nested field types.
// Elements which can have a #title attribute according to FAPI Reference.
if (!isset($suffix)) {
$suffix =
sprintf(
' <span class="translation-entity-all-languages">(%s)</span>',
t('all languages')
);
$fapi_title_elements = [
'checkbox',
'checkboxes',
'date',
'details',
'fieldset',
'file',
'item',
'password',
'password_confirm',
'radio',
'radios',
'select',
'textarea',
'textfield',
'weight',
];
}
$element_type = $element['#type'] ?? NULL;
// Update #title attribute for all elements that are allowed to have a
// #title attribute according to the Form API Reference. The reason for this
// check is because some elements have a #title attribute even though it is
// not rendered; for instance, field containers.
if (($element_type !== NULL) &&
in_array($element_type, $fapi_title_elements) &&
!empty($element['#title'])) {
$element['#title'] .= $suffix;
}
// If the current element does not have a (valid) title, try child elements.
elseif ($children = Element::children($element)) {
foreach ($children as $delta) {
$element[$delta] =
static::addTranslatabilityClue($element[$delta], $form_state);
}
}
// If there are no children, fall back to the current #title attribute if it
// exists.
elseif (isset($element['#title'])) {
$element['#title'] .= $suffix;
}
return $element;
}
/**
* Form process callback for adding submit callbacks to entity forms.
*
* This is necessary because entity forms don't respond to adding '#submit'
* callbacks at the top level -- they have to be added to the submit button
* itself. But, submit buttons/actions on entity forms are not part of the
* form at the time the IMF widget is being constructed. So, we have to
* register a process callback so we can act on the mostly complete form.
*
* @param array $form
* The render array for the entity edit form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The state of the edit form.
*
* @return array
* The modified form render array.
*
* @noinspection PhpUnusedParameterInspection
*/
public static function entityFormProcess(
array $form,
FormStateInterface $form_state): array {
$have_submit = FALSE;
// Apply to all actions that save the node form, not just the save button.
// This is necessary for compatibility with modules like "Create and
// Continue", "Add Another", "Entity Save and Add Another", etc.
foreach (Element::children($form['actions']) as $action_key) {
$action = $form['actions'][$action_key];
$action_type = $action['#type'] ?? NULL;
$submit_callbacks = $action['#submit'] ?? [];
if (($action_type == 'submit') && in_array('::save', $submit_callbacks)) {
array_unshift(
$form['actions'][$action_key]['#submit'],
[static::class, 'saveModifiedMedia']
);
// Sanity check for assertions.
$have_submit = TRUE;
}
}
assert($have_submit, 'This entity form is missing a submit button.');
return $form;
}
/**
* Saves all media files that have been modified by this widget.
*
* @param array $form
* The entity edit form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The state of the edit form.
*/
public static function saveModifiedMedia(
array $form,
FormStateInterface $form_state): void {
$imf_fields = $form_state->get('inline_media_form_fields');
foreach ($imf_fields as $imf_field) {
$widget_state =
static::getWidgetState(
$imf_field['parents'],
$imf_field['field_name'],
$form_state
);
assert(!empty($widget_state));
foreach ($widget_state['media'] as $widget_delta_state) {
$wrapped_media = $widget_delta_state['wrapper'];
assert($wrapped_media instanceof MediaItemAdapterInterface);
try {
$wrapped_media->saveIfNecessary();
}
catch (EntityStorageException $ex) {
$media_id = $wrapped_media->toMediaEntity()->id();
$form_state->setError(
$form,
t('Unable to save modified media item %id: @error',
[
'%id' => $media_id,
'@error' => $ex->getMessage(),
]
)
);
watchdog_exception(
'inline_media_form',
$ex,
'Unable to save media item %id - %type: @message in %function (line %line of %file).',
['%id' => $media_id]
);
}
}
}
}
// ===========================================================================
// Static Utility Methods
// ===========================================================================
/**
* Gets newly selected media items.
*
* Adapted from MediaLibraryWidget::getNewMediaItems.
*
* @param array $element
* The wrapping element for this widget.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return \Drupal\media\MediaInterface[]
* An array of selected media items.
*
* @see \Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget::getNewMediaItems
*/
protected static function getNewMediaItems(
array $element,
FormStateInterface $form_state): array {
$new_items = [];
// Get the new media IDs passed to our hidden button. We need to use the
// actual user input, since when #limit_validation_errors is used, the
// unvalidated user input is not added to the form state.
// @see FormValidator::handleErrorsWithLimitedValidation()
$values = $form_state->getUserInput();
$path = array_merge($element['#parents'], ['add_more']);
$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 */
$new_items = Media::loadMultiple($ids);
}
}
return $new_items;
}
/**
* Extracts common submit element info during an AJAX submit callback.
*
* @param array $form
* The edit form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the edit form.
* @param int $position
* The relative location within the form of the triggering element.
*
* @return array
* Submit element information.
*/
public static function getSubmitElementInfo(
array $form,
FormStateInterface $form_state,
$position = InlineMediaFormWidget::ACTION_POSITION_BASE): array {
$submit = [
'button' => $form_state->getTriggeringElement(),
];
// Go up in the form, to the widgets container.
$submit_parents = $submit['button']['#array_parents'];
if ($position === static::ACTION_POSITION_BASE) {
$element =
NestedArray::getValue($form, array_slice($submit_parents, 0, -2));
}
elseif ($position === static::ACTION_POSITION_HEADER) {
$element =
NestedArray::getValue($form, array_slice($submit_parents, 0, -3));
}
elseif ($position === static::ACTION_POSITION_HEADER_TOOLBAR) {
$element =
NestedArray::getValue($form, array_slice($submit_parents, 0, -4));
}
elseif ($position === static::ACTION_POSITION_ITEM_ROW) {
$element =
NestedArray::getValue($form, array_slice($submit_parents, 0, -5));
$delta = array_slice($submit_parents, -5, -4);
$submit['delta'] = $delta[0];
}
else {
throw new \InvalidArgumentException("Invalid position: ${position}");
}
$submit['element'] = $element;
$submit['field_name'] = $element['#field_name'];
$submit['parents'] = $element['#field_parents'];
// Get widget state.
$submit['widget_state'] =
static::getWidgetState(
$submit['parents'],
$submit['field_name'],
$form_state
);
assert(!empty($submit['widget_state']));
return $submit;
}
/**
* Automatically collapses any open media files, if auto-collapse is enabled.
*
* @param array $widget_state
* The current widget state.
*
* @return array
* The altered widget state, with appropriate media files collapsed.
*/
public static function applyAutoCollapse(array $widget_state): array {
$real_item_count = $widget_state['real_item_count'];
$auto_collapse = $widget_state['auto_collapse'];
$auto_collapse_threshold = $widget_state['auto_collapse_threshold'];
$auto_collapse_enabled = ($auto_collapse !== self::AUTO_COLLAPSE_DISABLED);
if ($auto_collapse_enabled &&
($real_item_count > $auto_collapse_threshold)) {
foreach ($widget_state['media'] as &$widget_delta_state) {
$mode = $widget_delta_state['mode'] ?? NULL;
if ($mode === self::EDIT_MODE_OPEN) {
$widget_delta_state['mode'] = self::EDIT_MODE_CLOSED;
}
}
}
return $widget_state;
}
// ===========================================================================
// Constructor
// ===========================================================================
/**
* Constructs a MediaFileSelectorWidget object.
*
* @param string $plugin_id
* The plugin_id for the widget.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The definition of the field to which the widget is associated.
* @param array $settings
* The widget settings.
* @param array $third_party_settings
* Any third party settings.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The Drupal module handler.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The manager of all entity type information.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_bundle_info
* Information on the different bundles of each entity type.
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
* The repository of entity display information.
* @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_manager
* The manager of plug-ins that provide the user with a way to select which
* entities should be the target of an entity reference field.
* @param \Drupal\content_translation\ContentTranslationManagerInterface|null $translation_manager
* The content translation management interface; or, NULL if translation
* support is not enabled on the site.
* @param \Drupal\Core\Session\AccountProxyInterface $user
* The logged-in user.
* @param \Drupal\Component\Datetime\Time $time
* The time service.
*/
public function __construct(
string $plugin_id,
$plugin_definition,
FieldDefinitionInterface $field_definition,
array $settings,
array $third_party_settings,
ModuleHandlerInterface $module_handler,
EntityTypeManagerInterface $entity_type_manager,
EntityTypeBundleInfoInterface $entity_bundle_info,
EntityDisplayRepositoryInterface $entity_display_repository,
SelectionPluginManagerInterface $selection_manager,
?ContentTranslationManagerInterface $translation_manager,
AccountProxyInterface $user,
Time $time) {
parent::__construct(
$plugin_id,
$plugin_definition,
$field_definition,
$settings,
$third_party_settings
);
$this->moduleHandler = $module_handler;
$this->entityTypeManager = $entity_type_manager;
$this->entityBundleInfo = $entity_bundle_info;
$this->entityDisplayRepository = $entity_display_repository;
$this->selectionManager = $selection_manager;
$this->translationManager = $translation_manager;
$this->user = $user;
$this->time = $time;
}
// ===========================================================================
// Public Methods
// ===========================================================================
/**
* {@inheritdoc}
*/
public static function defaultSettings(): array {
return [
'title' => t('media file'),
'title_plural' => t('media files'),
'initial_mode' => self::EDIT_MODE_CLOSED,
'auto_collapse' => self::AUTO_COLLAPSE_DISABLED,
'auto_collapse_threshold' => 0,
'preview_display_mode' => 'inline_media_form',
'form_display_mode' => 'inline_media_form',
'library_view_display' => 'media_library.widget',
'library_tab_order' => [],
'features' => [
self::FEATURE_COLLAPSE_EDIT_ALL => self::FEATURE_COLLAPSE_EDIT_ALL,
],
];
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form,
FormStateInterface $form_state): array {
$field_name = $this->fieldDefinition->getName();
$elements = [];
$elements['title'] = [
'#type' => 'textfield',
'#title' => $this->t('Title text'),
'#description' => $this->t('Label to appear as title on the button as "Add new [title]". This label is translatable.'),
'#default_value' => $this->getSetting('title'),
'#required' => TRUE,
];
$elements['title_plural'] = [
'#type' => 'textfield',
'#title' => $this->t('Plural title text'),
'#description' => $this->t('Label in its plural form.'),
'#default_value' => $this->getSetting('title_plural'),
'#required' => TRUE,
];
$elements['initial_mode'] = [
'#type' => 'select',
'#title' => $this->t('Initial edit mode'),
'#description' => $this->t('How each media file initially appears when the edit form is first opened.'),
'#options' => $this->getSettingOptions('initial_mode'),
'#default_value' => $this->getSetting('initial_mode'),
'#required' => TRUE,
];
$edit_mode_selector =
'select[name="fields[' . $field_name . '][settings_edit_form][settings][edit_mode]"]';
$elements['auto_collapse'] = [
'#type' => 'select',
'#title' => $this->t('Auto-collapse media files'),
'#description' => $this->t('If a media file opened for editing should be collapsed when starting to edit a different media file.'),
'#options' => $this->getSettingOptions('auto_collapse'),
'#default_value' => $this->getSetting('auto_collapse'),
'#required' => TRUE,
'#states' => [
'visible' => [
$edit_mode_selector => ['value' => self::EDIT_MODE_CLOSED],
],
],
];
$elements['auto_collapse_threshold'] = [
'#type' => 'number',
'#title' => $this->t('Threshold for auto-collapse'),
'#default_value' => $this->getSetting('auto_collapse_threshold'),
'#description' => $this->t('Number of items (deltas) that must be present before media files start getting automatically collapsed. For example, if the threshold is 3, and there are only 2 or fewer media files, all media files will remain open when jumping between editing different media files.'),
'#min' => 0,
'#states' => [
'invisible' => [
$edit_mode_selector => ['value' => self::EDIT_MODE_OPEN],
],
],
];
$target_type_id = $this->getFieldSetting('target_type');
$view_display_mode_options =
$this->entityDisplayRepository->getViewModeOptions($target_type_id);
$elements['preview_display_mode'] = [
'#type' => 'select',
'#options' => $view_display_mode_options,
'#title' => $this->t('Preview display mode'),
'#description' => $this->t('The display mode to use when displaying a preview of each media file next to its summary.'),
'#default_value' => $this->getSetting('preview_display_mode'),
'#required' => TRUE,
];
$form_display_mode_options =
$this->entityDisplayRepository->getFormModeOptions($target_type_id);
$elements['form_display_mode'] = [
'#type' => 'select',
'#options' => $form_display_mode_options,
'#title' => $this->t('Form display mode'),
'#description' => $this->t('The form display mode to use when rendering the media file form.'),
'#default_value' => $this->getSetting('form_display_mode'),
'#required' => TRUE,
];
if (self::doesMediaLibrarySupportCustomViewDisplay()) {
$view_displays = $this->getMediaLibraryViewDisplays();
$elements['library_view_display'] = [
'#type' => 'select',
'#options' => $view_displays,
'#title' => $this->t('Media library view display'),
'#description' => $this->t('The view and display mode to use when displaying listings in the media library.'),
'#default_value' => $this->getSetting('library_view_display'),
'#required' => TRUE,
];
}
$elements['features'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Enable widget features'),
'#options' => $this->getSettingOptions('features'),
'#default_value' => $this->getSetting('features'),
'#description' => $this->t('The features available to users during editing.'),
'#multiple' => TRUE,
];
$allowed_types = $this->getAllowedTypes();
if (count($allowed_types) > 1) {
$library_tab_order = $this->buildMediaTabSortWidget($allowed_types);
$elements['library_tab_order'] = $library_tab_order;
}
return $elements;
}
/**
* {@inheritdoc}
*/
public function settingsSummary(): array {
$summary = [];
$summary[] =
$this->t('Title: @title', ['@title' => $this->getSetting('title')]);
$summary[] =
$this->t(
'Plural title: @title_plural',
['@title_plural' => $this->getSetting('title_plural')]
);
$edit_mode_options = $this->getSettingOptions('initial_mode');
$initial_mode = $this->getSetting('initial_mode');
$edit_mode_label = $edit_mode_options[$initial_mode];
$summary[] =
$this->t(
'Initial edit mode: @edit_mode',
['@edit_mode' => $edit_mode_label]
);
if ($initial_mode == self::EDIT_MODE_CLOSED) {
$auto_collapse_options = $this->getSettingOptions('auto_collapse');
$auto_collapse = $this->getSetting('auto_collapse');
$auto_collapse_label = $auto_collapse_options[$auto_collapse];
$summary[] =
$this->t(
'Auto-collapse: @auto_collapse',
['@auto_collapse' => $auto_collapse_label]
);
}
$auto_collapse_threshold = $this->getSetting('auto_collapse_threshold');
if (($initial_mode == self::EDIT_MODE_CLOSED) &&
($auto_collapse_threshold > 0)) {
$summary[] =
$this->t(
'Auto-collapse threshold: @mode_limit',
['@mode_limit' => $auto_collapse_threshold]
);
}
$summary[] =
$this->t(
'Preview display mode: @preview_display_mode',
['@preview_display_mode' => $this->getSetting('preview_display_mode')]
);
$summary[] =
$this->t(
'Form display mode: @form_display_mode',
['@form_display_mode' => $this->getSetting('form_display_mode')]
);
if (self::doesMediaLibrarySupportCustomViewDisplay()) {
$summary[] =
$this->t(
'Media library view display: @library_view_display',
['@library_view_display' => $this->getSetting('library_view_display')]
);
}
$features_options = $this->getSettingOptions('features');
$features_enabled = $this->getSetting('features');
$features_enabled_labels =
array_intersect_key($features_options, array_filter($features_enabled));
if (!empty($features_enabled_labels)) {
$summary[] =
$this->t(
'Features: @features',
['@features' => implode(', ', $features_enabled_labels)]
);
}
$allowed_media_types = $this->getAllowedTypes();
if (count($allowed_media_types) > 1) {
$media_type_labels = [];
foreach ($allowed_media_types as $media_info) {
$media_type_labels[] = $media_info['label'];
}
$order_string = implode(', ', $media_type_labels);
$summary[] =
t('Media library tab order: @order', ['@order' => $order_string]);
}
return $summary;
}
/**
* {@inheritdoc}
*
* Adds dependencies on display modes and views referenced by this widget.
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
$display_mode_settings = [
'preview_display_mode',
'form_display_mode',
];
if (self::doesMediaLibrarySupportCustomViewDisplay()) {
$view_display_settings = ['library_view_display'];
}
else {
$view_display_settings = [];
}
$display_modes =
array_unique(
array_map(
function ($setting_name) {
return $this->getSetting($setting_name);
},
$display_mode_settings
)
);
$view_ids =
array_unique(
array_map(
function ($setting_name) {
$view_display_name = $this->getSetting($setting_name);
[$view_id] = explode('.', $view_display_name);
return $view_id;
},
$view_display_settings
)
);
$display_storage = $this->getEntityViewModeStorage();
foreach ($display_modes as $display_mode_name) {
/** @var \Drupal\Core\Entity\Entity\EntityViewMode $view_mode */
$view_mode = $display_storage->load('media.' . $display_mode_name);
if (!empty($view_mode)) {
$dependency_key = $view_mode->getConfigDependencyKey();
$dependency_name = $view_mode->getConfigDependencyName();
$dependencies =
array_merge_recursive(
$dependencies,
[$dependency_key => [$dependency_name]]
);
}
}
$view_storage = $this->entityTypeManager->getStorage('view');
foreach ($view_ids as $view_id) {
$view = $view_storage->load($view_id);
if (!empty($view)) {
$dependency_key = $view->getConfigDependencyKey();
$dependency_name = $view->getConfigDependencyName();
$dependencies =
array_merge_recursive(
$dependencies,
[$dependency_key => [$dependency_name]]
);
}
}
return $dependencies;
}
/**
* {@inheritdoc}
*/
public function form(FieldItemListInterface $items,
array &$form,
FormStateInterface $form_state,
$get_delta = NULL): array {
$parents = $form['#parents'];
// Identify the manage field settings default value form.
if (in_array('default_value_input', $parents, TRUE)) {
// Since the entity is neither reusable nor cloneable, having a default
// value is not supported.
$elements = [
'#markup' => $this->t(
'No widget available for: %label.',
['%label' => $items->getFieldDefinition()->getLabel()]
),
];
}
else {
$field_name = $this->getFieldName();
$parents = $form['#parents'];
$field_path = array_merge($parents, [$field_name]);
$id_prefix = implode('-', $field_path);
$wrapper_id = Html::getId($id_prefix . '-wrapper');
$this->fieldParents = $parents;
$this->fieldIdPrefix = $id_prefix;
$this->fieldWrapperId = $wrapper_id;
$imf_fields = $form_state->get('inline_media_form_fields') ?? [];
$imf_fields[] = [
'parents' => $parents,
'field_name' => $field_name,
];
$form_state->set('inline_media_form_fields', $imf_fields);
$form['#process'][] = [static::class, 'entityFormProcess'];
$elements = parent::form($items, $form, $form_state, $get_delta);
// Signal to content_translation that this field should be treated as
// multilingual and not be hidden, see
// \Drupal\content_translation\ContentTranslationHandler::entityFormSharedElements().
$elements['#multilingual'] = TRUE;
}
return $elements;
}
/**
* {@inheritdoc}
*/
public function formMultipleElements(FieldItemListInterface $items,
array &$form,
FormStateInterface $form_state): array {
$widget_state = $this->getCurrentWidgetState($form_state);
$widget_state['ajax_wrapper_id'] = $this->fieldWrapperId;
$max = $widget_state['items_count'];
$this->realItemCount = $max;
$field_name = $this->fieldDefinition->getName();
$field_prefix = strtr($this->fieldIdPrefix, '_', '-');
$entity_type_id = $items->getEntity()->getEntityTypeId();
if (count($this->fieldParents) != 0) {
if ($entity_type_id === 'media') {
$form['#attributes']['class'][] = 'inline-media-form-nested';
}
}
$elements = [];
$elements['#prefix'] =
sprintf(
'<div class="inline-media-form-wrapper" id="%s">',
$this->fieldWrapperId,
);
$elements['#suffix'] = '</div>';
// Persist the widget state so formElement() can access it.
static::setWidgetState(
$this->fieldParents,
$field_name,
$form_state,
$widget_state
);
$elements =
$this->buildWidgetForEditMode(
$form,
$form_state,
$items,
$elements
);
return $elements;
}
/**
* {@inheritdoc}
*
* @see \Drupal\content_translation\Controller\ContentTranslationController::prepareTranslation()
* Uses a similar approach to populate a new translation.
*
* @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection
*/
public function formElement(FieldItemListInterface $items,
$delta,
array $element,
array &$form,
FormStateInterface $form_state): array {
$parents = $element['#field_parents'];
$widget_state = $this->getCurrentWidgetState($form_state, $parents);
$this->initializeWidgetStateForDelta($items, $delta, $widget_state);
/** @var \Drupal\Core\Entity\ContentEntityInterface $host_entity */
$host_entity = $items->getEntity();
assert($host_entity instanceof ContentEntityInterface);
$field_name = $this->fieldDefinition->getName();
$widget_delta_state =& $widget_state['media'][$delta];
$this->applyTranslation(
$host_entity,
$form_state,
$widget_delta_state
);
$wrapped_media = $widget_delta_state['wrapper'] ?? NULL;
assert(
($wrapped_media === NULL) ||
($wrapped_media instanceof MediaItemAdapterInterface)
);
$can_view_entity = $this->canView($wrapped_media);
if (!$can_view_entity || ($wrapped_media === NULL)) {
$element['#access'] = FALSE;
$widget_state['inaccessible_item_count'] =
($widget_state['inaccessible_item_count'] ?? 0) + 1;
unset($widget_state['media'][$delta]);
}
else {
$element =
$this->buildSingleWidget(
$form,
$form_state,
$field_name,
$widget_state,
$items,
$element,
$delta,
$wrapped_media
);
}
static::setWidgetState($parents, $field_name, $form_state, $widget_state);
return $element;
}
/**
* {@inheritdoc}
*/
public function extractFormValues(FieldItemListInterface $items,
array $form,
FormStateInterface $form_state): void {
// Filter possible empty items.
$items->filterEmptyItems();
// Remove buttons from header actions.
$field_name = $this->fieldDefinition->getName();
$path = array_merge($form['#parents'], [$field_name]);
$form_state_variables = $form_state->getValues();
$key_exists = NULL;
$values =
NestedArray::getValue($form_state_variables, $path, $key_exists);
if ($key_exists) {
unset($values['header_actions']);
NestedArray::setValue($form_state_variables, $path, $values);
$form_state->setValues($form_state_variables);
}
parent::extractFormValues($items, $form, $form_state);
}
/**
* Validates a single media file delta.
*
* @param array $element
* The form element (the widget) being validated.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The state of the edit form.
*/
public function elementValidate(array $element,
FormStateInterface $form_state): void {
$parents = $element['#field_parents'];
$widget_state = $this->getCurrentWidgetState($form_state, $parents);
$delta = $element['#delta'];
$widget_delta_state = $widget_state['media'][$delta] ?? NULL;
if ($widget_delta_state != NULL) {
$wrapped_media = $widget_delta_state['wrapper'];
assert($wrapped_media instanceof MediaItemAdapterInterface);
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display */
$display = $widget_delta_state['display'];
if ($widget_delta_state['mode'] == self::EDIT_MODE_OPEN) {
$media_entity = $wrapped_media->toMediaEntity();
// Extract the form values on submit for getting the current media file.
$display->extractFormValues(
$media_entity,
$element['subform'],
$form_state
);
}
}
$field_name = $this->fieldDefinition->getName();
static::setWidgetState($parents, $field_name, $form_state, $widget_state);
}
/**
* Assembles an action button for a media file widget by adding boilerplate.
*
* @param array $base_element
* Partial render array for the button.
*
* @return array
* Fully-populated render array for the button.
*/
public function buildActionButton(array $base_element): array {
// Do not expand elements that do not have submit handler.
if (empty($base_element['#submit'])) {
return $base_element;
}
$button = $base_element + [
'#type' => 'submit',
'#theme_wrappers' => ['input__submit__media_file_action'],
];
// Html::getId will give us '-' char in name but we want '_' for now so
// we use strtr to search & replace '-' to '_'.
$button['#name'] = strtr(Html::getId($base_element['#name']), '-', '_');
$button['#id'] = Html::getUniqueId($button['#name']);
if (isset($button['#ajax'])) {
$button['#ajax'] += [
'effect' => 'fade',
// Since a normal throbber is added inline, this has the potential to
// break a layout if the button is located in dropbuttons. Instead,
// it's safer to just show the fullscreen progress element instead.
'progress' => ['type' => 'fullscreen'],
];
}
return $button;
}
/**
* {@inheritdoc}
*/
public function flagErrors(FieldItemListInterface $items,
ConstraintViolationListInterface $violations,
array $form,
FormStateInterface $form_state): void {
$parents = $form['#parents'];
$widget_state = $this->getCurrentWidgetState($form_state, $parents);
parent::flagErrors($items, $violations, $form, $form_state);
}
/**
* {@inheritdoc}
*/
public function errorElement(array $element,
ConstraintViolationInterface $error,
array $form,
FormStateInterface $form_state): array {
// Validation errors might be a about a specific form element;
// attempt to find a matching element.
if (empty($error->arrayPropertyPath)) {
$error_element = $element;
}
else {
$error_element =
NestedArray::getValue($element, $error->arrayPropertyPath) ?? $element;
}
return $error_element;
}
/**
* Special handling to validate form elements with multiple values.
*
* @param array $elements
* The form elements (widget) being validated.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The state of the edit form.
*/
public function multipleElementValidate(
array $elements,
FormStateInterface $form_state): void {
$parents = $elements['#field_parents'];
$field_name = $this->fieldDefinition->getName();
$widget_state = $this->getCurrentWidgetState($form_state, $parents);
if ($elements['#required'] && ($widget_state['real_item_count'] < 1)) {
$form_state->setError(
$elements,
t('@name field is required.',
['@name' => $this->fieldDefinition->getLabel()])
);
}
static::setWidgetState($parents, $field_name, $form_state, $widget_state);
}
/**
* {@inheritdoc}
*/
public function massageFormValues(array $values,
array $form,
FormStateInterface $form_state): array {
$parents = $form['#parents'];
$widget_state = $this->getCurrentWidgetState($form_state, $parents);
$element =
NestedArray::getValue(
$form_state->getCompleteForm(),
$widget_state['array_parents']
);
foreach ($values as $delta => &$item) {
$original_delta = $item['_original_delta'];
$widget_delta_state =& $widget_state['media'][$original_delta];
$edit_mode = $widget_delta_state['mode'] ?? NULL;
$wrapped_media = $widget_delta_state['wrapper'] ?? NULL;
$display = $widget_delta_state['display'];
if ($wrapped_media !== NULL) {
$delta_element = $element[$original_delta];
$item =
$this->massageDeltaFormValues(
$display,
$form_state,
$delta_element,
$wrapped_media,
$item,
$edit_mode
);
}
}
return $values;
}
// ===========================================================================
// Protected Methods
// ===========================================================================
/**
* Gets the machine name of the field this widget is editing.
*
* @return string
* The machine name of the field.
*/
protected function getFieldName(): string {
return $this->fieldDefinition->getName();
}
/**
* Gets whether translation support is enabled and available.
*
* This determination is based on whether the translation manager has been
* injected at construction time.
*
* @return bool
* TRUE if translation support is enabled on the site.
*/
protected function hasTranslationManager(): bool {
return ($this->translationManager !== NULL);
}
/**
* Checks if it is safe to re-order, add, or remove media files.
*
* @return bool
* TRUE if we can allow reference changes; or, FALSE, otherwise.
*/
protected function allowReferenceChanges(): bool {
return !$this->isTranslating;
}
/**
* Returns what media file types the field can reference, sorted by weight.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface|null $field_definition
* (optional) The field definition for which the allowed types should be
* returned, defaults to the current field.
*
* @return array
* A list of arrays keyed by the media file type machine name with the
* following properties.
* - label: The label of the media file type.
* - weight: The weight of the media file type.
*/
protected function getAllowedTypes(
FieldDefinitionInterface $field_definition = NULL): array {
$result = [];
if ($field_definition === NULL) {
$field_definition = $this->fieldDefinition;
}
assert(!empty($field_definition));
$target_type_id = $field_definition->getSetting('target_type');
$bundles = $this->entityBundleInfo->getBundleInfo($target_type_id);
$target_bundles = $this->getSelectionHandlerSetting('target_bundles');
$sorted_bundles = $this->getSetting('library_tab_order');
$bundle_weights = array_flip($sorted_bundles);
$sequence_weight = 0;
foreach ($bundles as $machine_name => $bundle) {
if (empty($target_bundles) || in_array($machine_name, $target_bundles)) {
// Fallback to sequence order in case new media types were added since
// the tab order was configured.
$weight = $bundle_weights[$machine_name] ?? $sequence_weight;
$result[$machine_name] = [
'label' => $bundle['label'],
'weight' => $weight,
];
++$sequence_weight;
}
}
// Re-sort the target types by their weights.
uasort($result, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
return $result;
}
/**
* Gets the storage interface for entity view modes.
*
* @return \Drupal\Core\Entity\EntityStorageInterface
* The view mode storage service.
*/
protected function getEntityViewModeStorage(): EntityStorageInterface {
try {
return $this->entityTypeManager->getStorage('entity_view_mode');
}
catch (InvalidPluginDefinitionException | PluginNotFoundException $ex) {
throw new \RuntimeException(
'Failed to load entity view mode storage: ' . $ex->getMessage(),
0,
$ex
);
}
}
/**
* Gets the storage interface for views.
*
* @return \Drupal\Core\Entity\EntityStorageInterface
* The view storage service.
*/
protected function getViewStorage(): EntityStorageInterface {
try {
return $this->entityTypeManager->getStorage('view');
}
catch (InvalidPluginDefinitionException | PluginNotFoundException $ex) {
throw new \RuntimeException(
'Failed to load view storage: ' . $ex->getMessage(),
0,
$ex
);
}
}
/**
* Returns select options for a plugin setting.
*
* @param string $setting_name
* The name of the widget setting. Supported settings:
* - "edit_mode": Options for how each media file is displayed in the edit
* form by default.
* - "auto_collapse": Options for whether a media file opened for editing
* should be collapsed when starting to edit a different media file.
*
* @return array|null
* An array of setting option usable as a value for a "#options" key.
*/
protected function getSettingOptions(string $setting_name): ?array {
switch ($setting_name) {
case 'initial_mode':
$options = [
self::EDIT_MODE_OPEN => $this->t('Open'),
self::EDIT_MODE_CLOSED => $this->t('Closed'),
];
break;
case 'auto_collapse':
$options = [
self::AUTO_COLLAPSE_DISABLED => $this->t('No'),
self::AUTO_COLLAPSE_ENABLED => $this->t('Yes'),
];
break;
case 'features':
$options = [
self::FEATURE_COLLAPSE_EDIT_ALL => $this->t('Collapse / edit all'),
];
break;
}
return ($options ?? NULL);
}
/**
* Returns the value of a setting for the entity reference selection handler.
*
* @param string $setting_name
* The setting name.
*
* @return mixed
* The setting value.
*/
protected function getSelectionHandlerSetting(string $setting_name) {
$settings = $this->getFieldSetting('handler_settings');
return ($settings[$setting_name] ?? NULL);
}
/**
* Checks if a widget feature is enabled or not.
*
* @param string $feature
* Feature name to check.
*
* @return bool
* TRUE if the feature is enabled, otherwise FALSE.
*/
protected function isFeatureEnabled(string $feature): bool {
$features = $this->getSetting('features');
return !empty($features[$feature]);
}
/**
* Gets a list of all view displays that can be used for the media browser.
*
* Adapted from
* \Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget::settingsForm()
* introduced in #2971209.
*
* @return string[]
* An array of view displays, in the format VIEW_ID.DISPLAY_ID.
*/
protected function getMediaLibraryViewDisplays(): array {
$view_storage = $this->getViewStorage();
$views =
$view_storage->loadByProperties(['base_table' => 'media_field_data']);
$displays = [];
/** @var \Drupal\views\Entity\View $view */
foreach ($views as $view) {
foreach ($view->get('display') as $id => $display) {
$view->getExecutable()->setDisplay($id);
$display = $view->getExecutable()->getDisplay();
if (!$display->usesFields() || !$display->hasPath()) {
continue;
}
foreach ($display->options['fields'] as $field) {
if ($field['id'] == 'media_library_select_form') {
$display_id = sprintf("%s.%s", $view->id(), $id);
$displays[$display_id] =
sprintf(
'%s - %s',
$view->label(),
$display->display['display_title']
);
break;
}
}
}
}
return $displays;
}
/**
* Detects whether the Media Library supports a configurable view display.
*
* This requires a patch from #2971209 to be applied to Core (or to be running
* a version of Drupal that includes that feature).
*
* @return bool
* TRUE if Drupal core has been patched with support for configurable view
* displays.
*/
protected static function doesMediaLibrarySupportCustomViewDisplay(): bool {
// Detects changes introduced by
// https://git.drupalcode.org/project/drupal/-/merge_requests/4019 as of
// 3c872001742fa677e9e97eda8a96f911ec78c99f.
$class_name = MediaLibraryState::class;
$view_constant_name = 'DEFAULT_VIEW';
$display_constant_name = 'DEFAULT_DISPLAY';
return defined(implode('::', [$class_name, $view_constant_name])) &&
defined(implode('::', [$class_name, $display_constant_name]));
}
/**
* Determines whether the logged-in user can view the given media entity.
*
* @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
* The media entity on which to perform an access check.
*
* @return bool
* TRUE if the logged-in user can view the given entity.
*/
protected function canView(MediaItemAdapterInterface $wrapped_media): bool {
return $wrapped_media->canView();
}
/**
* Determines whether the logged-in user can modify the given media entity.
*
* @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
* The media entity on which to perform an access check.
*
* @return bool
* TRUE if the logged-in user can modify the given entity.
*/
protected function canModify(MediaItemAdapterInterface $wrapped_media): bool {
return ($wrapped_media->canModify() &&
!$this->shouldForceCloseForTranslation($wrapped_media));
}
/**
* Determines whether the logged-in user can delete the given media entity.
*
* @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
* The media entity on which to perform an access check.
*
* @return bool
* TRUE if the logged-in user can delete the given entity.
*/
protected function canDelete(MediaItemAdapterInterface $wrapped_media): bool {
return $wrapped_media->canDelete();
}
/**
* Gets the form display configured for editing the given media entity.
*
* @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
* The media adapter wrapping the media entity.
*
* @return \Drupal\Core\Entity\Display\EntityFormDisplayInterface
* The form display.
*/
protected function getFormDisplayMode(
MediaItemAdapterInterface $wrapped_media): EntityFormDisplayInterface {
$media_entity = $wrapped_media->toMediaEntity();
// @codingStandardsIgnoreLine
/** @noinspection PhpUnnecessaryLocalVariableInspection */
$form_display =
EntityFormDisplay::collectRenderDisplay(
$media_entity,
$this->getSetting('form_display_mode')
);
return $form_display;
}
/**
* Gets the state of the widget for all deltas.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The state of the edit form.
* @param array|null $parents
* Optional parameter for explicitly specifying the parents of the form
* element. If omitted, the parents are read from $this->fieldParents.
*
* @return array
* The state of the field widget.
*/
protected function getCurrentWidgetState(
FormStateInterface $form_state,
array $parents = NULL): array {
$field_name = $this->fieldDefinition->getName();
if ($parents === NULL) {
$parents = $this->fieldParents;
assert($parents !== NULL);
}
$widget_state = static::getWidgetState($parents, $field_name, $form_state);
assert(!empty($widget_state));
if (!isset($widget_state['media'])) {
$auto_collapse = $this->getSetting('auto_collapse');
$auto_collapse_threshold = $this->getSetting('auto_collapse_threshold');
$widget_state['auto_collapse'] = $auto_collapse;
$widget_state['auto_collapse_threshold'] = $auto_collapse_threshold;
$widget_state['show_bulk_rename_options'] = FALSE;
$widget_state['media'] = [];
}
return $widget_state;
}
/**
* Initializes the state of the edit widget for a single delta.
*
* @param \Drupal\Core\Field\FieldItemListInterface $items
* All of the deltas/distinct values of the field.
* @param int $delta
* The index of the delta being initialized.
* @param array $widget_state
* A reference to the widget state for the delta being initialized.
*/
protected function initializeWidgetStateForDelta(
FieldItemListInterface $items,
int $delta,
array &$widget_state): void {
$widget_delta_state =& $widget_state['media'][$delta];
$wrapped_media = $widget_delta_state['wrapper'] ?? NULL;
$item_mode = $widget_delta_state['mode'] ?? NULL;
assert(
($wrapped_media === NULL) ||
($wrapped_media instanceof MediaItemAdapterInterface)
);
$initial_mode = $this->getSetting('initial_mode');
if (($wrapped_media === NULL) && isset($items[$delta]->entity)) {
// This widget is not yet initialized, but we have an entity to use.
$media_entity = $items[$delta]->entity;
$item_mode = $initial_mode;
$wrapped_media = MediaItemAdapter::create($media_entity);
}
assert(!empty($wrapped_media));
$widget_delta_state['wrapper'] = $wrapped_media;
$widget_delta_state['entity'] = $wrapped_media->toMediaEntity();
$widget_delta_state['mode'] = $item_mode;
}
/**
* Counts the number of media files in the editing mode specified.
*
* @param array $widget_state
* The state of the media file editing widget.
* @param string $target_edit_mode
* The mode for which a count is desired.
*
* @return int
* The number of media files in the given mode.
*/
protected function countMediaFilesInMode(array $widget_state,
string $target_edit_mode): int {
$media_files = $widget_state['media'];
return array_reduce(
$media_files,
function (int $current_count, array $widget_delta_state) use ($target_edit_mode) {
if ($widget_delta_state['mode'] === $target_edit_mode) {
return $current_count + 1;
}
else {
return $current_count;
}
},
0
);
}
/**
* Counts the number of editable media files in the editing mode specified.
*
* @param array $widget_state
* The state of the media file editing widget.
* @param string $target_edit_mode
* The mode for which a count is desired.
*
* @return int
* The number of editable media files in the given mode.
*/
protected function countEditableMediaFilesInMode(array $widget_state,
string $target_edit_mode): int {
$media_files = $widget_state['media'];
return array_reduce(
$media_files,
function (int $current_count, array $widget_delta_state) use ($target_edit_mode) {
$wrapper = $widget_delta_state['wrapper'];
assert($wrapper instanceof MediaItemAdapterInterface);
if ($this->canModify($wrapper) &&
($widget_delta_state['mode'] === $target_edit_mode)) {
return $current_count + 1;
}
else {
return $current_count;
}
},
0
);
}
/**
* Builds a widget for arranging the order tabs appear in the media library.
*
* Adapted from
* \Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget::settingsForm().
*
* @param array[] $media_types
* An associative array in which each key is the machine name of a media
* type, and the value is an array with the following keys:
* - label: The human-friendly name for the media type.
* - weight: The sort-order/display order of the media type.
*
* @return array
* A render array for the media tab widget.
*/
protected function buildMediaTabSortWidget(array $media_types): array {
$library_tab_order = [
'#type' => 'table',
'#value_callback' => [static::class, 'storeMediaTabWeights'],
'#header' => [
$this->t('Media type'),
$this->t('Weight'),
],
'#tabledrag' => [
[
'action' => 'order',
'relationship' => 'sibling',
'group' => 'weight',
],
],
'#attached' => [
'library' => [
'inline_media_form/widget',
],
],
];
foreach ($media_types as $media_type_id => $type_info) {
$label = $type_info['label'];
$weight = $type_info['weight'];
$library_tab_order[$media_type_id] = [
'#weight' => $weight,
'#attributes' => ['class' => ['draggable']],
'label' => ['#markup' => $label],
'weight' => [
'#type' => 'weight',
'#title' => t('Weight for @title', ['@title' => $label]),
'#title_display' => 'invisible',
'#default_value' => $weight,
'#attributes' => [
'class' => ['weight'],
],
],
];
}
return $library_tab_order;
}
/**
* Builds the widget for collapsing/editing multiple deltas/media files.
*
* @param array $form
* The entity edit form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The state of the edit form.
* @param \Drupal\Core\Field\FieldItemListInterface $items
* All of the deltas/distinct values of the field.
* @param array $elements
* The base render array for the top-level widget form element.
*
* @return array
* The fully-populated render array for the widget.
*/
protected function buildWidgetForEditMode(array &$form,
FormStateInterface $form_state,
FieldItemListInterface $items,
array &$elements): array {
$max = $this->realItemCount;
$field_definition = $this->fieldDefinition;
$field_storage_definition = $field_definition->getFieldStorageDefinition();
$field_name = $field_definition->getName();
$field_title = $field_definition->getLabel();
$is_required = $field_definition->isRequired();
$is_multiple = $field_storage_definition->isMultiple();
$cardinality = $field_storage_definition->getCardinality();
$token_service = \Drupal::token();
$description =
FieldFilteredMarkup::create(
$token_service->replace($field_definition->getDescription())
);
if ($max > 0) {
for ($delta = 0; $delta < $max; ++$delta) {
// Add a new empty item if it doesn't exist yet at this delta.
if (!isset($items[$delta])) {
$items->appendItem();
}
// For multiple fields, title and description are handled by the
// wrapping table.
$current_element = [
'#title' => $is_multiple ? '' : $field_title,
'#description' => $is_multiple ? '' : $description,
];
$current_element =
$this->formSingleElement(
$items,
$delta,
$current_element,
$form,
$form_state
);
if (!empty($current_element)) {
// Input field for the delta (drag-n-drop reordering).
if ($is_multiple) {
// We name the element '_weight' to avoid clashing with elements
// defined by widget.
$current_element['_weight'] = [
'#type' => 'weight',
'#title_display' => 'invisible',
'#title' => $this->t(
'Weight for row @number',
['@number' => $delta + 1]
),
// Note: this 'delta' is the FAPI #type 'weight' element's
// property.
'#delta' => $max,
'#default_value' => $items[$delta]->_weight ?: $delta,
'#weight' => 100,
];
}
$has_access = $current_element['#access'] ?? TRUE;
// Access for the top element is set to FALSE only when the media file
// is removed. Media file access checking is done at a lower level.
if (!$has_access) {
--$this->realItemCount;
}
else {
$elements[$delta] = $current_element;
}
}
}
}
// NOTE: Widget state for each delta gets populated during
// formSingleElement() calls above.
$widget_state = $this->getCurrentWidgetState($form_state);
$widget_state['real_item_count'] = $this->realItemCount;
static::setWidgetState(
$this->fieldParents,
$field_name,
$form_state,
$widget_state
);
$referenceable_bundles = $this->getAllowedTypes($field_definition);
$target_bundles =
array_map(
function ($bundle_info) {
return $bundle_info['label'];
},
$referenceable_bundles
);
$title_label = $this->getSetting('title');
$title_plural = $this->getSetting('title_plural');
$elements += [
'#element_validate' => [[$this, 'multipleElementValidate']],
'#required' => $is_required,
'#field_name' => $field_name,
'#cardinality' => $cardinality,
'#max_delta' => $max - 1,
'#target_bundles' => $target_bundles,
'#reference_title' => $title_label,
'#reference_title_plural' => $title_plural,
];
$elements += [
'#theme' => 'field_multiple_value_form',
'#field_name' => $field_name,
'#cardinality' => $cardinality,
'#cardinality_multiple' => TRUE,
'#required' => $is_required,
'#title' => $field_title,
'#description' => $description,
'#max_delta' => $max - 1,
];
/** @var \Drupal\Core\Entity\ContentEntityInterface $host_entity */
$host_entity = $items->getEntity();
assert($host_entity instanceof ContentEntityInterface);
$this->detectTranslationMode($host_entity, $form_state);
$header_actions =
$this->buildHeaderActions(
$form,
$form_state,
$items,
$elements,
$widget_state
);
if (!empty($header_actions)) {
// Add a weight element so we guarantee that header actions will stay in
// first row. We will use this later in
// inline_media_form_preprocess_field_multiple_value_form().
$header_actions['_weight'] = [
'#type' => 'weight',
'#default_value' => -100,
];
$elements['header_actions'] = $header_actions;
}
$is_cardinality_unlimited =
($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
$has_room = ($this->realItemCount < $cardinality);
$all_items_accessible = empty($widget_state['inaccessible_item_count']);
// Disable ability to adjust weights if the user cannot view all the deltas.
$can_reorder_deltas =
$all_items_accessible && $this->allowReferenceChanges();
if (($this->realItemCount > 0) && !$all_items_accessible) {
$warning_message =
$this->createMessage(
$this->t(
'Items cannot be re-ordered because access to some @title is restricted. Only accessible @title are shown below.',
['@title' => $this->getSetting('title_plural')]
)
);
$warning_message['#weight'] = -100;
$elements['reorder_warning'] = $warning_message;
}
if (($is_cardinality_unlimited || $has_room) &&
!$form_state->isProgrammed() && $this->allowReferenceChanges()) {
$triggering_element = $form_state->getTriggeringElement();
$elements['add_more'] =
$this->buildOpenerRegion(
$host_entity,
$field_name,
$cardinality,
$widget_state,
$triggering_element
);
$elements['add_more']['#attributes']['class'][] =
'inline-media-form-add-wrapper';
}
$elements['#allow_reference_changes'] = $can_reorder_deltas;
$elements['#inline_media_form_widget'] = TRUE;
$elements['#attached']['library'][] = 'inline_media_form/widget';
$any_files_open =
($this->countMediaFilesInMode($widget_state, self::EDIT_MODE_OPEN) > 0);
$elements['#attributes']['class'][] = 'media-widget';
if ($any_files_open) {
// This allow us to hide the draggable "grippies" for weight when any of
// the media files are open for editing.
$elements['#attributes']['class'][] = 'media-widget--has-open-files';
}
else {
$elements['#attributes']['class'][] = 'media-widget--all-files-closed';
}
return $elements;
}
/**
* Builds header actions.
*
* @param array $form
* The entity edit form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The state of the edit form.
* @param \Drupal\Core\Field\FieldItemListInterface $items
* All of the deltas/distinct values of the field.
* @param array $element
* The form element render array containing the basic properties for the
* top-level widget.
* @param array $widget_state
* Field widget state.
*
* @return array[]
* The form element array.
*/
protected function buildHeaderActions(array $form,
FormStateInterface $form_state,
FieldItemListInterface $items,
array $element,
array $widget_state): array {
$actions = [];
$field_name = $this->fieldDefinition->getName();
$id_prefix = implode('-', array_merge($this->fieldParents, [$field_name]));
$open_count =
$this->countMediaFilesInMode($widget_state, self::EDIT_MODE_OPEN);
$closed_editable_count =
$this->countEditableMediaFilesInMode(
$widget_state,
self::EDIT_MODE_CLOSED
);
// Bulk rename option.
if (($widget_state['show_bulk_rename_options'] === FALSE) &&
($open_count === 0) && ($closed_editable_count != 0)) {
// Do not show option for bulk rename if:
// 1. There are no media files; OR
// 2. At least one media file is open for editing; OR
// 3. The bulk rename toolbar is already open.
$actions['actions']['bulk_rename'] =
$this->buildBulkRenameButton($field_name, $id_prefix);
}
$toolbar = $this->buildToolbar($widget_state);
$actions['toolbar'] = $toolbar;
// Collapse & expand all.
if (($this->fieldDefinition->getType() == 'entity_reference') &&
($this->realItemCount > 1) &&
$this->isFeatureEnabled(self::FEATURE_COLLAPSE_EDIT_ALL)) {
if ($open_count != 0) {
$actions['actions']['collapse_all'] =
$this->buildCollapseAllButton($field_name, $id_prefix);
}
if ($closed_editable_count != 0) {
$actions['actions']['edit_all'] = $this->buildEditAllButton($id_prefix);
}
}
$this->alterWidgetHeaderActions(
$form,
$form_state,
$items,
$element,
$actions
);
// Add inline_media_form_header flag which we use later in preprocessor to
// move header actions to table header.
if ($actions) {
// Set actions.
$actions['#type'] = 'inline_media_form_actions';
$actions['#inline_media_form_header'] = TRUE;
}
return $actions;
}
/**
* Builds the render array for the toolbar sub-component of the actions bar.
*
* This region can be empty if there is no toolbar to display. It is used for
* requesting additional parameters from users during short interactions like
* bulk renaming of media files.
*
* @param array $widget_state
* Field widget state.
*
* @return array
* The render array for the elements of the toolbar.
*/
protected function buildToolbar(array $widget_state): array {
$toolbar = [];
if ($widget_state['show_bulk_rename_options'] === TRUE) {
$toolbar = [
'bulk_rename' => $this->buildBulkRenameToolbar(),
];
}
return $toolbar;
}
/**
* Builds the toolbar for getting settings for bulk rename.
*
* @return array
* The render array for the bulk rename toolbar.
*/
protected function buildBulkRenameToolbar(): array {
$toolbar = [
'#type' => 'container',
'#title' => $this->t('Bulk rename'),
'#attributes' => [
'class' => [
'inline-media-form-toolbar',
'inline-media-form-toolbar--bulk-rename',
],
],
];
$toolbar['name'] = [
'#type' => 'textfield',
'#title' => $this->t('Bulk rename to'),
'#required' => TRUE,
];
$toolbar['number_sequentially'] = [
'#type' => 'checkbox',
'#title' => $this->t('Sequentially number media in the order they appear below.'),
'#default_value' => TRUE,
];
$limit_validation_errors = [
[
$this->getFieldName(),
'header_actions',
'toolbar',
'bulk_rename',
'name',
],
];
$toolbar['rename'] = [
'#type' => 'submit',
'#value' => $this->t('Rename'),
'#button_type' => 'primary',
'#limit_validation_errors' => $limit_validation_errors,
'#submit' => [
[static::class, 'bulkRenameAllMediaSubmit'],
],
'#ajax' => [
'effect' => 'fade',
'progress' => ['type' => 'fullscreen'],
'callback' => [static::class, 'handleToolbarAjax'],
'wrapper' => $this->fieldWrapperId,
],
];
$toolbar['cancel'] = [
'#type' => 'submit',
'#value' => $this->t('Cancel'),
'#limit_validation_errors' => $limit_validation_errors,
'#submit' => [
[static::class, 'hideBulkRenameToolbarSubmit'],
],
'#ajax' => [
'effect' => 'fade',
'progress' => ['type' => 'fullscreen'],
'callback' => [static::class, 'handleToolbarAjax'],
'wrapper' => $this->fieldWrapperId,
],
];
return $toolbar;
}
/**
* Builds the edit/summary widget for a single media file.
*
* @param array $form
* The entity edit form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the entity edit form.
* @param string $field_name
* The machine name of the field.
* @param array $widget_state
* The state of all of the widgets of this field.
* @param \Drupal\Core\Field\FieldItemListInterface $items
* All of the deltas/distinct values of the field.
* @param array $element
* The base widget form element render array.
* @param int $delta
* The index of the value that the widget is being constructed for.
* @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
* The media file for which a widget is being constructed.
*
* @return array
* The fully-populated render array for the widget.
*/
protected function buildSingleWidget(
array $form,
FormStateInterface $form_state,
string $field_name,
array &$widget_state,
FieldItemListInterface $items,
array $element,
int $delta,
MediaItemAdapterInterface $wrapped_media): array {
$parents = $element['#field_parents'];
$widget_delta_state =& $widget_state['media'][$delta];
$media_type = $this->getFieldSetting('target_type');
$title_label = $this->getSetting('title');
$media_bundle = $wrapped_media->getMediaType();
$media_entity = $wrapped_media->toMediaEntity();
$item_mode = $widget_delta_state['mode'] ?? NULL;
$has_unsaved_changes = $wrapped_media->isChanged();
if ($this->shouldForceCloseForTranslation($wrapped_media)) {
$item_mode = self::EDIT_MODE_CLOSED;
}
$id_prefix = implode('-', array_merge($parents, [$field_name, $delta]));
$element =
$this->prepareSingleWidget(
$media_bundle,
$parents,
$field_name,
$delta,
$id_prefix,
$element,
$has_unsaved_changes
);
$status_icons = [];
$can_update_entity = $this->canModify($wrapped_media);
$preview_display_mode = $this->getSetting('preview_display_mode');
$view_display =
$this->entityDisplayRepository->getViewDisplay(
$media_type,
$media_bundle,
$preview_display_mode
);
$media_teaser = $view_display->build($media_entity);
$element['top']['preview']['teaser'] = [
'#prefix' => '<div class="inline-media-file-preview-teaser">',
'#suffix' => '</div>',
'#weight' => 1,
'content' => $media_teaser,
];
// Widget actions.
$widget_actions = [
'actions' => [],
'dropdown_actions' => [],
];
if ($item_mode != self::EDIT_MODE_REMOVED) {
$widget_actions['dropdown_actions']['remove_button'] =
$this->buildRemoveButton($widget_state, $id_prefix, $delta);
// Force the closed mode when the user cannot edit the media file.
if (!$can_update_entity) {
$item_mode = self::EDIT_MODE_CLOSED;
}
}
if ($item_mode == self::EDIT_MODE_OPEN) {
$widget_actions['actions']['collapse_button'] =
$this->buildCollapseButton(
$field_name,
$widget_state,
$delta,
$id_prefix,
$parents,
$wrapped_media
);
}
else {
$widget_actions['actions']['edit_button'] =
$this->buildEditButton(
$field_name,
$widget_state,
$delta,
$id_prefix,
$parents,
$wrapped_media
);
if ($wrapped_media->isChanged()) {
$status_icons['changed'] = [
'#theme' => 'inline_media_form_info_icon',
'#icon' => 'changed',
'#message' => $this->t(
'You have unsaved changes on this @title item.',
['@title' => $title_label]
),
];
}
if (!$wrapped_media->isPublished()) {
$status_icons['preview'] = [
'#theme' => 'inline_media_form_info_icon',
'#icon' => 'unpublished',
'#message' => $this->t('Unpublished'),
];
}
}
// If update is disabled we will show the disabled pencil icon.
if (!$can_update_entity) {
$widget_actions['actions']['edit_disabled'] = [
'#theme' => 'inline_media_form_info_icon',
'#icon' => 'edit-disabled',
'#weight' => 1,
'#message' => $this->t(
'You are not allowed to edit this @title.',
['@title' => $title_label]
),
];
}
$this->alterWidgetDeltaActions(
$form,
$form_state,
$items,
$delta,
$element,
$wrapped_media,
$widget_actions
);
if (!empty($widget_actions['actions'])) {
// Expand all actions to proper submit elements and add it to top
// actions sub component.
$element['top']['actions']['actions'] =
array_map(
[$this, 'buildActionButton'],
$widget_actions['actions']
);
}
if (!empty($widget_actions['dropdown_actions'])) {
// Expand all dropdown actions to proper submit elements and add
// them to top dropdown actions sub component.
$element['top']['actions']['dropdown_actions'] =
array_map(
[$this, 'buildActionButton'],
$widget_actions['dropdown_actions']
);
}
$form_display = $this->getFormDisplayMode($wrapped_media);
// @todo Remove as part of https://www.drupal.org/node/2640056
if ($this->moduleHandler->moduleExists('field_group')) {
$element =
$this->applyFieldGroupWorkaroundForIssue2640056(
$element,
$media_entity,
$form_display
);
}
if ($item_mode == self::EDIT_MODE_OPEN) {
$element =
$this->buildOpenWidget(
$element,
$form_state,
$form_display,
$wrapped_media
);
}
elseif ($item_mode == self::EDIT_MODE_CLOSED) {
$element =
$this->buildClosedWidget(
$element,
$form_display,
$wrapped_media
);
}
else {
$element['subform'] = [];
}
// If we have any info items lets add them to the top section.
if (!empty($status_icons)) {
foreach ($status_icons as $info_item) {
if (!isset($info_item['#access']) || $info_item['#access']) {
$element['top']['icons']['items'] = $status_icons;
break;
}
}
}
$element['subform']['#attributes']['class'][] = 'inline-media-form-subform';
$element['subform']['#access'] = $can_update_entity;
if ($item_mode == self::EDIT_MODE_REMOVED) {
$element['#access'] = FALSE;
}
$widget_delta_state['wrapper'] = $wrapped_media;
$widget_delta_state['entity'] = $wrapped_media->toMediaEntity();
$widget_delta_state['display'] = $form_display;
$widget_delta_state['mode'] = $item_mode;
return $element;
}
/**
* Adds standard boilerplate container IDs, CSS classes, and callbacks.
*
* @param string $media_type_id
* The machine name of the bundle/media type for which the widget is being
* prepared.
* @param string[] $parents
* The names of each form element under which the widget is nested.
* @param string $field_name
* The machine name of the field.
* @param int $delta
* The index of the value that the widget is being constructed for.
* @param string $id_prefix
* A unique prefix to use for the elements of this field and delta.
* @param array $element
* The base widget form element render array.
* @param bool $has_unsaved_changes
* Whether the widget has changes that have not yet been persisted to the
* DB.
*
* @return array
* The form element, with boilerplate properties added.
*/
protected function prepareSingleWidget(string $media_type_id,
array $parents,
string $field_name,
int $delta,
string $id_prefix,
array $element,
bool $has_unsaved_changes): array {
$wrapper_id = Html::getUniqueId($id_prefix . '-item-wrapper');
$element_parents = $parents;
$element_parents[] = $field_name;
$element_parents[] = $delta;
$element_parents[] = 'subform';
$element += [
'#type' => 'container',
'#element_validate' => [[$this, 'elementValidate']],
'#media_file_type' => $media_type_id,
'subform' => [
'#type' => 'container',
'#parents' => $element_parents,
],
];
$element['#prefix'] = '<div id="' . $wrapper_id . '">';
$element['#suffix'] = '</div>';
// Create top section structure with all needed subsections.
$element['top'] = [
'#type' => 'container',
'#weight' => -1000,
'#attributes' => [
'class' => [
'inline-media-file-top',
'add-above-off',
],
],
// Section for a preview of the media file.
'preview' => [
'#type' => 'container',
'#attributes' => ['class' => ['inline-media-file-preview']],
],
// Section for info icons.
'icons' => [
'#type' => 'container',
'#attributes' => ['class' => ['inline-media-file-info']],
],
'summary' => [
'#type' => 'container',
'#attributes' => ['class' => ['inline-media-file-summary']],
],
// Element for actions and dropdown actions on media files.
'actions' => [
'#type' => 'inline_media_form_actions',
],
];
if ($has_unsaved_changes) {
// Make it possible for us to call attention to the summary if the file
// has unsaved changes.
$element['top']['summary']['#attributes']['class'][] =
'inline-media-file-summary--changed';
}
return $element;
}
/**
* Adjusts edit mode and entity being edited, as appropriate for translation.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $host_entity
* The parent entity being edited.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The state of the entity edit form.
* @param array $widget_delta_state
* A reference to the state of the widget for the delta currently being
* rendered.
*/
protected function applyTranslation(ContentEntityInterface $host_entity,
FormStateInterface $form_state,
array &$widget_delta_state): void {
$langcode = $form_state->get('langcode');
$wrapped_media = $widget_delta_state['wrapper'] ?? NULL;
assert(
($wrapped_media === NULL) ||
($wrapped_media instanceof MediaItemAdapterInterface)
);
$item_mode = $widget_delta_state['mode'];
$media_entity = $wrapped_media->toMediaEntity();
// Detect if we are translating.
$this->detectTranslationMode($host_entity, $form_state);
if ($this->isTranslating) {
assert($host_entity instanceof TranslationStatusInterface);
$host_translation_status = $host_entity->getTranslationStatus($langcode);
$host_translation_was_created =
($host_translation_status == TranslationStatusInterface::TRANSLATION_CREATED);
// If the node is being translated, the media files should be all open
// when the form is not being rebuilt (E.g. when clicking on a media
// files action) and when the translation is being added.
if (!$form_state->isRebuilding() && $host_translation_was_created) {
$item_mode = self::EDIT_MODE_OPEN;
}
// Add translation if missing for the target language.
if (!$media_entity->hasTranslation($langcode)) {
// Get the selected translation of the media file entity.
$entity_langcode = $media_entity->language()->getId();
$source = $form_state->get(['content_translation', 'source']);
$source_langcode = $source ? $source->getId() : $entity_langcode;
// Make sure the source language version is used if available. It is a
// the host and fetching the translation without this check could lead
// valid scenario to have no media files items in the source version of
// to an exception.
if ($media_entity->hasTranslation($source_langcode)) {
$media_entity = $media_entity->getTranslation($source_langcode);
}
// The media files entity has no content translation source field if
// no media file entity field is translatable, even if the host is.
if ($media_entity->hasField('content_translation_source')) {
// Initialise the translation with source language values.
$media_entity->addTranslation($langcode, $media_entity->toArray());
$translation = $media_entity->getTranslation($langcode);
$this->translationManager
->getTranslationMetadata($translation)
->setSource($media_entity->language()->getId());
}
}
// If any media files type is translatable do not switch.
if ($media_entity->hasField('content_translation_source')) {
// Switch the media file to the translation.
$media_entity = $media_entity->getTranslation($langcode);
}
}
else {
// Set the langcode, since we are not translating.
$langcode_key = $media_entity->getEntityType()->getKey('langcode');
if ($media_entity->get($langcode_key)->value != $langcode) {
// If a translation in the given language already exists, switch to
// that. If there is none yet, update the language.
if ($media_entity->hasTranslation($langcode)) {
$media_entity = $media_entity->getTranslation($langcode);
}
else {
$media_entity->set($langcode_key, $langcode);
}
}
}
$wrapped_media = MediaItemAdapter::create($media_entity);
$widget_delta_state['wrapper'] = $wrapped_media;
$widget_delta_state['entity'] = $wrapped_media->toMediaEntity();
$widget_delta_state['mode'] = $item_mode;
}
/**
* Builds the widget region for opening the media browser.
*
* @param \Drupal\Core\Entity\EntityInterface $host_entity
* The entity containing the media field.
* @param string $field_name
* The machine name of the field.
* @param int $cardinality
* The total number of media files that can be selected by the user. Can be
* FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED.
* @param array $widget_state
* The current widget state.
* @param array|null $triggering_element
* If the form is rebuilding, the element that triggered the rebuild of the
* form; or, NULL, if the form is not rebuilding.
*
* @return array
* The form element array.
*/
protected function buildOpenerRegion(EntityInterface $host_entity,
string $field_name,
int $cardinality,
array $widget_state,
?array $triggering_element): array {
$elements = [];
$plural_label = $this->getSetting('title_plural');
if ($this->user->hasPermission('view media')) {
// All the media types the field can reference (regardless of user).
$referenceable_media_type_ids = array_keys($this->getAllowedTypes());
if (count($referenceable_media_type_ids) === 0) {
$elements['icons'] =
$this->createMessage(
$this->t(
'This field is not yet configured to allow @title to be referenced.',
['@title' => $plural_label]
)
);
}
else {
$referenced_media_ids = $this->getReferencedMediaIds($widget_state);
$elements =
$this->buildMediaLibraryOpener(
$host_entity,
$field_name,
$cardinality,
$referenceable_media_type_ids,
$referenced_media_ids,
$triggering_element
);
}
}
else {
$elements['icons'] =
$this->createMessage(
$this->t(
'You are not allowed to view or add any @title.',
['@title' => $plural_label]
)
);
}
return $elements;
}
/**
* Builds the button for selecting a media file to be added.
*
* @param \Drupal\Core\Entity\EntityInterface $host_entity
* The entity containing the media field.
* @param string $field_name
* The machine name of the field.
* @param int $cardinality
* The total number of media files that can be selected by the user. Can be
* FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED.
* @param string[] $allowed_media_type_ids
* The machine names of the media types that can be selected.
* @param int[] $referenced_entity_ids
* The unique identifiers of the media files that have been selected so far.
* @param array|null $triggering_element
* If the form is rebuilding, the element that triggered the rebuild of the
* form; or, NULL, if the form is not rebuilding.
*
* @return array
* The form element array.
*/
protected function buildMediaLibraryOpener(
EntityInterface $host_entity,
string $field_name,
int $cardinality,
array $allowed_media_type_ids,
array $referenced_entity_ids,
?array $triggering_element): array {
$has_limited_cardinality =
($cardinality !== FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
$selected_count = count($referenced_entity_ids);
$field_widget_id = $this->fieldIdPrefix;
$element = [
'#type' => 'container',
'#cardinality' => $cardinality,
'#attributes' => [
'id' => $field_widget_id,
'class' => ['js-media-library-widget'],
],
'#attached' => [
'library' => ['media_library/widget'],
],
];
if ($has_limited_cardinality) {
$remaining_count = $cardinality - $selected_count;
$title_label = $this->getSetting('title');
$title_plural = $this->getSetting('title_plural');
if ($remaining_count !== 0) {
$cardinality_message =
$this->formatPlural(
$remaining_count,
'One more @title can be selected.',
'@count more @title_plural can be selected.',
[
'@title' => $title_label,
'@title_plural' => $title_plural,
]
);
}
else {
$cardinality_message =
$this->t(
'The maximum number of @title_plural have been selected.',
['@title_plural' => $title_plural]
);
}
// Add a line break between the field message and the cardinality message.
if (!empty($element['#description'])) {
$element['#description'] .= '<br />';
}
$element['#description'] .= $cardinality_message;
}
else {
$remaining_count = FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED;
}
$state = [
'host_entity' => $host_entity,
'field_widget_id' => $field_widget_id,
'field_name' => $field_name,
'referenced_entity_ids' => $referenced_entity_ids,
'allowed_media_type_ids' => $allowed_media_type_ids,
'remaining_count' => $remaining_count,
'library_view_display' => $this->getSetting('library_view_display'),
];
$button_text =
$this->t(
'Add @title',
['@title' => $this->getSetting('title_plural')]
);
$unique_opener_name = $field_widget_id . '-media-library-open-button';
$unique_update_name = $field_widget_id . '-media-library-update';
// Add a button that will load the Media library in a modal using AJAX.
$open_button = [
'#type' => 'button',
'#value' => $button_text,
'#name' => $unique_opener_name,
'#media_library_opener_state' => $state,
// Allow the media library to be opened even if there are form errors.
'#limit_validation_errors' => [],
'#attributes' => [
'class' => [
'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',
],
'#ajax' => [
'callback' => [static::class, 'handleOpenMediaLibraryAjax'],
'progress' => [
'type' => 'throbber',
'message' => $this->t('Opening media library.'),
],
],
];
// 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 ($has_limited_cardinality && ($remaining_count === 0)) {
if (!empty($triggering_element) &&
($trigger_parents = $triggering_element['#array_parents']) &&
end($trigger_parents) === 'media_library_update_widget') {
// The widget is being rebuilt from a selection change.
$open_button['#attributes']['data-disabled-focus'] = 'true';
}
else {
// The widget is being built without a selection change, so we can just
// set the item to disabled now, there is no need to set the focus
// first.
$open_button['#disabled'] = TRUE;
}
$open_button['#attributes']['class'][] = 'visually-hidden';
}
$element['open_button'] = $open_button;
// This hidden field and button are used to add new items to the widget.
$element['media_library_selection'] = [
'#type' => 'hidden',
'#attributes' => [
// This is used to pass the selection from the modal to the widget.
'data-media-library-widget-value' => $field_widget_id,
],
];
// We need to prevent the widget from being validated when no media items
// are selected. When a media field is added in a subform, entity
// validation is triggered in EntityFormDisplay::validateFormValues().
// Since the media item is not added to the form yet, this triggers errors
// for required media fields.
if (empty($referenced_entity_ids)) {
$limit_validation_errors = [];
}
else {
$limit_validation_errors =
[array_merge($this->fieldParents, [$field_name])];
}
// 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' => $this->t('Update widget'),
'#name' => $unique_update_name,
'#limit_validation_errors' => $limit_validation_errors,
'#validate' => [
[static::class, 'validateSelectedMediaItems'],
],
'#submit' => [
[static::class, 'addSelectedMediaItemSubmit'],
],
'#attributes' => [
'data-media-library-widget-update' => $field_widget_id,
'class' => ['js-hide'],
],
'#ajax' => [
'callback' => [static::class, 'handleMediaSelectedAjax'],
'wrapper' => $this->fieldWrapperId,
'progress' => [
'type' => 'throbber',
'message' => $this->t('Adding selection.'),
],
],
];
return $element;
}
/**
* Helper to create a media file UI message.
*
* @param string $message
* Message text.
* @param string $type
* Message type.
*
* @return array
* Render array of message.
*/
protected function createMessage(string $message,
string $type = 'warning'): array {
return [
'#type' => 'container',
'#markup' => $message,
'#attributes' => ['class' => ['messages', 'messages--' . $type]],
];
}
/**
* Prepares the widget state to add a new media file at a specific position.
*
* In addition to the widget state change, also user input could be modified
* to handle adding of a new media file at a specific position between
* existing media files.
*
* @param array $widget_state
* A reference to the state of the widget.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* Form state.
* @param array $field_path
* Path to inline media form field.
* @param int $target_delta
* Delta position in list of media files, where new media file will be
* added.
*/
protected static function prepareDeltaPosition(array &$widget_state,
FormStateInterface $form_state,
array $field_path,
int $target_delta): void {
// Increase number of items to create place for new media file.
++$widget_state['items_count'];
++$widget_state['real_item_count'];
// Change user input in order to create new delta position.
$user_input =& $form_state->getUserInput();
$field_input = NestedArray::getValue($user_input, $field_path);
// Rearrange all original deltas to make one place for the new element.
$new_deltas = [];
$original_deltas = $widget_state['original_deltas'] ?? [];
foreach ($original_deltas as $current_delta => $original_delta) {
if (($current_delta >= $target_delta)) {
$new_delta = ($current_delta + 1);
}
else {
$new_delta = $current_delta;
}
$new_deltas[$new_delta] = $original_delta;
$field_input[$original_delta]['_weight'] = $new_delta;
}
// Add information into delta mapping for the new element.
$original_deltas_size = count($original_deltas);
$new_deltas[$target_delta] = $original_deltas_size;
$field_input[$original_deltas_size]['_weight'] = $target_delta;
$widget_state['original_deltas'] = $new_deltas;
NestedArray::setValue($user_input, $field_path, $field_input);
}
/**
* Re-sorts media files based on their new weights.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param array $field_values_parents
* The field value parents.
*
* @noinspection PhpDocMissingThrowsInspection
* @noinspection PhpUnhandledExceptionInspection
* If deltas are missing, we have a code bug that could corrupt data; so a
* fatal exception is acceptable under those circumstances.
*/
protected static function reorderMediaFiles(
FormStateInterface $form_state,
array $field_values_parents): void {
$field_name = end($field_values_parents);
$field_values =
NestedArray::getValue($form_state->getValues(), $field_values_parents);
$complete_field_storage = NestedArray::getValue(
$form_state->getStorage(), [
'field_storage',
'#parents',
]
);
$new_field_storage = $complete_field_storage;
// Set a flag to prevent this from running twice, as the entity is built
// for validation as well as saving and would fail the second time as we
// already altered the field storage.
if (!empty($new_field_storage['#fields'][$field_name]['reordered'])) {
return;
}
$new_field_storage['#fields'][$field_name]['reordered'] = TRUE;
// Clear out all current media file keys in all nested media file widgets
// as there might be fewer than before or none in a certain widget.
$clear_media_files = function ($field_storage) use (&$clear_media_files) {
foreach ($field_storage as $key => $value) {
if ($key === '#fields') {
foreach ($value as $field_name => $widget_state) {
if (isset($widget_state['media'])) {
$field_storage['#fields'][$field_name]['media'] = [];
}
}
}
else {
$field_storage[$key] = $clear_media_files($field_storage[$key]);
}
}
return $field_storage;
};
// Only clear the current field and its children to avoid deleting
// media file references in other fields.
$new_field_storage['#fields'][$field_name]['media'] = [];
if (isset($new_field_storage[$field_name])) {
$new_field_storage[$field_name] =
$clear_media_files($new_field_storage[$field_name]);
}
$reorder_media = function ($reorder_values, $parents = [], FieldableEntityInterface $parent_entity = NULL) use ($complete_field_storage, &$new_field_storage, &$reorder_media) {
foreach ($reorder_values as $field_name => $values) {
foreach ($values['list'] as $delta => $item_values) {
$old_keys = array_merge(
$parents, [
'#fields',
$field_name,
'media',
$delta,
]
);
$path = explode('][', $item_values['_path']);
$new_field_name = array_pop($path);
$key_parents = [];
foreach ($path as $i => $key) {
$key_parents[] = $key;
if ($i % 2 == 1) {
$key_parents[] = 'subform';
}
}
$new_keys = array_merge(
$key_parents, [
'#fields',
$new_field_name,
'media',
$item_values['_weight'],
]
);
$key_exists = NULL;
$item_state =
NestedArray::getValue(
$complete_field_storage,
$old_keys,
$key_exists
);
if (!$key_exists && $parent_entity) {
// If key does not exist, then this parent widget was previously
// not expanded. This can only happen on nested levels. In that
// case, initialize a new item state and set the widget state to
// an empty array if it is not already set from an earlier item.
// If something else is placed there, it will be put in there,
// otherwise the widget will know that nothing is there anymore.
$item_state = [
'entity' => $parent_entity->get($field_name)->get($delta)->entity,
'mode' => self::EDIT_MODE_CLOSED,
];
$widget_state_keys =
array_slice($old_keys, 0, count($old_keys) - 2);
if (!NestedArray::getValue($new_field_storage, $widget_state_keys)) {
NestedArray::setValue(
$new_field_storage,
$widget_state_keys,
['media' => []]
);
}
}
// Ensure the referenced media file will be saved.
$wrapped_media = $item_state['wrapper'];
assert($wrapped_media instanceof MediaItemAdapterInterface);
NestedArray::setValue($new_field_storage, $new_keys, $item_state);
}
}
};
// Recalculate original deltas.
$recalculate_original_deltas = function ($field_storage, ContentEntityInterface $parent_entity) use (&$recalculate_original_deltas) {
if (isset($field_storage['#fields'])) {
foreach ($field_storage['#fields'] as $field_name => $widget_state) {
if (isset($widget_state['media'])) {
// If the parent field does not exist but we have media files in
// widget state, something went wrong and we have a mismatch.
// Throw an exception.
if (!$parent_entity->hasField($field_name) &&
!empty($widget_state['media'])) {
throw new \LogicException(
sprintf(
"Reordering media files resulted in a media file on non-existing field %d on parent entity %s/%s",
$field_name,
$parent_entity->getEntityTypeId(),
$parent_entity->id()
)
);
}
// Sort the media files by key so that they will be assigned to
// the entity in the right order. Reset the deltas.
ksort($widget_state['media']);
$widget_state['media'] = array_values($widget_state['media']);
$media_count = count($widget_state['media']);
$original_deltas = range(0, $media_count - 1);
$field =& $field_storage['#fields'][$field_name];
$field['original_deltas'] = $original_deltas;
$field['items_count'] = $media_count;
$field['real_item_count'] = $media_count;
// Update the parent entity and point to the new children, if the
// parent field does not exist, we also have no media files, so
// we can just skip this, this is a dead leaf after re-ordering.
if ($parent_entity->hasField($field_name)) {
$parent_entity->set(
$field_name,
array_column($widget_state['media'], 'entity')
);
// Next process that field recursively.
foreach (array_keys($widget_state['media']) as $delta) {
if (isset($field_storage[$field_name][$delta]['subform'])) {
$field_storage[$field_name][$delta]['subform'] =
$recalculate_original_deltas(
$field_storage[$field_name][$delta]['subform'],
$parent_entity->get($field_name)->get($delta)->entity
);
}
}
}
}
}
}
return $field_storage;
};
$parent_entity = $form_state->getFormObject()->getEntity();
$new_field_storage =
$recalculate_original_deltas($new_field_storage, $parent_entity);
$form_state->set(['field_storage', '#parents'], $new_field_storage);
}
/**
* Determine if this widget should be in translation mode.
*
* Initializes $this->isTranslating.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $host_entity
* The entity containing the field that this widget corresponds to.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the entity edit form.
*/
protected function detectTranslationMode(
ContentEntityInterface $host_entity,
FormStateInterface $form_state): void {
if ($this->isTranslating != NULL) {
return;
}
$this->isTranslating = FALSE;
if (!($host_entity instanceof TranslationStatusInterface) ||
!$host_entity->isTranslatable()) {
return;
}
if (!$host_entity->getEntityType()->hasKey('default_langcode')) {
return;
}
$default_langcode_key =
$host_entity->getEntityType()->getKey('default_langcode');
if (!$host_entity->hasField($default_langcode_key)) {
return;
}
// Supporting
// \Drupal\content_translation\Controller\ContentTranslationController.
if (!empty($form_state->get('content_translation'))) {
// Adding a translation.
$this->isTranslating = TRUE;
}
$langcode = $form_state->get('langcode');
if ($host_entity->hasTranslation($langcode) &&
$host_entity->getTranslation($langcode)->get($default_langcode_key)->value == 0) {
// Editing a translation.
$this->isTranslating = TRUE;
}
}
/**
* Determines whether to force-close a media file during translation.
*
* If untranslatable fields are hidden while translating, we are translating
* the parent, and a media file is open, then close the media file if it does
* not have translatable fields.
*
* @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
* The media entity being examined.
*
* @return bool
* TRUE if the media file should be closed for translation; or, FALSE,
* otherwise.
*/
protected function shouldForceCloseForTranslation(
MediaItemAdapterInterface $wrapped_media): bool {
$translating_force_close = FALSE;
if ($this->hasTranslationManager()) {
$settings =
$this->translationManager->getBundleTranslationSettings(
'media',
$wrapped_media->getMediaType()
);
if (!empty($settings['untranslatable_fields_hide']) &&
$this->isTranslating) {
$translating_force_close = TRUE;
$media_entity = $wrapped_media->toMediaEntity();
$form_display = $this->getFormDisplayMode($wrapped_media);
$exposed_fields = array_keys($form_display->get('content'));
// Check if the media file has translatable fields.
foreach ($exposed_fields as $field) {
if ($media_entity->hasField($field)) {
$field_definition =
$media_entity->get($field)->getFieldDefinition();
$target_entity_type_id = $field_definition->getType();
$target_bundle_id =
$field_definition->getSetting('target_type');
// Check if we are referencing media files.
$is_media_reference =
($target_entity_type_id == 'entity_reference' &&
$target_bundle_id == 'media');
if ($is_media_reference || $field_definition->isTranslatable()) {
$translating_force_close = FALSE;
break;
}
}
}
}
}
return $translating_force_close;
}
/**
* Builds a button for opening a specific media file up for editing.
*
* @param string $field_name
* The machine name of the field.
* @param array $widget_state
* The state of all of the widgets of this field.
* @param int $delta
* The index of the value that the widget is being constructed for.
* @param string $id_prefix
* Unique prefix to use for buttons of the specific media file widget for
* which the button is being rendered.
* @param string[] $parents
* The machine name of each of the form parents of the button.
* @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
* The media file for which the button is being constructed.
*
* @return array
* Render array for the new button.
*/
protected function buildEditButton(
string $field_name,
array $widget_state,
int $delta,
string $id_prefix,
array $parents,
MediaItemAdapterInterface $wrapped_media): array {
$can_modify = $this->canModify($wrapped_media);
return $this->buildActionButton([
'#type' => 'submit',
'#value' => $this->t('Edit'),
'#name' => $id_prefix . '_edit',
'#weight' => 1,
'#submit' => [[static::class, 'mediaFileItemSubmit']],
'#delta' => $delta,
'#access' => $can_modify,
'#edit_mode' => self::EDIT_MODE_OPEN,
'#attributes' => [
'class' => [
'inline-media-form-icon-button',
'inline-media-form-icon-button-edit',
'button--extrasmall',
],
'title' => $this->t('Edit'),
],
'#limit_validation_errors' => [
array_merge($parents, [$field_name, $delta]),
],
'#ajax' => [
'callback' => [static::class, 'handleSingleItemAjax'],
'wrapper' => $widget_state['ajax_wrapper_id'],
],
]);
}
/**
* Builds a button for collapsing a media file that is open for editing.
*
* @param string $field_name
* The machine name of the field.
* @param array $widget_state
* The state of all of the widgets of this field.
* @param int $delta
* The index of the value that the widget is being constructed for.
* @param string $id_prefix
* Unique prefix to use for buttons of the specific media file widget for
* which the button is being rendered.
* @param string[] $parents
* The machine name of each of the form parents of the button.
* @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
* The media file for which the button is being constructed.
*
* @return array
* Render array for the new button.
*/
protected function buildCollapseButton(
string $field_name,
array $widget_state,
int $delta,
string $id_prefix,
array $parents,
MediaItemAdapterInterface $wrapped_media): array {
$can_modify = $this->canModify($wrapped_media);
return [
'#value' => $this->t('Collapse'),
'#name' => $id_prefix . '_collapse',
'#weight' => 1,
'#submit' => [[static::class, 'mediaFileItemSubmit']],
'#access' => $can_modify,
'#edit_mode' => self::EDIT_MODE_CLOSED,
'#delta' => $delta,
'#attributes' => [
'class' => [
'inline-media-form-icon-button',
'inline-media-form-icon-button-collapse',
'button--extrasmall',
],
'title' => $this->t('Collapse'),
],
'#limit_validation_errors' => [
array_merge($parents, [$field_name, $delta]),
],
'#ajax' => [
'callback' => [static::class, 'handleSingleItemAjax'],
'wrapper' => $widget_state['ajax_wrapper_id'],
],
];
}
/**
* Builds a button for removing a reference to a specific media file.
*
* @param array $widget_state
* The state of all of the widgets of this field.
* @param string $id_prefix
* Unique prefix to use for buttons of the specific media file widget for
* which the button is being rendered.
* @param int $delta
* The index of the value that the widget is being constructed for.
*
* @return array
* Render array for the new button.
*/
protected function buildRemoveButton(array $widget_state,
string $id_prefix,
int $delta): array {
return [
'#type' => 'submit',
'#value' => $this->t('Remove'),
'#name' => $id_prefix . '_remove',
'#weight' => 501,
'#submit' => [[static::class, 'mediaFileItemSubmit']],
'#delta' => $delta,
'#edit_mode' => self::EDIT_MODE_REMOVED,
'#attributes' => [
'class' => ['button--small'],
],
// Ignore all validation errors because deleting invalid media files
// is allowed.
'#limit_validation_errors' => [],
'#ajax' => [
'callback' => [static::class, 'handleSingleItemAjax'],
'wrapper' => $widget_state['ajax_wrapper_id'],
],
];
}
/**
* Builds a button for collapsing all open media files.
*
* @param string $field_name
* The machine name of the field.
* @param string $id_prefix
* Unique prefix to use for buttons of the specific media file widget for
* which the button is being rendered.
*
* @return array
* Render array for the new button.
*/
protected function buildCollapseAllButton(string $field_name,
string $id_prefix): array {
$submitCallback = [[static::class, 'changeAllEditModeSubmit']];
return $this->buildActionButton([
'#type' => 'submit',
'#value' => $this->t('Collapse all'),
'#submit' => $submitCallback,
'#name' => $id_prefix . '_collapse_all',
'#weight' => -1,
'#edit_mode' => self::EDIT_MODE_CLOSED,
'#attributes' => [
'class' => [
'inline-media-form-icon-button',
'inline-media-form-icon-button-collapse',
'button--extrasmall',
],
'title' => $this->t('Collapse all'),
],
'#limit_validation_errors' => [
array_merge(
$this->fieldParents,
[$field_name]
),
],
'#ajax' => [
'callback' => [static::class, 'handleAllItemAjax'],
'wrapper' => $this->fieldWrapperId,
],
]);
}
/**
* Builds a button for bulk renaming all media files.
*
* @param string $field_name
* The machine name of the field.
* @param string $id_prefix
* Unique prefix to use for buttons of the specific media file widget for
* which the button is being rendered.
*
* @return array
* Render array for the new button.
*/
protected function buildBulkRenameButton(string $field_name,
string $id_prefix): array {
$submitCallback = [[static::class, 'showBulkRenameToolbarSubmit']];
return $this->buildActionButton([
'#type' => 'submit',
'#value' => $this->t('Bulk rename'),
'#name' => $id_prefix . '_bulk_rename',
'#submit' => $submitCallback,
'#attributes' => [
'class' => [
'inline-media-form-icon-button',
'inline-media-form-icon-button-bulk-rename',
'button--extrasmall',
],
'title' => $this->t('Bulk rename'),
],
'#limit_validation_errors' => [
array_merge(
$this->fieldParents,
[$field_name]
),
],
'#ajax' => [
'callback' => [static::class, 'handleAllItemAjax'],
'wrapper' => $this->fieldWrapperId,
],
]);
}
/**
* Builds a button for opening up all open media files for editing.
*
* @param string $id_prefix
* Unique prefix to use for buttons of the specific media file widget for
* which the button is being rendered.
*
* @return array
* Render array for the new button.
*/
protected function buildEditAllButton(string $id_prefix): array {
$submitCallback = [[static::class, 'changeAllEditModeSubmit']];
return $this->buildActionButton([
'#type' => 'submit',
'#value' => $this->t('Edit all'),
'#submit' => $submitCallback,
'#name' => $id_prefix . '_edit-all',
'#edit_mode' => self::EDIT_MODE_OPEN,
'#attributes' => [
'class' => [
'inline-media-form-icon-button',
'inline-media-form-icon-button-edit',
'button--extrasmall',
],
'title' => $this->t('Edit all'),
],
'#limit_validation_errors' => [],
'#ajax' => [
'callback' => [static::class, 'handleAllItemAjax'],
'wrapper' => $this->fieldWrapperId,
],
]);
}
/**
* Builds the widget for editing a single media file.
*
* @param array $element
* The base widget form element render array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the entity edit form.
* @param \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display
* The form display to use for editing the media file.
* @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
* The media file for which a widget is being constructed.
*
* @return array
* A render array for the open editing widget.
*/
protected function buildOpenWidget(
array $element,
FormStateInterface $form_state,
EntityFormDisplayInterface $display,
MediaItemAdapterInterface $wrapped_media): array {
$has_view_access = $this->canView($wrapped_media);
$media_entity = $wrapped_media->toMediaEntity();
$display->buildForm($media_entity, $element['subform'], $form_state);
$hide_untranslatable_fields =
$media_entity->isDefaultTranslationAffectedOnly();
$summary = $wrapped_media->toFieldSummaries($display->getMode());
if (!empty($summary)) {
$element['top']['summary']['fields_info'] = [
'#theme' => 'inline_media_form_summary',
'#expanded' => TRUE,
'#access' => $has_view_access,
'#summary' => [
'content' => $summary,
],
];
}
foreach (Element::children($element['subform']) as $field) {
if ($media_entity->hasField($field)) {
$field_definition =
$media_entity->get($field)->getFieldDefinition();
// Do a check if we have to add a class to the form element. We need
// the class to show and hide elements, depending of the active
// perspective. We need them to filter out entity reference fields that
// reference media files, cause otherwise we have problems with showing
// and hiding the right fields in nested media files.
if (($field_definition->getType() == 'entity_reference') &&
($field_definition->getSetting('target_type') == 'media')) {
$is_media_field = TRUE;
}
else {
$is_media_field = FALSE;
}
if (!$is_media_field) {
$element['subform'][$field]['#attributes']['class'][] =
'inline-media-form-content';
$element['top']['summary']['fields_info'] = [
'#theme' => 'inline_media_form_summary',
'#expanded' => TRUE,
'#access' => $has_view_access,
'#summary' => [
'content' => $summary,
],
];
}
$translatable = $field_definition->isTranslatable();
// Hide untranslatable fields when configured to do so except
// media file fields.
if (!$translatable && $this->isTranslating && !$is_media_field) {
if ($hide_untranslatable_fields) {
$element['subform'][$field]['#access'] = FALSE;
}
else {
$element['subform'][$field]['widget']['#after_build'][] =
[static::class, 'addTranslatabilityClue'];
}
}
}
}
// Remove the revision log message widget. We populate it automatically from
// the log message of the parent entity. Using '#access' => FALSE is
// insufficient, because the hidden form element will still load the log
// message from the last revision of this media file, and it will override
// the value we will set in copyRevisionMetadata().
unset($element['subform']['revision_log_message']);
return $element;
}
/**
* Builds the widget for previewing a media file that is not open for editing.
*
* @param array $element
* The base widget form element render array.
* @param \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display
* The form display to use for editing the media file.
* @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
* The media file for which a widget is being constructed.
*
* @return array
* A render array for the closed/summary widget.
*/
protected function buildClosedWidget(
array $element,
EntityFormDisplayInterface $display,
MediaItemAdapterInterface $wrapped_media): array {
$element['subform'] = [];
$summary = $wrapped_media->toFieldSummaries($display->getMode());
if (!empty($summary)) {
$element['top']['summary']['fields_info'] = [
'#theme' => 'inline_media_form_summary',
'#expanded' => FALSE,
'#summary' => [
'content' => $summary,
],
];
}
return $element;
}
/**
* Applies a workaround for rendering field groups in media file edit forms.
*
* This workaround should no longer be necessary after #2640056.
*
* @param array $element
* The base widget form element render array.
* @param \Drupal\media\MediaInterface $media_entity
* The media file for which a widget is being constructed.
* @param \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display
* The form display being used to present the media file.
*
* @return array
* The modified widget render array.
*/
protected function applyFieldGroupWorkaroundForIssue2640056(
array $element,
MediaInterface $media_entity,
EntityFormDisplayInterface $form_display): array {
$context = [
'entity_type' => $media_entity->getEntityTypeId(),
'bundle' => $media_entity->bundle(),
'entity' => $media_entity,
'context' => 'form',
'display_context' => 'form',
'mode' => $form_display->getMode(),
];
field_group_attach_groups($element['subform'], $context);
if (method_exists(FormatterHelper::class, 'formProcess')) {
$element['subform']['#process'][] =
[FormatterHelper::class, 'formProcess'];
}
elseif (function_exists('field_group_form_pre_render')) {
$element['subform']['#pre_render'][] = 'field_group_form_pre_render';
}
elseif (function_exists('field_group_form_process')) {
$element['subform']['#process'][] = 'field_group_form_process';
}
return $element;
}
/**
* Invokes hook_imf_widget_header_actions_alter().
*
* @param array $form
* The render array for the entity form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the entity form.
* @param \Drupal\Core\Field\FieldItemListInterface $items
* All of the deltas/distinct values of the field.
* @param array $element
* The form element render array containing the basic properties for the
* top-level widget.
* @param array $header_actions
* A reference to the contents of the header actions:
* - actions - The default actions, which are always visible.
* - dropdown_actions - Actions that appear in the drop-down
* sub-component.
* - toolbar - The toolbar for providing input to the active tool (e.g.,
* bulk rename).
*/
protected function alterWidgetHeaderActions(
array $form,
FormStateInterface $form_state,
FieldItemListInterface $items,
array $element,
array &$header_actions): void {
$original_widget_state =
$this->getCurrentWidgetState($form_state, $this->fieldParents);
$context = [
'form' => $form,
'widget' => $original_widget_state,
'items' => $items,
'element' => $element,
'form_state' => $form_state,
'is_translating' => $this->isTranslating,
];
// Allow modules to alter widget actions.
$this->moduleHandler->alter(
'imf_widget_header_actions',
$header_actions,
$context
);
}
/**
* Invokes hook_imf_widget_delta_actions_alter().
*
* @param array $form
* The render array for the entity form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the entity form.
* @param \Drupal\Core\Field\FieldItemListInterface $items
* All of the deltas/distinct values of the field.
* @param int $delta
* The order/ordinal position of this item in the array of items (0, 1, 2,
* etc).
* @param array $element
* The form element render array containing the basic properties for the
* widget.
* @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
* The media item adapter wrapper around the media file for this widget.
* @param array $widget_actions
* A reference to the array of 'actions' and 'dropdown_actions':
* - actions - The default actions, which are always visible.
* - dropdown_actions - Actions that appear in the drop-down
* sub-component.
*/
protected function alterWidgetDeltaActions(
array $form,
FormStateInterface $form_state,
FieldItemListInterface $items,
int $delta,
array $element,
MediaItemAdapterInterface $wrapped_media,
array &$widget_actions): void {
$parents = $element['#field_parents'];
$original_widget_state =
$this->getCurrentWidgetState($form_state, $parents);
$context = [
'form' => $form,
'widget' => $original_widget_state,
'items' => $items,
'delta' => $delta,
'element' => $element,
'form_state' => $form_state,
'wrapper' => $wrapped_media,
'is_translating' => $this->isTranslating,
'allow_reference_changes' => $this->allowReferenceChanges(),
];
// Allow modules to alter widget actions.
$this->moduleHandler->alter(
'imf_widget_delta_actions',
$widget_actions,
$context
);
}
/**
* Gets the IDs of all media entities currently referenced by the widget.
*
* @param array $widget_state
* The current widget state.
*
* @return int[]
* The IDs of all media entities that the field references.
*/
protected function getReferencedMediaIds(array $widget_state): array {
return array_reduce(
$widget_state['media'],
function (array $referenced_ids, array $widget_delta_state) {
$entity = $widget_delta_state['entity'] ?? NULL;
$mode = $widget_delta_state['mode'] ?? NULL;
assert(($entity !== NULL) && ($entity instanceof EntityInterface));
if ($mode !== self::EDIT_MODE_REMOVED) {
$referenced_ids[] = $entity->id();
}
return $referenced_ids;
},
[]
);
}
/**
* Massages form values of a delta into the format expected for field values.
*
* @param \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display
* The form display being used for editing this delta.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param array $delta_element
* The widget form element for this delta.
* @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
* The adapter around the media file for this delta.
* @param array $value
* The submitted form value for this delta, as produced by the widget.
* @param string $edit_mode
* The edit mode of the widget for this particular delta.
*
* @return array
* The field values to use for this delta.
*/
protected function massageDeltaFormValues(
EntityFormDisplayInterface $display,
FormStateInterface $form_state,
array $delta_element,
MediaItemAdapterInterface $wrapped_media,
array $value,
string $edit_mode): array {
$this->copyRevisionMetadata($form_state, $wrapped_media);
if ($edit_mode == self::EDIT_MODE_REMOVED) {
// If our mode is "removed", don't save or reference this entity.
$value['target_id'] = NULL;
}
else {
$media_entity = $wrapped_media->toMediaEntity();
if ($edit_mode == self::EDIT_MODE_OPEN) {
$display->extractFormValues(
$media_entity,
$delta_element['subform'],
$form_state
);
}
// A content entity form saves without any rebuild. It needs to set the
// language to update it in case of language change.
$langcode_key = $media_entity->getEntityType()->getKey('langcode');
$form_langcode = $form_state->get('langcode');
if ($media_entity->get($langcode_key)->value != $form_langcode) {
// If a translation in the given language already exists, switch to
// that. If there is none yet, update the language.
if ($media_entity->hasTranslation($form_langcode)) {
$media_entity = $media_entity->getTranslation($form_langcode);
}
else {
$media_entity->set($langcode_key, $form_langcode);
}
$wrapped_media = MediaItemAdapter::create($media_entity);
}
// We can only use the entity form display to display validation errors
// if it is in edit mode.
if (!$form_state->isValidationComplete()) {
if ($edit_mode === self::EDIT_MODE_OPEN) {
$display->validateFormValues(
$media_entity,
$delta_element['subform'],
$form_state
);
}
elseif ($form_state->getLimitValidationErrors() === NULL) {
// Assume that the entity is being saved/previewed, in this case,
// validate even the closed media files. If there are validation
// errors, add them on the parent level. Validation errors do not
// rebuild the form so it's not possible to un-collapse the form
// at this point.
$violations = $media_entity->validate();
$violations->filterByFieldAccess();
if (!empty($violations)) {
/** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
foreach ($violations as $violation) {
// Ignore text format related validation errors by ignoring
// the .format property.
if (substr($violation->getPropertyPath(), -7) === '.format') {
continue;
}
$form_state->setError(
$delta_element,
$this->t('Validation error on collapsed media file @path: @message',
[
'@path' => $violation->getPropertyPath(),
'@message' => $violation->getMessage(),
]
)
);
}
}
}
}
$value['wrapper'] = $wrapped_media;
$value['entity'] = $wrapped_media->toMediaEntity();
$value['target_id'] = $media_entity->id();
$value['target_revision_id'] = $media_entity->getRevisionId();
}
return $value;
}
/**
* Copies the revision flag and revision log message from the parent entity.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the entity edit form.
* @param \Drupal\inline_media_form\MediaItemAdapterInterface $wrapped_media
* The adapter around the media file receiving the revision info.
*/
protected function copyRevisionMetadata(
FormStateInterface $form_state,
MediaItemAdapterInterface $wrapped_media): void {
// Only create a new revision if we actually have changes.
if ($wrapped_media->isSaveNeeded()) {
$media_entity = $wrapped_media->toMediaEntity();
$create_revision = $form_state->getValue('revision') ?? FALSE;
$revision_log_values = $form_state->getValue('revision_log') ?? [];
$revision_message =
NestedArray::getValue($revision_log_values, [0, 'value']);
$media_entity->setNewRevision($create_revision);
$media_entity->setRevisionCreationTime($this->time->getRequestTime());
$media_entity->setRevisionUserId($this->user->id());
if (empty($revision_message)) {
$media_entity->setRevisionLogMessage(NULL);
}
else {
$media_entity->setRevisionLogMessage($revision_message);
}
}
}
/**
* Builds the state needed to open the media library.
*
* @param array $opener_state
* The state of the media library modal.
* @param string $build_id
* The build ID of the entity edit form array.
*
* @return \Drupal\media_library\MediaLibraryState
* The state to use when opening the media library.
*/
protected static function buildMediaLibraryState(array $opener_state,
string $build_id): MediaLibraryState {
$host_entity = $opener_state['host_entity'];
$field_widget_id = $opener_state['field_widget_id'];
$field_name = $opener_state['field_name'];
$referenced_entity_ids = $opener_state['referenced_entity_ids'];
$allowed_media_type_ids = $opener_state['allowed_media_type_ids'];
$remaining_count = $opener_state['remaining_count'];
$library_view_display = $opener_state['library_view_display'];
$host_entity_type = $host_entity->getEntityType();
// 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-widget-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_type' => static::class,
'field_widget_id' => $field_widget_id,
'entity_type_id' => $host_entity_type->id(),
'bundle' => $host_entity->bundle(),
'field_name' => $field_name,
];
if (!empty($referenced_entity_ids)) {
/** @var \Drupal\Core\TempStore\PrivateTempStoreFactory */
$temp_store_factory = \Drupal::service('tempstore.private');
// Store the referenced IDs in a temp store to avoid cluttering up the
// query string. This also avoids a problem where certain CDNs like Fastly
// will not serve very long URLs.
//
// https://docs.fastly.com/en/guides/resource-limits#request-and-response-limits
$temp_store_id = 'media_library_opener_' . $build_id;
$temp_store = $temp_store_factory->get($temp_store_id);
$temp_store->set('current_ids_selected', $referenced_entity_ids);
$opener_parameters['selected_id_store_id'] = $temp_store_id;
}
// Only add the entity ID when we actually have one. The entity ID needs to
// be a string to ensure that the media library state generates its
// tamper-proof hash in a consistent way.
if (!$host_entity->isNew()) {
$opener_parameters['entity_id'] = (string) $host_entity->id();
if ($host_entity_type->isRevisionable()) {
assert($host_entity instanceof RevisionableInterface);
$opener_parameters['revision_id'] =
(string) $host_entity->getRevisionId();
}
}
$initial_media_type_id = reset($allowed_media_type_ids);
if (self::doesMediaLibrarySupportCustomViewDisplay()) {
[$library_view, $library_display] = explode('.', $library_view_display);
$state = MediaLibraryState::create(
'media_library.opener.field_widget',
$allowed_media_type_ids,
$initial_media_type_id,
$remaining_count,
$opener_parameters,
$library_view,
$library_display
);
}
else {
$state = MediaLibraryState::create(
'media_library.opener.field_widget',
$allowed_media_type_ids,
$initial_media_type_id,
$remaining_count,
$opener_parameters
);
}
return $state;
}
}
