Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions core/modules/editor/editor.module
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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().
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
langcode: en
status: true
dependencies:
module:
- editor
- media
id: media.editor_embed
label: Editor embed
targetEntityType: media
cache: true
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
227 changes: 227 additions & 0 deletions core/modules/media/js/plugins/mediaembed/plugin.js
Original file line number Diff line number Diff line change
@@ -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);
19 changes: 19 additions & 0 deletions core/modules/media/media.module
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ function media_theme() {
'media' => [
'render element' => 'elements',
],
'media_embed' => [
'render element' => 'element',
],
];
}

Expand Down Expand Up @@ -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'];
}
10 changes: 10 additions & 0 deletions core/modules/media/media.routing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
19 changes: 19 additions & 0 deletions core/modules/media/src/Controller/MediaEmbedDialog.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Drupal\media\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Routing\RequestContext;
use Drupal\media\Entity\Media;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;

class MediaEmbedDialog extends ControllerBase {

public function form($editor, $media_type) {
$entity = Media::create(['bundle' => $media_type, 'uid' => $this->currentUser()->id()]);
$form = $this->entityFormBuilder()->getForm($entity, 'editor_embed');
return $form;
}

}
1 change: 1 addition & 0 deletions core/modules/media/src/Entity/Media.php
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions core/modules/media/src/Exception/MediaNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Drupal\media\Exception;

/**
* Exception thrown when the embedded entity cannot be loaded.
*/
class MediaNotFoundException extends \Exception {}
Loading