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'] = '
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
+ */
+#}
+