diff --git a/js/patternkit.jsoneditor.js b/js/patternkit.jsoneditor.js index b49fb07f71362f81e773c7c85d554df4bc545c4e..56645e2c0a0801c75496c406558311a488b43f2e 100644 --- a/js/patternkit.jsoneditor.js +++ b/js/patternkit.jsoneditor.js @@ -307,7 +307,15 @@ patternkitEditorArray(jQuery, Drupal, JSONEditor); } // Ajax command response to allow updating Editor field values. Drupal.AjaxCommands.prototype.patternkitEditorUpdate = function (ajax, response, status) { - window.patternkitEditor.getEditor(response.selector).setValue(response.value); + let editor = window.patternkitEditor.getEditor(response.selector); + + if (editor) { + editor.setValue(response.value); + } + else { + window.console?.debug('Unable to find an editor at "%s" to assign value "%s".', + response.selector, response.value); + } }; let $target = $('#patternkit-editor-target', context); diff --git a/modules/patternkit_media_library/patternkit_media_library.services.yml b/modules/patternkit_media_library/patternkit_media_library.services.yml index d2fb1886be6bd609e4fe5c4a5ac695557b422574..7e1df3eeeae4f89d76a94ebe91dae4e64e999143 100644 --- a/modules/patternkit_media_library/patternkit_media_library.services.yml +++ b/modules/patternkit_media_library/patternkit_media_library.services.yml @@ -1,4 +1,11 @@ services: + _defaults: + autoconfigure: true + + logger.channel.patternkit_media_library: + parent: logger.channel_base + arguments: [ 'patternkit_media_library' ] + patternkit.opener.jsonlibrary: class: Drupal\patternkit_media_library\MediaLibraryJSONLibraryOpener arguments: diff --git a/modules/patternkit_media_library/src/MediaLibraryJSONLibraryOpener.php b/modules/patternkit_media_library/src/MediaLibraryJSONLibraryOpener.php index 2933503d1e97abafd42d77304a0033b92fcca4cd..953717d6291422bc29c593022fdd761373f04d67 100644 --- a/modules/patternkit_media_library/src/MediaLibraryJSONLibraryOpener.php +++ b/modules/patternkit_media_library/src/MediaLibraryJSONLibraryOpener.php @@ -4,61 +4,41 @@ namespace Drupal\patternkit_media_library; use Drupal\Core\Access\AccessResult; use Drupal\Core\Ajax\AjaxResponse; -use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Ajax\CloseDialogCommand; +use Drupal\Core\Ajax\MessageCommand; +use Drupal\Core\Ajax\ScrollTopCommand; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\File\FileUrlGeneratorInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Template\Attribute; +use Drupal\Core\Utility\Error; +use Drupal\file\FileInterface; use Drupal\media\MediaInterface; use Drupal\media_library\MediaLibraryOpenerInterface; use Drupal\media_library\MediaLibraryState; use Drupal\patternkit\AJAX\PatternkitEditorUpdateCommand; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; /** * The media library opener for field widgets. */ -class MediaLibraryJSONLibraryOpener implements MediaLibraryOpenerInterface { +class MediaLibraryJSONLibraryOpener implements MediaLibraryOpenerInterface, LoggerAwareInterface { - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected EntityTypeManagerInterface $entityTypeManager; - - /** - * The file url generator service. - * - * @var \Drupal\Core\File\FileUrlGeneratorInterface - */ - protected FileUrlGeneratorInterface $fileUrlGenerator; - - /** - * The configuration factory service. - * - * @var \Drupal\Core\Config\ConfigFactoryInterface - */ - protected ConfigFactoryInterface $configFactory; - - /** - * The loaded settings for the Pattern Media Library module. - * - * @var array - */ - protected array $settings; + use LoggerAwareTrait; /** * MediaLibraryFieldWidgetOpener constructor. * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager * The entity type manager. - * @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator + * @param \Drupal\Core\File\FileUrlGeneratorInterface $fileUrlGenerator * The file url generator service. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, FileUrlGeneratorInterface $file_url_generator) { - $this->entityTypeManager = $entity_type_manager; - $this->fileUrlGenerator = $file_url_generator; - } + public function __construct( + protected EntityTypeManagerInterface $entityTypeManager, + protected FileUrlGeneratorInterface $fileUrlGenerator, + ) {} /** * {@inheritdoc} @@ -90,19 +70,81 @@ class MediaLibraryJSONLibraryOpener implements MediaLibraryOpenerInterface { return $response; } try { - /** @var \Drupal\media\Entity\Media $media */ + /** @var \Drupal\media\MediaInterface|null $media */ $media = $this->entityTypeManager->getStorage('media')->load($mid); + // Fail here if the Media entity couldn't be loaded. + if (!($media instanceof MediaInterface)) { + $this->logger->error('Unable to load media entity "@mid" for media opener.', [ + '@mid' => $mid, + ]); + return $response; + } + if ($use_media_token) { - $value = $this->getMediaToken($media); + $media_token = $this->getMediaToken($media); + $response->addCommand(new PatternkitEditorUpdateCommand($widget_id, $media_token)); } else { - $value = $this->getFileUrl($media); + $image_src = $this->getFileUrl($media); + $response->addCommand(new PatternkitEditorUpdateCommand($widget_id, $image_src)); + + // If we're populating the image src, also try to populate the image + // alt text and dimensions. If a schema doesn't match this format for + // an image field, this additional processing will be ignored + // altogether. + if (str_ends_with($widget_id, '.image.src')) { + // Attempt to load the source file to identify additional available + // attributes. + $image = $this->getSourceFile($media); + + // Stop here if the image was not able to be loaded. + if ($image !== NULL) { + // Remove 'src' from the end of the widget path to easily append the + // additional attributes. + $widget_base_path = substr($widget_id, 0, -3); + + $source_configuration = $media->getSource()?->getConfiguration(); + $source_field_name = is_array($source_configuration) ? $source_configuration['source_field'] : NULL; + + if ($source_field_name !== NULL) { + // Images will typically produce the following attributes: 'alt', + // 'height', 'target_id', 'title', and 'width'. + $attributes = $media->get($source_field_name)?->first()?->getValue() ?: []; + foreach ($attributes as $key => $value) { + // Only assign attributes with non-empty values, and ignore the + // internal value for the target_id referencing the file entity. + if ($key != 'target_id' && $value) { + $target = $widget_base_path . $key; + + // If a field is not found in the schema client-side, the + // command should fail silently. + $response->addCommand(new PatternkitEditorUpdateCommand($target, $value)); + } + } + } + } + } } - - $response->addCommand(new PatternkitEditorUpdateCommand($widget_id, $value)); } catch (\Exception $exception) { + // Log the failure server-side for review and visibility. + Error::logException($this->logger, $exception); + + // Display a message client-side for context when the value fails to be + // populated. + $response->addCommand(new MessageCommand( + 'There was a problem selecting the media asset. Save your work.', + '.patternkit-form-messages .messages-list', + ['type' => 'warning'] + )); + + // A redundant close dialog command is added by the + // MediaLibrarySelectForm, but we have to close the media dialog first for + // the scroll command to take effect. + $response->addCommand(new CloseDialogCommand('#drupal-modal')); + $response->addCommand(new ScrollTopCommand('.patternkit-form-messages')); + return $response; } @@ -145,20 +187,39 @@ class MediaLibraryJSONLibraryOpener implements MediaLibraryOpenerInterface { * @throws \Drupal\Core\Entity\EntityMalformedException */ protected function getFileUrl(MediaInterface $media): string { - $fid = $media->getSource()->getSourceFieldValue($media); - $file = $this->entityTypeManager->getStorage('file')->load($fid); + $file = $this->getSourceFile($media); - if ($file->hasLinkTemplate('canonical')) { + if ($file?->hasLinkTemplate('canonical')) { $url = $file->toUrl()->setAbsolute(FALSE); } - elseif ($file->access('download')) { + elseif ($file?->access('download')) { $url = $this->fileUrlGenerator->generateString($file->getFileUri()); } else { - $url = $file->label(); + $url = $file?->label(); } - return $url; + return (string) $url; + } + + /** + * Load the source file entity from the given media. + * + * @param \Drupal\media\MediaInterface $media + * The media entity to load the source file from. + * + * @return \Drupal\file\FileInterface|null + * The loaded source file entity or NULL if the file could not be loaded. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + protected function getSourceFile(MediaInterface $media): ?FileInterface { + $fid = $media->getSource()->getSourceFieldValue($media); + $file = $this->entityTypeManager->getStorage('file')->load($fid); + + assert($file === NULL || $file instanceof FileInterface); + return $file; } } diff --git a/src/Plugin/Block/PatternkitBlock.php b/src/Plugin/Block/PatternkitBlock.php index 465ea01286fa6d9121b0193850561cced0b2ac77..088c0ac7299ed14909e0593c56a5af96507bcfdc 100644 --- a/src/Plugin/Block/PatternkitBlock.php +++ b/src/Plugin/Block/PatternkitBlock.php @@ -266,14 +266,25 @@ class PatternkitBlock extends BlockBase implements ContainerFactoryPluginInterfa // in layout builder and elsewhere. $configuration['reusable'] = $patternkit_block->isNew() ? $is_block_form : $patternkit_block->isReusable(); + // Include a messages list wrapper to display AJAX warnings or messages. + // NB. The class must be added on a theme wrapper since the '#attributes' + // key is lost when the status_messages element renders as a placeholder. + $form['messages'] = [ + '#theme' => 'status_messages', + '#message_list' => [], + '#theme_wrappers' => [ + 'container' => [ + '#attributes' => [ + 'class' => ['patternkit-form-messages'], + ], + ], + ], + ]; + // Warn about accidental changes to reusable blocks. if (isset($configuration['reusable']) && $configuration['reusable']) { - $form['messages'] = [ - '#theme' => 'status_messages', - '#message_list' => [ - 'warning' => [$this->t('This block is reusable! Any changes made will be applied globally.')], - ], - ]; + $form['messages']['#message_list']['warning'][] = + $this->t('This block is reusable! Any changes made will be applied globally.'); } $form['reusable'] = [ @@ -560,7 +571,7 @@ class PatternkitBlock extends BlockBase implements ContainerFactoryPluginInterfa ['@version' => $pattern->getVersion()] ); } - $form['schema_desc'] = ['#markup' => $success_message]; + $form['messages']['#message_list']['status'][] = $success_message; } try {