diff --git a/src/Annotation/EntityEmbedDisplay.php b/src/Annotation/EntityEmbedDisplay.php index 0c21b18..3339088 100644 --- a/src/Annotation/EntityEmbedDisplay.php +++ b/src/Annotation/EntityEmbedDisplay.php @@ -55,4 +55,14 @@ class EntityEmbedDisplay extends Plugin { */ public $no_ui = FALSE; + /** + * Alt and title access. + * + * Whether the plugin supports per-embed alt and title overrides for media + * entities with an image source. + * + * @var bool + */ + public $supports_image_alt_and_title = FALSE; + } diff --git a/src/EntityEmbedDisplay/EntityEmbedDisplayBase.php b/src/EntityEmbedDisplay/EntityEmbedDisplayBase.php index a696fbc..3cb86cd 100644 --- a/src/EntityEmbedDisplay/EntityEmbedDisplayBase.php +++ b/src/EntityEmbedDisplay/EntityEmbedDisplayBase.php @@ -237,7 +237,7 @@ abstract class EntityEmbedDisplayBase extends PluginBase implements ContainerFac * The currently set context value. */ public function getContextValue($name) { - return $this->context[$name]; + return !empty($this->context[$name]) ? $this->context[$name] : NULL; } /** @@ -323,6 +323,19 @@ abstract class EntityEmbedDisplayBase extends PluginBase implements ContainerFac return array_key_exists($name, $attributes) ? $attributes[$name] : $default; } + /** + * Checks if an attribute is set. + * + * @param string $name + * The name of the attribute. + * + * @return bool + * Returns TRUE if value is set. + */ + public function hasAttribute($name) { + return array_key_exists($name, $this->getAttributeValues()); + } + /** * Gets the current language code. * diff --git a/src/EntityEmbedDisplay/EntityEmbedDisplayManager.php b/src/EntityEmbedDisplay/EntityEmbedDisplayManager.php index ce560df..51353ee 100644 --- a/src/EntityEmbedDisplay/EntityEmbedDisplayManager.php +++ b/src/EntityEmbedDisplay/EntityEmbedDisplayManager.php @@ -7,6 +7,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\DefaultPluginManager; use Drupal\Component\Plugin\Exception\PluginException; +use Drupal\entity_embed\Plugin\entity_embed\EntityEmbedDisplay\MediaImageDecorator; /** * Provides an Entity Embed display plugin manager. @@ -111,7 +112,12 @@ class EntityEmbedDisplayManager extends DefaultPluginManager { * An array of valid plugin labels, keyed by plugin ID. */ public function getDefinitionOptionsForContext(array $context) { - assert(empty(array_diff_key($context, ['entity' => TRUE, 'entity_type' => TRUE, 'embed_button' => TRUE]))); + $values = [ + 'entity' => TRUE, + 'entity_type' => TRUE, + 'embed_button' => TRUE + ]; + assert(empty(array_diff_key($context, $values))); $definitions = $this->getDefinitionsForContexts($context); $definitions = $this->filterExposedDefinitions($definitions); $options = array_map(function ($definition) { @@ -176,4 +182,21 @@ class EntityEmbedDisplayManager extends DefaultPluginManager { }, $definitions); } + /** + * {@inheritdoc} + */ + public function createInstance($plugin_id, array $configuration = []) { + $instance = parent::createInstance($plugin_id, $configuration); + $definition = $instance->getPluginDefinition(); + + if (empty($definition['supports_image_alt_and_title'])) { + return $instance; + } + else { + // Use decorator pattern to add alt and title fields to dialog when + // embedding media with image source. + return new MediaImageDecorator($instance); + } + } + } diff --git a/src/EntityEmbedDisplay/FieldFormatterEntityEmbedDisplayBase.php b/src/EntityEmbedDisplay/FieldFormatterEntityEmbedDisplayBase.php index 038bba2..364c345 100644 --- a/src/EntityEmbedDisplay/FieldFormatterEntityEmbedDisplayBase.php +++ b/src/EntityEmbedDisplay/FieldFormatterEntityEmbedDisplayBase.php @@ -169,6 +169,11 @@ abstract class FieldFormatterEntityEmbedDisplayBase extends EntityEmbedDisplayBa $formatter = $this->getFieldFormatter(); $formatter->prepareView([$fakeEntity->id() => $items]); $build = $formatter->viewElements($items, $this->getLangcode()); + // Don't ever cache a representation of an embedded entity, since the host + // entity may be overriding specific values (such as an `alt` attribute) + // which means that this particular rendered representation is unique to the + // host entity, and hence nonsensical to cache separately anyway. + unset($build[0]['#cache']['keys']); // For some reason $build[0]['#printed'] is TRUE, which means it will fail // to render later. So for now we manually fix that. // @todo Investigate why this is needed. diff --git a/src/Form/EntityEmbedDialog.php b/src/Form/EntityEmbedDialog.php index e541cb0..160aa7a 100644 --- a/src/Form/EntityEmbedDialog.php +++ b/src/Form/EntityEmbedDialog.php @@ -488,7 +488,10 @@ class EntityEmbedDialog extends FormBase { ]; $plugin_id = !empty($values['attributes']['data-entity-embed-display']) ? $values['attributes']['data-entity-embed-display'] : $entity_element['data-entity-embed-display']; if (!empty($plugin_id)) { - if (is_string($entity_element['data-entity-embed-display-settings'])) { + if (empty($entity_element['data-entity-embed-display-settings'])) { + $entity_element['data-entity-embed-display-settings'] = []; + } + elseif (is_string($entity_element['data-entity-embed-display-settings'])) { $entity_element['data-entity-embed-display-settings'] = Json::decode($entity_element['data-entity-embed-display-settings']); } $display = $this->entityEmbedDisplayManager->createInstance($plugin_id, $entity_element['data-entity-embed-display-settings']); diff --git a/src/Plugin/Derivative/FieldFormatterDeriver.php b/src/Plugin/Derivative/FieldFormatterDeriver.php index 26a8b06..d420acc 100644 --- a/src/Plugin/Derivative/FieldFormatterDeriver.php +++ b/src/Plugin/Derivative/FieldFormatterDeriver.php @@ -65,9 +65,22 @@ class FieldFormatterDeriver extends DeriverBase implements ContainerDeriverInter if (!isset($base_plugin_definition['field_type'])) { throw new \LogicException("Undefined field_type definition in plugin {$base_plugin_definition['id']}."); } + + $no_media_image_decorator = [ + 'entity_reference_entity_id', + 'entity_reference_label', + ]; + foreach ($this->formatterManager->getOptions($base_plugin_definition['field_type']) as $formatter => $label) { $this->derivatives[$formatter] = $base_plugin_definition; $this->derivatives[$formatter]['label'] = $label; + + // The base entity embed display plugin annotation has opted into + // `supports_image_alt_and_title`. For some derivatives we know that they + // do not support this, so opt them back out. + if (in_array($formatter, $no_media_image_decorator, TRUE)) { + $this->derivatives[$formatter]['supports_image_alt_and_title'] = FALSE; + } } return $this->derivatives; } diff --git a/src/Plugin/Derivative/ViewModeDeriver.php b/src/Plugin/Derivative/ViewModeDeriver.php index f23d70e..ff21b55 100644 --- a/src/Plugin/Derivative/ViewModeDeriver.php +++ b/src/Plugin/Derivative/ViewModeDeriver.php @@ -64,6 +64,9 @@ class ViewModeDeriver extends DeriverBase implements ContainerDeriverInterface { $this->derivatives[$definition['id']]['view_mode'] = $view_mode; $this->derivatives[$definition['id']]['entity_types'] = $definition['targetEntityType']; $this->derivatives[$definition['id']]['no_ui'] = $mode; + if ($definition['targetEntityType'] === 'media') { + $this->derivatives[$definition['id']]['supports_image_alt_and_title'] = TRUE; + } } } return $this->derivatives; diff --git a/src/Plugin/entity_embed/EntityEmbedDisplay/EntityReferenceFieldFormatter.php b/src/Plugin/entity_embed/EntityEmbedDisplay/EntityReferenceFieldFormatter.php index ef5793d..8909dda 100644 --- a/src/Plugin/entity_embed/EntityEmbedDisplay/EntityReferenceFieldFormatter.php +++ b/src/Plugin/entity_embed/EntityEmbedDisplay/EntityReferenceFieldFormatter.php @@ -21,7 +21,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * id = "entity_reference", * label = @Translation("Entity Reference"), * deriver = "Drupal\entity_embed\Plugin\Derivative\FieldFormatterDeriver", - * field_type = "entity_reference" + * field_type = "entity_reference", + * supports_image_alt_and_title = TRUE * ) */ class EntityReferenceFieldFormatter extends FieldFormatterEntityEmbedDisplayBase { diff --git a/src/Plugin/entity_embed/EntityEmbedDisplay/ImageFieldFormatter.php b/src/Plugin/entity_embed/EntityEmbedDisplay/ImageFieldFormatter.php index 034e714..c175ded 100644 --- a/src/Plugin/entity_embed/EntityEmbedDisplay/ImageFieldFormatter.php +++ b/src/Plugin/entity_embed/EntityEmbedDisplay/ImageFieldFormatter.php @@ -178,7 +178,7 @@ class ImageFieldFormatter extends FileFieldFormatter { // double quotes in place of empty alt text only if that was filled // intentionally by the user. if (!empty($entity_element) && $entity_element['data-entity-embed-display'] == 'image:image') { - $alt = '""'; + $alt = MediaImageDecorator::EMPTY_STRING; } } @@ -211,7 +211,7 @@ class ImageFieldFormatter extends FileFieldFormatter { public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { // When the alt attribute is set to two double quotes, transform it to the // empty string: two double quotes signify "empty alt attribute". See above. - if (trim($form_state->getValue(['attributes', 'alt'])) === '""') { + if (trim($form_state->getValue(['attributes', 'alt'])) === MediaImageDecorator::EMPTY_STRING) { $form_state->setValue(['attributes', 'alt'], ''); } } diff --git a/src/Plugin/entity_embed/EntityEmbedDisplay/MediaImageDecorator.php b/src/Plugin/entity_embed/EntityEmbedDisplay/MediaImageDecorator.php new file mode 100644 index 0000000..9c9e43f --- /dev/null +++ b/src/Plugin/entity_embed/EntityEmbedDisplay/MediaImageDecorator.php @@ -0,0 +1,269 @@ +decorated = $decorated; + } + + /** + * Passes through all unknown calls to the decorated object. + */ + public function __call($method, $args) { + return call_user_func_array([$this->decorated, $method], $args); + } + + /** + * {@inheritdoc} + */ + public function access(AccountInterface $account = NULL) { + return $this->decorated->access($account); + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + return $this->decorated->validateConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return $this->decorated->defaultConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + return $this->decorated->calculateDependencies(); + } + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return $this->decorated->getConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function getPluginDefinition() { + return $this->decorated->getPluginDefinition(); + } + + /** + * {@inheritdoc} + */ + public function getPluginId() { + return $this->decorated->getPluginId(); + } + + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + return $this->decorated->setConfiguration($configuration); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = $this->decorated->buildConfigurationForm($form, $form_state); + + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $this->decorated->getEntityFromContext(); + + if ($image_field = $this->getMediaImageSourceField($entity)) { + + $settings = $entity->{$image_field}->getItemDefinition()->getSettings(); + $attributes = $this->getAttributeValues(); + + $alt = isset($attributes['alt']) ? $attributes['alt'] : $entity->{$image_field}->alt; + $title = isset($attributes['title']) ? $attributes['title'] : $entity->{$image_field}->title; + + // Setting empty alt to double quotes. See ImageFieldFormatter. + if ($settings['alt_field_required'] && $alt === '') { + $alt = static::EMPTY_STRING; + } + + if (!empty($settings['alt_field'])) { + // Add support for editing the alternate and title text attributes. + $form['alt'] = [ + '#type' => 'textfield', + '#title' => $this->t('Alternate text'), + '#default_value' => $alt, + '#description' => $this->t('This text will be used by screen readers, search engines, or when the image cannot be loaded.'), + '#required' => $settings['alt_field_required'], + '#required_error' => $this->t('Alternative text is required.
(Only in rare cases should this be left empty. To create empty alternative text, enter "" — two double quotes without any content).'), + '#maxlength' => 512, + ]; + } + + if (!empty($settings['title_field'])) { + $form['title'] = [ + '#type' => 'textfield', + '#title' => $this->t('Title'), + '#default_value' => $title, + '#description' => t('The title is used as a tool tip when the user hovers the mouse over the image.'), + '#maxlength' => 1024, + '#required' => $settings['title_field_required'], + ]; + } + } + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $this->decorated->getEntityFromContext(); + if ($image_field = $this->getMediaImageSourceField($entity)) { + $settings = $entity->{$image_field}->getItemDefinition()->getSettings(); + $values = $form_state->getValue(['attributes', 'data-entity-embed-display-settings']); + + if (!empty($settings['alt_field'])) { + // When the alt attribute is set to two double quotes, transform it to + // the empty string: two double quotes signify "empty alt attribute". + // See ImagefieldFormatter. + if (trim($values['alt']) === static::EMPTY_STRING) { + $values['alt'] = static::EMPTY_STRING; + } + // If the alt text is unchanged from the values set on the + // field, there's no need for the alt property to be set. + elseif ($values['alt'] === $entity->{$image_field}->alt) { + $values['alt'] = ''; + } + + $form_state->setValue(['attributes', 'alt'], $values['alt']); + $form_state->unsetValue([ + 'attributes', + 'data-entity-embed-display-settings', + 'alt', + ]); + } + + if (!empty($settings['title_field'])) { + if (empty($values['title'])) { + $values['title'] = ''; + } + // If the title text is unchanged from the values set on the + // field, there's no need for the title property to be set. + elseif ($values['title'] === $entity->{$image_field}->title) { + $values['title'] = ''; + } + + $form_state->setValue(['attributes', 'title'], $values['title']); + $form_state->unsetValue([ + 'attributes', + 'data-entity-embed-display-settings', + 'title', + ]); + } + } + $this->decorated->submitConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function build() { + $build = $this->decorated->build(); + + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $this->decorated->getEntityFromContext(); + + if ($image_field = $this->getMediaImageSourceField($entity)) { + $settings = $entity->{$image_field}->getItemDefinition()->getSettings(); + + if (!empty($settings['alt_field']) && $this->hasAttribute('alt')) { + $entity->{$image_field}->alt = $this->getAttributeValue('alt'); + $entity->thumbnail->alt = $this->getAttributeValue('alt'); + } + + if (!empty($settings['title_field']) && $this->hasAttribute('title')) { + $entity->{$image_field}->title = $this->getAttributeValue('title'); + $entity->thumbnail->title = $this->getAttributeValue('title'); + } + } + + return $build; + } + + /** + * Get image field from source config. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * Embedded entity. + * + * @return string|null + * String of image field name. + */ + protected function getMediaImageSourceField(EntityInterface $entity) { + if (!$entity instanceof MediaInterface) { + return NULL; + } + + try { + $field_definition = $entity->getSource() + ->getSourceFieldDefinition($entity->bundle->entity); + $field_name = $field_definition->getName(); + $field = $entity->get($field_name); + $item = $field->first(); + if (!empty($item) && $item instanceof ImageItem) { + // Check that either alt field or title field is enabled. + if ($field_definition->getSetting('alt_field') || $field_definition->getSetting('title_field')) { + return $field_name; + } + } + } + catch (\Exception $e) { + return NULL; + } + + return NULL; + } + +} diff --git a/tests/modules/entity_embed_translation_test/config/install/core.entity_form_display.node.article.default.yml b/tests/modules/entity_embed_test/config/install/core.entity_form_display.node.article.default.yml similarity index 100% rename from tests/modules/entity_embed_translation_test/config/install/core.entity_form_display.node.article.default.yml rename to tests/modules/entity_embed_test/config/install/core.entity_form_display.node.article.default.yml diff --git a/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.embed.yml b/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.embed.yml new file mode 100644 index 0000000..60ae55d --- /dev/null +++ b/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.embed.yml @@ -0,0 +1,29 @@ +langcode: en +status: true +dependencies: + config: + - field.field.media.image.field_media_image + - image.style.medium + - media.type.image + module: + - image + - user +id: media.image.default +targetEntityType: media +bundle: image +mode: embed +content: + field_media_image: + type: image + weight: 2 + region: content + label: hidden + settings: + image_style: medium + image_link: '' + third_party_settings: { } +hidden: + name: true + thumbnail: true + created: true + uid: true diff --git a/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.thumb.yml b/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.thumb.yml new file mode 100644 index 0000000..4c4338c --- /dev/null +++ b/tests/modules/entity_embed_test/config/install/core.entity_view_display.media.image.thumb.yml @@ -0,0 +1,28 @@ +langcode: en +status: true +dependencies: + config: + - image.style.medium + - media.type.image + module: + - image + - user +id: media.image.default +targetEntityType: media +bundle: image +mode: thumb +content: + thumbnail: + type: image + weight: 2 + region: content + label: hidden + settings: + image_style: medium + image_link: '' + third_party_settings: { } +hidden: + name: true + field_media_image: true + created: true + uid: true diff --git a/tests/modules/entity_embed_translation_test/config/install/core.entity_view_display.node.article.default.yml b/tests/modules/entity_embed_test/config/install/core.entity_view_display.node.article.default.yml similarity index 100% rename from tests/modules/entity_embed_translation_test/config/install/core.entity_view_display.node.article.default.yml rename to tests/modules/entity_embed_test/config/install/core.entity_view_display.node.article.default.yml diff --git a/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.embed.yml b/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.embed.yml new file mode 100644 index 0000000..6a87a05 --- /dev/null +++ b/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.embed.yml @@ -0,0 +1,9 @@ +langcode: en +status: false +dependencies: + module: + - media +id: media.embed +label: 'Embed' +targetEntityType: media +cache: true diff --git a/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.thumb.yml b/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.thumb.yml new file mode 100644 index 0000000..54f487c --- /dev/null +++ b/tests/modules/entity_embed_test/config/install/core.entity_view_mode.media.thumb.yml @@ -0,0 +1,9 @@ +langcode: en +status: false +dependencies: + module: + - media +id: media.thumb +label: 'Thumb (View Mode)' +targetEntityType: media +cache: true diff --git a/tests/modules/entity_embed_translation_test/config/install/editor.editor.full_html.yml b/tests/modules/entity_embed_test/config/install/editor.editor.full_html.yml similarity index 100% rename from tests/modules/entity_embed_translation_test/config/install/editor.editor.full_html.yml rename to tests/modules/entity_embed_test/config/install/editor.editor.full_html.yml diff --git a/tests/modules/entity_embed_translation_test/config/install/embed.button.test_media_entity_embed.yml b/tests/modules/entity_embed_test/config/install/embed.button.test_media_entity_embed.yml similarity index 100% rename from tests/modules/entity_embed_translation_test/config/install/embed.button.test_media_entity_embed.yml rename to tests/modules/entity_embed_test/config/install/embed.button.test_media_entity_embed.yml diff --git a/tests/modules/entity_embed_translation_test/config/install/embed.button.test_node.yml b/tests/modules/entity_embed_test/config/install/embed.button.test_node.yml similarity index 100% rename from tests/modules/entity_embed_translation_test/config/install/embed.button.test_node.yml rename to tests/modules/entity_embed_test/config/install/embed.button.test_node.yml diff --git a/tests/modules/entity_embed_translation_test/config/install/field.field.media.image.field_media_image.yml b/tests/modules/entity_embed_test/config/install/field.field.media.image.field_media_image.yml similarity index 100% rename from tests/modules/entity_embed_translation_test/config/install/field.field.media.image.field_media_image.yml rename to tests/modules/entity_embed_test/config/install/field.field.media.image.field_media_image.yml diff --git a/tests/modules/entity_embed_translation_test/config/install/field.field.node.article.body.yml b/tests/modules/entity_embed_test/config/install/field.field.node.article.body.yml similarity index 100% rename from tests/modules/entity_embed_translation_test/config/install/field.field.node.article.body.yml rename to tests/modules/entity_embed_test/config/install/field.field.node.article.body.yml diff --git a/tests/modules/entity_embed_translation_test/config/install/field.storage.media.field_media_image.yml b/tests/modules/entity_embed_test/config/install/field.storage.media.field_media_image.yml similarity index 100% rename from tests/modules/entity_embed_translation_test/config/install/field.storage.media.field_media_image.yml rename to tests/modules/entity_embed_test/config/install/field.storage.media.field_media_image.yml diff --git a/tests/modules/entity_embed_translation_test/config/install/filter.format.full_html.yml b/tests/modules/entity_embed_test/config/install/filter.format.full_html.yml similarity index 100% rename from tests/modules/entity_embed_translation_test/config/install/filter.format.full_html.yml rename to tests/modules/entity_embed_test/config/install/filter.format.full_html.yml diff --git a/tests/modules/entity_embed_translation_test/config/install/media.type.image.yml b/tests/modules/entity_embed_test/config/install/media.type.image.yml similarity index 100% rename from tests/modules/entity_embed_translation_test/config/install/media.type.image.yml rename to tests/modules/entity_embed_test/config/install/media.type.image.yml diff --git a/tests/modules/entity_embed_translation_test/config/install/node.type.article.yml b/tests/modules/entity_embed_test/config/install/node.type.article.yml similarity index 100% rename from tests/modules/entity_embed_translation_test/config/install/node.type.article.yml rename to tests/modules/entity_embed_test/config/install/node.type.article.yml diff --git a/tests/modules/entity_embed_test/entity_embed_test.info.yml b/tests/modules/entity_embed_test/entity_embed_test.info.yml index 45e7865..9b44c30 100644 --- a/tests/modules/entity_embed_test/entity_embed_test.info.yml +++ b/tests/modules/entity_embed_test/entity_embed_test.info.yml @@ -3,3 +3,13 @@ type: module description: 'Support module for the Entity Embed module tests.' core: 8.x package: Testing +dependencies: + - drupal:file + - drupal:image + - drupal:node + - drupal:text + - drupal:media + - drupal:ckeditor + - drupal:editor + - embed:embed + - entity_embed:entity_embed diff --git a/tests/src/FunctionalJavascript/EntityEmbedTestBase.php b/tests/src/FunctionalJavascript/EntityEmbedTestBase.php index 2c23dbf..113e7a6 100644 --- a/tests/src/FunctionalJavascript/EntityEmbedTestBase.php +++ b/tests/src/FunctionalJavascript/EntityEmbedTestBase.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\entity_embed\FunctionalJavascript; +use Drupal\Component\Utility\Html; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; /** @@ -57,4 +58,23 @@ JS; $this->getSession()->wait($timeout, $condition); } + /** + * Creates an embed code with given attributes. + * + * @param array $attributes + * The attributes to add. + * + * @return string + * A string containing a element with the given attributes. + */ + protected function createEmbedCode(array $attributes) { + $dom = Html::load('This placeholder should not be rendered.'); + $xpath = new \DOMXPath($dom); + $drupal_entity = $xpath->query('//drupal-entity')[0]; + foreach ($attributes as $attribute => $value) { + $drupal_entity->setAttribute($attribute, $value); + } + return Html::serialize($dom); + } + } diff --git a/tests/src/FunctionalJavascript/MediaImageTest.php b/tests/src/FunctionalJavascript/MediaImageTest.php new file mode 100644 index 0000000..f6df168 --- /dev/null +++ b/tests/src/FunctionalJavascript/MediaImageTest.php @@ -0,0 +1,516 @@ +adminUser = $this->drupalCreateUser([ + 'use text format full_html', + 'administer nodes', + 'edit any article content', + ]); + } + + /** + * Tests alt and title overriding for embedded images. + */ + public function testAltAndTitle() { + \Drupal::service('file_system')->copy($this->root . '/core/misc/druplicon.png', 'public://example.jpg'); + /** @var \Drupal\file\FileInterface $file */ + $file = File::create([ + 'uri' => 'public://example.jpg', + 'uid' => $this->adminUser->id(), + ]); + $file->save(); + + $this->createNode([ + 'type' => 'article', + 'title' => 'Red-lipped batfish', + ]); + + $media = Media::create([ + 'bundle' => 'image', + 'name' => 'Screaming hairy armadillo', + 'field_media_image' => [ + [ + 'target_id' => $file->id(), + 'alt' => 'default alt', + 'title' => 'default title', + ], + ], + ]); + $media->save(); + + $host = $this->createNode([ + 'type' => 'article', + 'title' => 'Animals with strange names', + 'body' => [ + 'value' => '', + 'format' => 'full_html', + ], + ]); + + $this->drupalLogin($this->adminUser); + $this->drupalGet('node/' . $host->id() . '/edit'); + $this->waitForEditor(); + + $this->assignNameToCkeditorIframe(); + + $this->assertSession() + ->waitForElementVisible('css', 'a.cke_button__test_node') + ->click(); + $this->assertSession()->waitForId('drupal-modal'); + + // Test that node embed doesn't display alt and title fields. + $this->assertSession() + ->fieldExists('entity_id') + ->setValue('Red-lipped batfish (1)'); + $this->assertSession()->elementExists('css', 'button.js-button-next')->click(); + $form = $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed'); + + // Assert that the review step displays the selected entity with the label. + $text = $form->getText(); + $this->assertContains('Red-lipped batfish', $text); + + $select = $this->assertSession() + ->selectExists('attributes[data-entity-embed-display]'); + + $select->setValue('view_mode:node.full'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + // The view_mode:node.full display shouldn't have alt and title fields. + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][alt]'); + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][title]'); + + $select = $this->assertSession() + ->selectExists('attributes[data-entity-embed-display]'); + + $select->setValue('view_mode:node.teaser'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + // The view_mode:node.teaser display shouldn't have alt and title fields. + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][alt]'); + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][title]'); + + // Close the dialog. + $this->assertSession()->elementExists('css', '.ui-dialog-titlebar-close')->press(); + + // Now test with media. + $this->assertSession() + ->waitForElementVisible('css', 'a.cke_button__test_media_entity_embed') + ->click(); + $this->assertSession()->waitForId('drupal-modal'); + + $this->assertSession() + ->fieldExists('entity_id') + ->setValue('Screaming hairy armadillo (1)'); + $this->assertSession()->elementExists('css', 'button.js-button-next')->click(); + $form = $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed'); + + // Assert that the review step displays the selected entity with the label. + $text = $form->getText(); + $this->assertContains('Screaming hairy armadillo', $text); + + $select = $this->assertSession() + ->selectExists('attributes[data-entity-embed-display]'); + + $select->setValue('entity_reference:entity_reference_entity_id'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + // The entity_reference:entity_reference_entity_id display shouldn't have + // alt and title fields. + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][alt]'); + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][title]'); + + $select->setValue('entity_reference:entity_reference_label'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + // The entity_reference:entity_reference_label display shouldn't have alt + // and title fields. + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][alt]'); + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][title]'); + + // Test the entity embed display that ships with core media. + $select->setValue('entity_reference:media_thumbnail'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + $this->assertSession() + ->selectExists('attributes[data-entity-embed-display]') + ->setValue('view_mode:media.embed'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $alt = $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][alt]'); + $this->assertEquals($media->field_media_image->alt, $alt->getValue()); + $title = $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][title]'); + $this->assertEquals($media->field_media_image->title, $title->getValue()); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals("default alt", $img->getAttribute('alt')); + $this->assertEquals("default title", $img->getAttribute('title')); + + $this->reopenDialog(); + + $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][alt]') + ->setValue('Satanic leaf-tailed gecko alt'); + $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][title]') + ->setValue('Satanic leaf-tailed gecko title'); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals("Satanic leaf-tailed gecko alt", $img->getAttribute('alt')); + $this->assertEquals("Satanic leaf-tailed gecko title", $img->getAttribute('title')); + + $this->reopenDialog(); + + // Test a view mode that displays thumbnail field. + $select->setValue('view_mode:media.thumb'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + $this->assertSession() + ->selectExists('attributes[data-entity-embed-display]') + ->setValue('view_mode:media.embed'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $alt = $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][alt]'); + $this->assertEquals('Satanic leaf-tailed gecko alt', $alt->getValue()); + $title = $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][title]'); + $this->assertEquals('Satanic leaf-tailed gecko title', $title->getValue()); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals('Satanic leaf-tailed gecko alt', $img->getAttribute('alt')); + $this->assertEquals('Satanic leaf-tailed gecko title', $img->getAttribute('title')); + + $this->reopenDialog(); + + $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][alt]') + ->setValue('Goblin shark alt'); + $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][title]') + ->setValue('Goblin shark title'); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals("Goblin shark alt", $img->getAttribute('alt')); + $this->assertEquals("Goblin shark title", $img->getAttribute('title')); + + $this->reopenDialog(); + + // Test a view mode that displays the media's image field. + $select->setValue('view_mode:media.embed'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + // Test that the view_mode:media.embed display has alt and title fields, + // and that the default values match the values on the media's + // source image field. + $this->assertSession() + ->selectExists('attributes[data-entity-embed-display]') + ->setValue('view_mode:media.embed'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $alt = $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][alt]'); + $this->assertEquals("Goblin shark alt", $alt->getValue()); + $title = $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][title]'); + $this->assertEquals("Goblin shark title", $title->getValue()); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals("Goblin shark alt", $img->getAttribute('alt')); + $this->assertEquals("Goblin shark title", $img->getAttribute('title')); + + $this->reopenDialog(); + + $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][alt]') + ->setValue('Satanic leaf-tailed gecko alt'); + $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][title]') + ->setValue('Satanic leaf-tailed gecko title'); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals('Satanic leaf-tailed gecko alt', $img->getAttribute('alt')); + $this->assertEquals('Satanic leaf-tailed gecko title', $img->getAttribute('title')); + + $this->config('field.field.media.image.field_media_image') + ->set('settings.alt_field', FALSE) + ->set('settings.title_field', FALSE) + ->save(); + + $field = FieldConfig::load('media.image.field_media_image'); + $settings = $field->getSettings(); + $settings['alt_field'] = FALSE; + $settings['title_field'] = FALSE; + $field->set('settings', $settings); + $field->save(); + + drupal_flush_all_caches(); + + $this->reopenDialog(); + + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][alt]'); + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][title]'); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals('default alt', $img->getAttribute('alt')); + $this->assertEquals('default title', $img->getAttribute('title')); + + $field = FieldConfig::load('media.image.field_media_image'); + $settings = $field->getSettings(); + $settings['alt_field'] = TRUE; + $field->set('settings', $settings); + $field->save(); + + drupal_flush_all_caches(); + + $this->reopenDialog(); + + // Test that when only the alt field is enabled, only alt field should + // display. + $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][alt]')->setValue('Satanic leaf-tailed gecko alt'); + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][title]'); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals('Satanic leaf-tailed gecko alt', $img->getAttribute('alt')); + $this->assertEquals('default title', $img->getAttribute('title')); + + $field = FieldConfig::load('media.image.field_media_image'); + $settings = $field->getSettings(); + $settings['alt_field'] = FALSE; + $settings['title_field'] = TRUE; + $field->set('settings', $settings); + $field->save(); + + drupal_flush_all_caches(); + + $this->reopenDialog(); + + // With only title field enabled, only title field should display. + $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][title]')->setValue('Satanic leaf-tailed gecko title'); + $this->assertSession() + ->fieldNotExists('attributes[data-entity-embed-display-settings][alt]'); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEquals('Satanic leaf-tailed gecko title', $img->getAttribute('title')); + $this->assertEquals('default alt', $img->getAttribute('alt')); + + $field = FieldConfig::load('media.image.field_media_image'); + $settings = $field->getSettings(); + $settings['alt_field'] = TRUE; + $settings['title_field'] = TRUE; + $settings['alt_field_required'] = FALSE; + $settings['title_field_required'] = TRUE; + $field->set('settings', $settings); + $field->save(); + + drupal_flush_all_caches(); + + $this->reopenDialog(); + + $alt = $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][alt]'); + $this->assertFalse($alt->hasAttribute('required')); + $title = $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][title]'); + $this->assertTrue($title->hasAttribute('required')); + + $this->submitDialog(); + + $field = FieldConfig::load('media.image.field_media_image'); + $settings = $field->getSettings(); + $settings['alt_field_required'] = TRUE; + $settings['title_field_required'] = FALSE; + $field->set('settings', $settings); + $field->save(); + + drupal_flush_all_caches(); + + $this->reopenDialog(); + + $alt = $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][alt]'); + $this->assertTrue($alt->hasAttribute('required')); + $title = $this->assertSession() + ->fieldExists('attributes[data-entity-embed-display-settings][title]'); + $this->assertFalse($title->hasAttribute('required')); + + // Test that setting value to double quote will allow setting the alt + // and title to empty. + $alt->setValue(MediaImageDecorator::EMPTY_STRING); + $title->setValue(MediaImageDecorator::EMPTY_STRING); + + $this->submitDialog(); + + $img = $this->assertSession()->elementExists('css', 'img'); + $this->assertEmpty($img->getAttribute('alt')); + $this->assertEmpty($img->getAttribute('title')); + + // Test the same embed with different alt and title text. + $input = $this->createEmbedCode([ + 'alt' => 'alt 1', + 'title' => 'title 1', + 'data-embed-button' => 'test_media_entity_embed', + 'data-entity-embed-display' => 'view_mode:media.embed', + 'data-entity-embed-display-settings' => '', + 'data-entity-type' => 'media', + 'data-entity-uuid' => $media->uuid(), + 'data-langcode' => 'en', + ]); + $input .= $this->createEmbedCode([ + 'alt' => 'alt 2', + 'title' => 'title 2', + 'data-embed-button' => 'test_media_entity_embed', + 'data-entity-embed-display' => 'view_mode:media.embed', + 'data-entity-embed-display-settings' => '', + 'data-entity-type' => 'media', + 'data-entity-uuid' => $media->uuid(), + 'data-langcode' => 'en', + ]); + $input .= $this->createEmbedCode([ + 'alt' => 'alt 3', + 'title' => 'title 3', + 'data-embed-button' => 'test_media_entity_embed', + 'data-entity-embed-display' => 'view_mode:media.embed', + 'data-entity-embed-display-settings' => '', + 'data-entity-type' => 'media', + 'data-entity-uuid' => $media->uuid(), + 'data-langcode' => 'en', + ]); + + $this->getSession()->switchToIFrame(); + + $this->assertSession() + ->waitForElementVisible('css', 'a.cke_button__source') + ->click(); + + $source = $this->assertSession() + ->waitForElementVisible('xpath', "//textarea[contains(@class, 'cke_source')]"); + $source->setValue($input); + + // Exit "source" mode. + $this->assertSession() + ->waitForElementVisible('css', 'a.cke_button__source') + ->click(); + + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assignNameToCkeditorIframe(); + $this->getSession()->switchToIFrame('ckeditor'); + + $img = $this->assertSession()->waitForElement('xpath', "//img[contains(@alt, 'alt 1')]"); + $this->assertEquals('alt 1', $img->getAttribute('alt')); + $this->assertEquals('title 1', $img->getAttribute('title')); + + $img = $this->assertSession()->elementExists('xpath', "//img[contains(@alt, 'alt 2')]"); + $this->assertEquals('alt 2', $img->getAttribute('alt')); + $this->assertEquals('title 2', $img->getAttribute('title')); + + $img = $this->assertSession()->elementExists('xpath', "//img[contains(@alt, 'alt 3')]"); + $this->assertEquals('alt 3', $img->getAttribute('alt')); + $this->assertEquals('title 3', $img->getAttribute('title')); + + // Save the host entity. + $this->getSession()->switchToIFrame(); + $this->assertSession()->buttonExists('Save')->press(); + + $img = $this->assertSession()->waitForElement('xpath', "//img[contains(@alt, 'alt 1')]"); + $this->assertEquals('alt 1', $img->getAttribute('alt')); + $this->assertEquals('title 1', $img->getAttribute('title')); + + $img = $this->assertSession()->elementExists('xpath', "//img[contains(@alt, 'alt 2')]"); + $this->assertEquals('alt 2', $img->getAttribute('alt')); + $this->assertEquals('title 2', $img->getAttribute('title')); + + $img = $this->assertSession()->elementExists('xpath', "//img[contains(@alt, 'alt 3')]"); + $this->assertEquals('alt 3', $img->getAttribute('alt')); + $this->assertEquals('title 3', $img->getAttribute('title')); + } + + /** + * Helper function to reopen EntityEmbedDialog for first embed. + */ + protected function reopenDialog() { + $this->getSession()->switchToIFrame(); + $select_and_edit_embed = <<getSession()->executeScript($select_and_edit_embed); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession()->waitForElementVisible('css', 'form.entity-embed-dialog-step--embed'); + } + + /** + * Helper function to submit dialog and focus on ckeditor frame. + */ + protected function submitDialog() { + $this->assertSession()->elementExists('css', 'button.button--primary')->press(); + $this->assertSession()->assertWaitOnAjaxRequest(); + + // Verify that the embedded entity preview in CKEditor displays the image + // with the default alt and title. + $this->getSession()->switchToIFrame('ckeditor'); + } + +}