diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module index f3bb1b531418..d704dd06a6ec 100644 --- a/core/modules/editor/editor.module +++ b/core/modules/editor/editor.module @@ -6,6 +6,7 @@ */ use Drupal\Component\Utility\Html; +use Drupal\Core\Entity\Entity\EntityFormMode; use Drupal\editor\Entity\Editor; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Field\FieldDefinitionInterface; @@ -16,6 +17,8 @@ use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Entity\EntityInterface; use Drupal\filter\FilterFormatInterface; use Drupal\filter\Plugin\FilterInterface; +use Drupal\media\Entity\Media; +use Drupal\media\Entity\MediaType; /** * Implements hook_help(). @@ -252,6 +255,56 @@ function editor_form_filter_admin_format_submit($form, FormStateInterface $form_ } } +/** + * Implements hook_form_FORM_ID_alter(). + */ +function editor_form_media_type_add_form_alter(&$form, &$form_state, $form_id) { + // Add this submit function to the submit _button_ rather than the form's + // '#submit' array, because the latter approach loses our submit function when + // the form is refreshed via ajax (e.g. when you have to choose a source field + // for your media type). + $form['actions']['submit']['#submit'][] = '_editor_add_media_embed_display'; +} + +/** + * A submit function added to the form that adds a media type. This will create + * a form display, containing only this media type's required fields, to be used + * when a user embeds media in the editor. + * + * @param array $form + * The media-type-addition form. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * That form's state. + * + * @internal + */ +function _editor_add_media_embed_display(array &$form, FormStateInterface $form_state) { + $entity_type = 'media'; + $bundle = $form_state->getValue('id'); + $form_mode = 'editor_embed'; + + // Create and enable the form display for this new media type, using the + // editor_embed form mode created during Editor module install. + $form_display = entity_get_form_display($entity_type, $bundle, $form_mode); + + // Show only the required fields; hide all others. + /** @var \Drupal\Core\Field\BaseFieldDefinition $field_definition */ + foreach (\Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type, $bundle) as $field_definition) { + $field_name = $field_definition->getName(); + if ($field_definition->isRequired()) { + $form_display->setComponent($field_name, []); + } + else { + $form_display->removeComponent($field_name); + } + } + + // Enable and save the cleaned-up form display. + $form_display->set('status', TRUE); + $form_display->save(); +} + /** * Loads an individual configured text editor based on text format ID. * diff --git a/core/modules/media/config/install/core.entity_form_mode.media.editor_embed.yml b/core/modules/media/config/install/core.entity_form_mode.media.editor_embed.yml new file mode 100644 index 000000000000..96d67e2790c2 --- /dev/null +++ b/core/modules/media/config/install/core.entity_form_mode.media.editor_embed.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - editor + - media +id: media.editor_embed +label: Editor embed +targetEntityType: media +cache: true diff --git a/core/modules/media/js/plugins/mediaembed/icons/hidpi/mediaembed.png b/core/modules/media/js/plugins/mediaembed/icons/hidpi/mediaembed.png new file mode 100644 index 000000000000..19f663a7b917 Binary files /dev/null and b/core/modules/media/js/plugins/mediaembed/icons/hidpi/mediaembed.png differ diff --git a/core/modules/media/js/plugins/mediaembed/icons/mediaembed.png b/core/modules/media/js/plugins/mediaembed/icons/mediaembed.png new file mode 100644 index 000000000000..07ddc2e0f3d6 Binary files /dev/null and b/core/modules/media/js/plugins/mediaembed/icons/mediaembed.png differ diff --git a/core/modules/media/js/plugins/mediaembed/plugin.js b/core/modules/media/js/plugins/mediaembed/plugin.js new file mode 100644 index 000000000000..9474d1896273 --- /dev/null +++ b/core/modules/media/js/plugins/mediaembed/plugin.js @@ -0,0 +1,227 @@ +/** + * DO NOT EDIT THIS FILE. + * See the following change record for more information, + * https://www.drupal.org/node/2815083 + * @preserve + **/ + +(function ($, Drupal, CKEDITOR) { + + "use strict"; + + CKEDITOR.plugins.add('mediaembed', { + // This plugin requires the Widgets System defined in the 'widget' plugin. + requires: 'widget', + + // The plugin initialization logic goes inside this method. + beforeInit: function (editor) { + // Configure CKEditor DTD for custom drupal-entity element. + // @see https://www.drupal.org/node/2448449#comment-9717735 + var dtd = CKEDITOR.dtd, tagName; + dtd['drupal-entity'] = {'#': 1}; + // Register drupal-entity element as allowed child, in each tag that can + // contain a div element. + for (tagName in dtd) { + if (dtd[tagName].div) { + dtd[tagName]['drupal-entity'] = 1; + } + } + + // Generic command for adding/editing entities of all types. + editor.addCommand('editdrupalentity', { + allowedContent: 'drupal-entity[data-embed-button,data-entity-type,data-entity-uuid,data-entity-embed-display,data-entity-embed-display-settings,data-align,data-caption]', + requiredContent: 'drupal-entity[data-embed-button,data-entity-type,data-entity-uuid,data-entity-embed-display,data-entity-embed-display-settings,data-align,data-caption]', + modes: { wysiwyg : 1 }, + canUndo: true, + exec: function (editor, data) { + data = data || {}; + + var existingElement = getSelectedEmbeddedEntity(editor); + + var existingValues = {}; + existingValues['editor-id'] = editor.element.getId(); + if (existingElement && existingElement.$ && existingElement.$.firstChild) { + var embedDOMElement = existingElement.$.firstChild; + // Populate array with the entity's current attributes. + var attribute = null, attributeName; + for (var key = 0; key < embedDOMElement.attributes.length; key++) { + attribute = embedDOMElement.attributes.item(key); + attributeName = attribute.nodeName.toLowerCase(); + if (attributeName.substring(0, 15) === 'data-cke-saved-') { + continue; + } + existingValues[attributeName] = existingElement.data('cke-saved-' + attributeName) || attribute.nodeValue; + } + } + + var embed_button_id = data.id ? data.id : existingValues['data-embed-button']; + + var dialogSettings = { + dialogClass: 'entity-select-dialog', + resizable: false + }; + + var saveCallback = function (values) { + var entityElement = editor.document.createElement('drupal-entity'); + var attributes = values.attributes; + for (var key in attributes) { + entityElement.setAttribute(key, attributes[key]); + } + + editor.insertHtml(entityElement.getOuterHtml()); + if (existingElement) { + // Detach the behaviors that were attached when the entity content + // was inserted. + Drupal.runEmbedBehaviors('detach', existingElement.$); + existingElement.remove(); + } + }; + + // Open the entity embed dialog for corresponding EmbedButton. + Drupal.ckeditor.openDialog(editor, Drupal.url('media/dialog/' + editor.config.drupal.format + '/' + embed_button_id), existingValues, saveCallback, dialogSettings); + } + }); + + // Register the entity embed widget. + editor.widgets.add('drupalentity', { + // Minimum HTML which is required by this widget to work. + allowedContent: 'drupal-entity[data-entity-type,data-entity-uuid,data-entity-embed-display,data-entity-embed-display-settings,data-align,data-caption]', + requiredContent: 'drupal-entity[data-entity-type,data-entity-uuid,data-entity-embed-display,data-entity-embed-display-settings,data-align,data-caption]', + + // Simply recognize the element as our own. The inner markup if fetched + // and inserted the init() callback, since it requires the actual DOM + // element. + upcast: function (element) { + var attributes = element.attributes; + if (attributes['data-entity-type'] === undefined || (attributes['data-entity-id'] === undefined && attributes['data-entity-uuid'] === undefined) || (attributes['data-view-mode'] === undefined && attributes['data-entity-embed-display'] === undefined)) { + return; + } + // Generate an ID for the element, so that we can use the Ajax + // framework. + element.attributes.id = generateEmbedId(); + return element; + }, + + // Fetch the rendered entity. + init: function () { + /** @type {CKEDITOR.dom.element} */ + var element = this.element; + // Use the Ajax framework to fetch the HTML, so that we can retrieve + // out-of-band assets (JS, CSS...). + var entityEmbedPreview = Drupal.ajax({ + base: element.getId(), + element: element.$, + url: Drupal.url('embed/preview/' + editor.config.drupal.format + '?' + $.param({ + value: element.getOuterHtml() + })), + progress: {type: 'none'}, + // Use a custom event to trigger the call. + event: 'entity_embed_dummy_event' + }); + entityEmbedPreview.execute(); + }, + + // Downcast the element. + downcast: function (element) { + // Only keep the wrapping element. + element.setHtml(''); + // Remove the auto-generated ID. + delete element.attributes.id; + return element; + } + }); + + // Register the toolbar buttons. + if (editor.ui.addButton) { + for (var key in editor.config.MediaEmbed_buttons) { + var button = editor.config.MediaEmbed_buttons[key]; + editor.ui.addButton(button.id, { + label: button.label, + data: button, + allowedContent: 'drupal-entity[!data-entity-type,!data-entity-uuid,!data-entity-embed-display,!data-entity-embed-display-settings,!data-align,!data-caption,!data-embed-button]', + click: function(editor) { + editor.execCommand('editdrupalentity', this.data); + }, + icon: button.image + }); + } + } + + // Register context menu option for editing widget. + if (editor.contextMenu) { + editor.addMenuGroup('drupalentity'); + editor.addMenuItem('drupalentity', { + label: Drupal.t('Edit Entity'), + icon: this.path + 'entity.png', + command: 'editdrupalentity', + group: 'drupalentity' + }); + + editor.contextMenu.addListener(function(element) { + if (isEditableEntityWidget(editor, element)) { + return { drupalentity: CKEDITOR.TRISTATE_OFF }; + } + }); + } + + // Execute widget editing action on double click. + editor.on('doubleclick', function (evt) { + var element = getSelectedEmbeddedEntity(editor) || evt.data.element; + + if (isEditableEntityWidget(editor, element)) { + editor.execCommand('editdrupalentity'); + } + }); + } + }); + + /** + * Get the surrounding drupalentity widget element. + * + * @param {CKEDITOR.editor} editor + */ + function getSelectedEmbeddedEntity(editor) { + var selection = editor.getSelection(); + var selectedElement = selection.getSelectedElement(); + if (isEditableEntityWidget(editor, selectedElement)) { + return selectedElement; + } + + return null; + } + + /** + * Checks if the given element is an editable drupalentity widget. + * + * @param {CKEDITOR.editor} editor + * @param {CKEDITOR.htmlParser.element} element + */ + function isEditableEntityWidget (editor, element) { + var widget = editor.widgets.getByElement(element, true); + if (!widget || widget.name !== 'drupalentity') { + return false; + } + + var button = $(element.$.firstChild).attr('data-embed-button'); + if (!button) { + // If there was no data-embed-button attribute, not editable. + return false; + } + + // The button itself must be valid. + return editor.config.DrupalEntity_buttons.hasOwnProperty(button); + } + + /** + * Generates unique HTML IDs for the widgets. + * + * @returns {string} + */ + function generateEmbedId() { + if (typeof generateEmbedId.counter == 'undefined') { + generateEmbedId.counter = 0; + } + return 'entity-embed-' + generateEmbedId.counter++; + } + +})(jQuery, Drupal, CKEDITOR); diff --git a/core/modules/media/media.module b/core/modules/media/media.module index 296945973ae5..c0891e76b88f 100644 --- a/core/modules/media/media.module +++ b/core/modules/media/media.module @@ -66,6 +66,9 @@ function media_theme() { 'media' => [ 'render element' => 'elements', ], + 'media_embed' => [ + 'render element' => 'element', + ], ]; } @@ -172,3 +175,19 @@ function media_form_field_ui_field_storage_add_form_alter(&$form, FormStateInter $form['add']['new_storage_type']['#weight'] = 0; $form['add']['description_wrapper']['#weight'] = 1; } + +/** + * Prepares variables for the media embed template. + * + * Default template: media-embed.html.twig. + * + * @param array $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #attributes, #children. + */ + function template_preprocess_media_embed(&$variables) { + $variables['element'] += ['#attributes' => []]; + $variables['attributes'] = $variables['element']['#attributes']; + $variables['children'] = $variables['element']['#children']; + } diff --git a/core/modules/media/media.routing.yml b/core/modules/media/media.routing.yml index 9fbadeff2947..b09d07a6e7ae 100644 --- a/core/modules/media/media.routing.yml +++ b/core/modules/media/media.routing.yml @@ -19,3 +19,13 @@ entity.media.revision: requirements: _access_media_revision: 'view' media: \d+ + +media.embed_dialog: + path: '/media/dialog/{editor}/{media_type}' + defaults: + _controller: '\Drupal\media\Controller\MediaEmbedDialog::form' + _title: 'Embed entity' + requirements: + _permission: 'update any media' + options: + _theme: ajax_base_page diff --git a/core/modules/media/src/Controller/MediaEmbedDialog.php b/core/modules/media/src/Controller/MediaEmbedDialog.php new file mode 100644 index 000000000000..8e1f558ec5d5 --- /dev/null +++ b/core/modules/media/src/Controller/MediaEmbedDialog.php @@ -0,0 +1,19 @@ + $media_type, 'uid' => $this->currentUser()->id()]); + $form = $this->entityFormBuilder()->getForm($entity, 'editor_embed'); + return $form; + } + +} diff --git a/core/modules/media/src/Entity/Media.php b/core/modules/media/src/Entity/Media.php index 6e5877193884..c17f45a836e8 100644 --- a/core/modules/media/src/Entity/Media.php +++ b/core/modules/media/src/Entity/Media.php @@ -38,6 +38,7 @@ * "add" = "Drupal\media\MediaForm", * "edit" = "Drupal\media\MediaForm", * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm", + * "editor_embed" = "Drupal\media\Form\MediaFormEmbed", * }, * "translation" = "Drupal\content_translation\ContentTranslationHandler", * "views_data" = "Drupal\media\MediaViewsData", diff --git a/core/modules/media/src/Exception/MediaNotFoundException.php b/core/modules/media/src/Exception/MediaNotFoundException.php new file mode 100644 index 000000000000..201397b57235 --- /dev/null +++ b/core/modules/media/src/Exception/MediaNotFoundException.php @@ -0,0 +1,8 @@ + '::ajaxFormRebuild', + ]; + $form['#prefix'] = '
'; + $form['#suffix'] = '
'; + $input = $form_state->getUserInput(); + $editor_dom_id = ''; + if (!empty($input['editor_object'])) { + $editor_dom_id = $input['editor_object']['editor-id']; + } + elseif (!empty($input['editor_dom_id'])) { + $editor_dom_id = $input['editor_id']; + } + $form['editor_dom_id'] = [ + '#type' => 'hidden', + '#value' => $editor_dom_id, + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function ajaxFormRebuild($form, FormStateInterface $form_state) { + + $response = new AjaxResponse(); + + // Display errors in form, if any. + if ($form_state->hasAnyErrors()) { + unset($form['#prefix'], $form['#suffix']); + $form['status_messages'] = array( + '#type' => 'status_messages', + '#weight' => -10, + ); + $response->addCommand(new HtmlCommand('#media-embed-dialog-form', $form)); + } + else { + // Embed the entity element in the editor and close the dialog. + $values = $form_state->getValues(); + $values['attributes'] = [ + 'data-embed-button' => 'media', + 'data-entity-embed-display' => 'view_mode:media.full', + 'data-entity-type' => 'media', + 'data-entity-uuid' => $this->getEntity()->uuid(), + ]; + + // Filter out empty attributes. + $values['attributes'] = array_filter($values['attributes'], function($value) { + return (bool) Unicode::strlen((string) $value); + }); + + $response->addCommand(new EditorDialogSave($values)); + $response->addCommand(new CloseModalDialogCommand()); + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public function actionsElement(array $form, FormStateInterface $form_state) { + $element = parent::actionsElement($form, $form_state); + $element['submit']['#ajax'] = [ + 'callback' => '::ajaxFormRebuild', + ]; + return $element; + } + +} diff --git a/core/modules/media/src/Plugin/CKEditorPlugin/MediaEmbed.php b/core/modules/media/src/Plugin/CKEditorPlugin/MediaEmbed.php new file mode 100644 index 000000000000..d78519b44046 --- /dev/null +++ b/core/modules/media/src/Plugin/CKEditorPlugin/MediaEmbed.php @@ -0,0 +1,100 @@ +entityTypeBundleInfo = $entity_type_bundle_info; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $container->get('entity_type.bundle.info'), + $configuration, + $plugin_id, + $plugin_definition + ); + } + + /** + * {@inheritdoc} + */ + public function getConfig(Editor $editor) { + return [ + 'MediaEmbed_dialogTitleAdd' => $this->t("Add Media"), + 'MediaEmbed_dialogTitleEdit' => $this->t("Edit Media"), + 'MediaEmbed_buttons' => $this->getButtons(), + ]; + } + + /** + * {@inheritdoc} + */ + public function getFile() { + return drupal_get_path('module', 'media') . '/js/plugins/mediaembed/plugin.js'; + } + + /** + * (@inheritdoc} + */ + public function getButtons() { + if ($this->buttons) { + return $this->buttons; + } + + $buttons = []; + foreach ($this->entityTypeBundleInfo->getBundleInfo('media') as $machine_name => $media_type) { + $buttons[$machine_name] = [ + 'label' => $media_type['label'], + 'id' => $machine_name, + 'image' => base_path() . drupal_get_path('module', 'media') . '/js/plugins/mediaembed/icons/mediaembed.png', + ]; + } + $this->buttons = $buttons; + return $buttons; + } + +} diff --git a/core/modules/media/src/Plugin/Filter/MediaEmbedFilter.php b/core/modules/media/src/Plugin/Filter/MediaEmbedFilter.php new file mode 100644 index 000000000000..41e3715c185a --- /dev/null +++ b/core/modules/media/src/Plugin/Filter/MediaEmbedFilter.php @@ -0,0 +1,291 @@ +entityTypeManager = $entity_type_manager; + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('renderer') + ); + } + + /** + * {@inheritdoc} + */ + public function process($text, $langcode) { + $result = new FilterProcessResult($text); + + if (strpos($text, 'data-entity-type') !== FALSE && (strpos($text, 'data-entity-embed-display') !== FALSE || strpos($text, 'data-view-mode') !== FALSE)) { + $dom = Html::load($text); + $xpath = new \DOMXPath($dom); + + foreach ($xpath->query('//drupal-entity[@data-entity-type and (@data-entity-uuid or @data-entity-id) and (@data-entity-embed-display or @data-view-mode)]') as $node) { + /** @var \DOMElement $node */ + $entity_type = $node->getAttribute('data-entity-type'); + $entity = NULL; + $entity_output = ''; + + // data-entity-embed-settings is deprecated, make sure we convert it to + // data-entity-embed-display-settings. + if (($settings = $node->getAttribute('data-entity-embed-settings')) && !$node->hasAttribute('data-entity-embed-display-settings')) { + $node->setAttribute('data-entity-embed-display-settings', $settings); + $node->removeAttribute('data-entity-embed-settings'); + } + + try { + // Load the entity either by UUID (preferred) or ID. + $id = NULL; + $entity = NULL; + if ($id = $node->getAttribute('data-entity-uuid')) { + $entity = $this->entityTypeManager->getStorage($entity_type) + ->loadByProperties(['uuid' => $id]); + $entity = current($entity); + } + else { + $id = $node->getAttribute('data-entity-id'); + $entity = $this->entityTypeManager->getStorage($entity_type)->load($id); + } + + if ($entity) { + // Protect ourselves from recursive rendering. + static $depth = 0; + $depth++; + if ($depth > 20) { + throw new RecursiveRenderingException(sprintf('Recursive rendering detected when rendering embedded %s entity %s.', $entity_type, $entity->id())); + } + + // If a UUID was not used, but is available, add it to the HTML. + if (!$node->getAttribute('data-entity-uuid') && $uuid = $entity->uuid()) { + $node->setAttribute('data-entity-uuid', $uuid); + } + + $context = $this->getNodeAttributesAsArray($node); + $context += array('data-langcode' => $langcode); + $build = $this->buildRenderArray($entity, $context); + // We need to render the embedded entity: + // - without replacing placeholders, so that the placeholders are + // only replaced at the last possible moment. Hence we cannot use + // either renderPlain() or renderRoot(), so we must use render(). + // - without bubbling beyond this filter, because filters must + // ensure that the bubbleable metadata for the changes they make + // when filtering text makes it onto the FilterProcessResult + // object that they return ($result). To prevent that bubbling, we + // must wrap the call to render() in a render context. + $entity_output = $this->renderer->executeInRenderContext(new RenderContext(), function () use (&$build) { + return $this->renderer->render($build); + }); + $result = $result->merge(BubbleableMetadata::createFromRenderArray($build)); + + $depth--; + } + else { + throw new MediaNotFoundException(sprintf('Unable to load embedded %s media entity %s.', $entity_type, $id)); + } + } + catch (\Exception $e) { + watchdog_exception('media_embed', $e); + } + + $this->replaceNodeContent($node, $entity_output); + } + + $result->setProcessedText(Html::serialize($dom)); + } + + return $result; + } + + /** + * Build a render array for the media entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to be rendered. + * @param array $context + * (optional) Array of context values, corresponding to the attributes on + * the embed HTML tag. + * + * @return array + * A render array. + */ + public function buildRenderArray(EntityInterface $entity, array $context = []) { + // Merge in default attributes. + $context += [ + 'data-entity-type' => $entity->getEntityTypeId(), + 'data-entity-uuid' => $entity->uuid(), + 'data-entity-embed-display' => 'entity_reference:entity_reference_entity_view', + 'data-entity-embed-display-settings' => [], + ]; + + // The caption text is double-encoded, so decode it here. + if (isset($context['data-caption'])) { + $context['data-caption'] = Html::decodeEntities($context['data-caption']); + } + + // Build and render the Entity Embed Display plugin, allowing modules to + // alter the result before rendering. + $build = [ + '#theme_wrappers' => ['media_embed'], + '#attributes' => ['class' => ['embedded-media']], + '#entity' => $entity, + '#context' => $context, + ]; + $build += $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId())->view($entity, 'default'); + // We're providing embed-specific theming, so don't use media's standard. + unset($build['#theme']); + + // Maintain data-align if it is there. + if (isset($context['data-align'])) { + $build['#attributes']['data-align'] = $context['data-align']; + } + elseif ((isset($context['class']))) { + $build['#attributes']['class'][] = $context['class']; + } + + // Maintain data-caption if it is there. + if (isset($context['data-caption'])) { + $build['#attributes']['data-caption'] = $context['data-caption']; + } + + // Make sure that access to the entity is respected. + $build['#access'] = $entity->access('view', NULL, TRUE); + + return $build; + } + + /** + * Replace the contents of a DOMNode. + * + * @param \DOMNode $node + * A DOMNode object. + * @param string $content + * The text or HTML that will replace the contents of $node. + */ + protected function replaceNodeContent(\DOMNode &$node, $content) { + if (strlen($content)) { + // Load the content into a new DOMDocument and retrieve the DOM nodes. + $replacement_nodes = Html::load($content)->getElementsByTagName('body') + ->item(0) + ->childNodes; + } + else { + $replacement_nodes = [$node->ownerDocument->createTextNode('')]; + } + + foreach ($replacement_nodes as $replacement_node) { + // Import the replacement node from the new DOMDocument into the original + // one, importing also the child nodes of the replacement node. + $replacement_node = $node->ownerDocument->importNode($replacement_node, TRUE); + $node->parentNode->insertBefore($replacement_node, $node); + } + $node->parentNode->removeChild($node); + } + + /** + * Convert the attributes on a DOMNode object to an array. + * + * This will also un-serialize any attribute values stored as JSON. + * + * @param \DOMNode $node + * A DOMNode object. + * + * @return array + * The attributes as an associative array, keyed by the attribute names. + */ + public function getNodeAttributesAsArray(\DOMNode $node) { + $return = []; + + // Convert the data attributes to the context array. + foreach ($node->attributes as $attribute) { + $key = $attribute->nodeName; + $return[$key] = $attribute->nodeValue; + + // Check for JSON-encoded attributes. + $data = json_decode($return[$key], TRUE, 10); + if ($data !== NULL && json_last_error() === JSON_ERROR_NONE) { + $return[$key] = $data; + } + } + + return $return; + } + + /** + * {@inheritdoc} + */ + public function tips($long = FALSE) { + if ($long) { + return $this->t(' +

You can embed entities. Additional properties can be added to the embed tag like data-caption and data-align if supported. Example:

+ <drupal-entity data-entity-type="node" data-entity-uuid="07bf3a2e-1941-4a44-9b02-2d1d7a41ec0e" data-view-mode="teaser" />'); + } + else { + return $this->t('You can embed entities.'); + } + } + +} diff --git a/core/modules/media/templates/media-embed.html.twig b/core/modules/media/templates/media-embed.html.twig new file mode 100644 index 000000000000..284d2d116c34 --- /dev/null +++ b/core/modules/media/templates/media-embed.html.twig @@ -0,0 +1,15 @@ +{# +/** + * @file + * Default theme implementation of a container used to wrap embedded entities. + * + * Available variables: + * - attributes: HTML attributes for the containing element. + * - children: The rendered child elements of the container. + * + * @see template_preprocess_media_embed() + * + * @ingroup themeable + */ +#} +{{ children }} diff --git a/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php b/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php new file mode 100644 index 000000000000..a3c5d2779cb9 --- /dev/null +++ b/core/modules/media/tests/src/Kernel/MediaEmbedFilterTest.php @@ -0,0 +1,85 @@ +container->get('plugin.manager.filter'); + $bag = new FilterPluginCollection($manager, []); + $this->filters = $bag->getAll(); + } + + /** + * Tests the media embed filter. + */ + public function testMediaEmbedFilter() { + // Get media embed filter. + $filter = $this->filters['media_embed']; + + // Create test function. + $test = function ($input) use ($filter) { + // Run $input through $filter. + $filtered_media = $filter->process($input, 'und')->getProcessedText(); + // Extract tag. + $dom = Html::load($filtered_media); + $img = $dom->getElementsByTagName('img')[0]; + $filtered_media_img = $dom->saveHTML($img); + return $filtered_media_img; + }; + + // Create media entity. + $media = Media::create(['bundle' => $this->testMediaType->id()]); + $media->save(); + $media_uuid = $media->uuid(); + + // Render entity. + /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $view_builder */ + $view_builder = \Drupal::entityTypeManager(); + $build = $view_builder->getViewBuilder($media->getEntityTypeId())->view($media); + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $rendered_media = $renderer->renderPlain($build)->__toString(); + + // Extract tag. + $dom = Html::load($rendered_media); + $img = $dom->getElementsByTagName('img')[0]; + $rendered_media_img = $dom->saveHTML($img); + + // Test filter using data-entity-embed-display attribute. + $input = ''; + $expected = $rendered_media_img; + $this->assertSame($expected, $test($input)); + + // Test filter using data-view-mode attribute. + $input = ''; + $expected = $rendered_media_img; + $this->assertSame($expected, $test($input)); + } + +}