Issue #2994696 by oknate, Wim Leers, phenaproxima, starshaped, lauriii, andrewmacpherson, ckrina, a.milkovsky, John Pitcairn, artis, cs_shadow, duncan.moo, Berdir, JamesK, Ambidex, BartK, idflood, Dave Reid, CTaPByK, Reinmar, kmoll, webflo, thenchev, wrd, yched, kkrzton, slashrsm, root_brute, Ruuds, MobliMic, ls206, marcoscano, pixelmord, narnua: Render embedded media items in CKEditor
parent
2f154624fd
commit
f5b124ab5e
|
@ -440,6 +440,7 @@
|
|||
},
|
||||
"ignoreFiles": [
|
||||
"assets/vendor/**/*.css",
|
||||
"tests/Drupal/Tests/Core/Asset/css_test_files/**/*.css"
|
||||
"tests/Drupal/Tests/Core/Asset/css_test_files/**/*.css",
|
||||
"modules/media/css/plugins/drupalmedia/ckeditor.drupalmedia.css"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -47,6 +47,8 @@
|
|||
return;
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.drupallink.registerLinkableWidget('image');
|
||||
|
||||
// Override default behaviour of 'drupalunlink' command.
|
||||
editor.getCommand('drupalunlink').on('exec', function(evt) {
|
||||
const widget = getFocusedWidget(editor);
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
return;
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.drupallink.registerLinkableWidget('image');
|
||||
|
||||
editor.getCommand('drupalunlink').on('exec', function (evt) {
|
||||
var widget = getFocusedWidget(editor);
|
||||
|
||||
|
|
|
@ -61,6 +61,35 @@
|
|||
};
|
||||
}
|
||||
|
||||
const registeredLinkableWidgets = [];
|
||||
|
||||
/**
|
||||
* Registers a widget name as linkable.
|
||||
*
|
||||
* @param {string} widgetName
|
||||
* The name of the widget to register as linkable.
|
||||
*/
|
||||
function registerLinkableWidget(widgetName) {
|
||||
registeredLinkableWidgets.push(widgetName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the focused widget, if one of the registered linkable widget names.
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* A CKEditor instance.
|
||||
*
|
||||
* @return {?CKEDITOR.plugins.widget}
|
||||
* The focused linkable widget instance, or null.
|
||||
*/
|
||||
function getFocusedLinkableWidget(editor) {
|
||||
const widget = editor.widgets.focused;
|
||||
if (widget && registeredLinkableWidgets.indexOf(widget.name) !== -1) {
|
||||
return widget;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the surrounding link element of current selection.
|
||||
*
|
||||
|
@ -121,9 +150,7 @@
|
|||
modes: { wysiwyg: 1 },
|
||||
canUndo: true,
|
||||
exec(editor) {
|
||||
const drupalImageUtils = CKEDITOR.plugins.drupalimage;
|
||||
const focusedImageWidget =
|
||||
drupalImageUtils && drupalImageUtils.getFocusedWidget(editor);
|
||||
const focusedLinkableWidget = getFocusedLinkableWidget(editor);
|
||||
let linkElement = getSelectedLink(editor);
|
||||
|
||||
// Set existing values based on selected element.
|
||||
|
@ -133,20 +160,22 @@
|
|||
}
|
||||
// Or, if an image widget is focused, we're editing a link wrapping
|
||||
// an image widget.
|
||||
else if (focusedImageWidget && focusedImageWidget.data.link) {
|
||||
existingValues = CKEDITOR.tools.clone(focusedImageWidget.data.link);
|
||||
else if (focusedLinkableWidget && focusedLinkableWidget.data.link) {
|
||||
existingValues = CKEDITOR.tools.clone(
|
||||
focusedLinkableWidget.data.link,
|
||||
);
|
||||
}
|
||||
|
||||
// Prepare a save callback to be used upon saving the dialog.
|
||||
const saveCallback = function(returnValues) {
|
||||
// If an image widget is focused, we're not editing an independent
|
||||
// link, but we're wrapping an image widget in a link.
|
||||
if (focusedImageWidget) {
|
||||
focusedImageWidget.setData(
|
||||
if (focusedLinkableWidget) {
|
||||
focusedLinkableWidget.setData(
|
||||
'link',
|
||||
CKEDITOR.tools.extend(
|
||||
returnValues.attributes,
|
||||
focusedImageWidget.data.link,
|
||||
focusedLinkableWidget.data.link,
|
||||
),
|
||||
);
|
||||
editor.fire('saveSnapshot');
|
||||
|
@ -330,5 +359,6 @@
|
|||
CKEDITOR.plugins.drupallink = {
|
||||
parseLinkAttributes: parseAttributes,
|
||||
getLinkAttributes: getAttributes,
|
||||
registerLinkableWidget,
|
||||
};
|
||||
})(jQuery, Drupal, drupalSettings, CKEDITOR);
|
||||
|
|
|
@ -49,6 +49,20 @@
|
|||
};
|
||||
}
|
||||
|
||||
var registeredLinkableWidgets = [];
|
||||
|
||||
function registerLinkableWidget(widgetName) {
|
||||
registeredLinkableWidgets.push(widgetName);
|
||||
}
|
||||
|
||||
function getFocusedLinkableWidget(editor) {
|
||||
var widget = editor.widgets.focused;
|
||||
if (widget && registeredLinkableWidgets.indexOf(widget.name) !== -1) {
|
||||
return widget;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getSelectedLink(editor) {
|
||||
var selection = editor.getSelection();
|
||||
var selectedElement = selection.getSelectedElement();
|
||||
|
@ -88,20 +102,19 @@
|
|||
modes: { wysiwyg: 1 },
|
||||
canUndo: true,
|
||||
exec: function exec(editor) {
|
||||
var drupalImageUtils = CKEDITOR.plugins.drupalimage;
|
||||
var focusedImageWidget = drupalImageUtils && drupalImageUtils.getFocusedWidget(editor);
|
||||
var focusedLinkableWidget = getFocusedLinkableWidget(editor);
|
||||
var linkElement = getSelectedLink(editor);
|
||||
|
||||
var existingValues = {};
|
||||
if (linkElement && linkElement.$) {
|
||||
existingValues = parseAttributes(editor, linkElement);
|
||||
} else if (focusedImageWidget && focusedImageWidget.data.link) {
|
||||
existingValues = CKEDITOR.tools.clone(focusedImageWidget.data.link);
|
||||
} else if (focusedLinkableWidget && focusedLinkableWidget.data.link) {
|
||||
existingValues = CKEDITOR.tools.clone(focusedLinkableWidget.data.link);
|
||||
}
|
||||
|
||||
var saveCallback = function saveCallback(returnValues) {
|
||||
if (focusedImageWidget) {
|
||||
focusedImageWidget.setData('link', CKEDITOR.tools.extend(returnValues.attributes, focusedImageWidget.data.link));
|
||||
if (focusedLinkableWidget) {
|
||||
focusedLinkableWidget.setData('link', CKEDITOR.tools.extend(returnValues.attributes, focusedLinkableWidget.data.link));
|
||||
editor.fire('saveSnapshot');
|
||||
return;
|
||||
}
|
||||
|
@ -244,6 +257,7 @@
|
|||
|
||||
CKEDITOR.plugins.drupallink = {
|
||||
parseLinkAttributes: parseAttributes,
|
||||
getLinkAttributes: getAttributes
|
||||
getLinkAttributes: getAttributes,
|
||||
registerLinkableWidget: registerLinkableWidget
|
||||
};
|
||||
})(jQuery, Drupal, drupalSettings, CKEDITOR);
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* @file
|
||||
* Media Embed filter: default styling for media embed errors.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The caption filter's styling overrides ours, so add a more specific selector
|
||||
* to account for that.
|
||||
*/
|
||||
.media-embed-error,
|
||||
.caption > .media-embed-error {
|
||||
max-width: 200px;
|
||||
padding: 100px 20px 20px;
|
||||
text-align: center;
|
||||
background-color: #ebebeb;
|
||||
background-image: url(../images/icons/no-thumbnail.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center top;
|
||||
background-size: 100px 100px;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* @file
|
||||
* Media embed: overrides to make focus styles and alignment work in CKEditor.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Allow the drupal-media element's width to collapse to the size of its
|
||||
* contents so that the outline has no extra white space (margin). This
|
||||
* emulates the image2 plugin's styles inherited by the drupallink CKEditor
|
||||
* plugin.
|
||||
*/
|
||||
drupal-media {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/**
|
||||
* For center alignment, take advantage of drupal-media's inline-block
|
||||
* display and center it as if it were text.
|
||||
*/
|
||||
.cke_widget_drupalmedia.align-center {
|
||||
text-align: center;
|
||||
}
|
|
@ -0,0 +1,302 @@
|
|||
/**
|
||||
* @file
|
||||
* Drupal Media embed plugin.
|
||||
*/
|
||||
|
||||
(function(jQuery, Drupal, CKEDITOR) {
|
||||
/**
|
||||
* Gets the focused widget, if of the type specific for this plugin.
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* A CKEditor instance.
|
||||
*
|
||||
* @return {?CKEDITOR.plugins.widget}
|
||||
* The focused drupalmedia widget instance, or null.
|
||||
*/
|
||||
function getFocusedWidget(editor) {
|
||||
const widget = editor.widgets.focused;
|
||||
|
||||
if (widget && widget.name === 'drupalmedia') {
|
||||
return widget;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes embedded items linkable by integrating with the drupallink plugin.
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* A CKEditor instance.
|
||||
*/
|
||||
function linkCommandIntegrator(editor) {
|
||||
if (!editor.plugins.drupallink) {
|
||||
return;
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.drupallink.registerLinkableWidget('drupalmedia');
|
||||
|
||||
editor.getCommand('drupalunlink').on('exec', function(evt) {
|
||||
const widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.setData('link', null);
|
||||
|
||||
this.refresh(editor, editor.elementPath());
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
|
||||
editor.getCommand('drupalunlink').on('refresh', function(evt) {
|
||||
const widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(
|
||||
widget.data.link ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,
|
||||
);
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
|
||||
// Register context menu items for editing link.
|
||||
if (editor.contextMenu) {
|
||||
editor.contextMenu.addListener(() => {
|
||||
const widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.data.link) {
|
||||
return {
|
||||
link: CKEDITOR.TRISTATE_OFF,
|
||||
unlink: CKEDITOR.TRISTATE_OFF,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Themes the error displayed when the media embed preview fails.
|
||||
*
|
||||
* @return {string}
|
||||
* An HTML string to insert in the CKEditor.
|
||||
*/
|
||||
Drupal.theme.mediaEmbedError = function() {
|
||||
const error = Drupal.t(
|
||||
'An error occurred while trying to preview the media. Please save your work and reload this page.',
|
||||
);
|
||||
return `<div class="media-embed-error media-embed-error--preview-error">${error}</div>`;
|
||||
};
|
||||
|
||||
CKEDITOR.plugins.add('drupalmedia', {
|
||||
requires: 'widget',
|
||||
|
||||
beforeInit(editor) {
|
||||
// Configure CKEditor DTD for custom drupal-media element.
|
||||
// @see https://www.drupal.org/node/2448449#comment-9717735
|
||||
const dtd = CKEDITOR.dtd;
|
||||
// Allow text within the drupal-media tag.
|
||||
dtd['drupal-media'] = { '#': 1 };
|
||||
// Register drupal-media element as an allowed child in each tag that can
|
||||
// contain a div element and as an allowed child of the a tag.
|
||||
Object.keys(dtd).forEach(tagName => {
|
||||
if (dtd[tagName].div) {
|
||||
dtd[tagName]['drupal-media'] = 1;
|
||||
}
|
||||
});
|
||||
dtd.a['drupal-media'] = 1;
|
||||
|
||||
editor.widgets.add('drupalmedia', {
|
||||
allowedContent:
|
||||
'drupal-media[data-entity-type,data-entity-uuid,data-view-mode,data-align,data-caption,alt,title]',
|
||||
// Minimum HTML which is required by this widget to work.
|
||||
requiredContent: 'drupal-media[data-entity-type,data-entity-uuid]',
|
||||
|
||||
pathName: Drupal.t('Embedded media'),
|
||||
|
||||
editables: {
|
||||
caption: {
|
||||
selector: 'figcaption',
|
||||
allowedContent: 'a[!href]; em strong cite code br',
|
||||
pathName: Drupal.t('Caption'),
|
||||
},
|
||||
},
|
||||
|
||||
upcast(element, data) {
|
||||
const attributes = element.attributes;
|
||||
// This matches the behavior of the corresponding server-side text filter plugin.
|
||||
if (
|
||||
element.name !== 'drupal-media' ||
|
||||
attributes['data-entity-type'] !== 'media' ||
|
||||
attributes['data-entity-uuid'] === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
data.attributes = CKEDITOR.tools.copy(attributes);
|
||||
data.hasCaption = data.attributes.hasOwnProperty('data-caption');
|
||||
data.link = null;
|
||||
if (element.parent.name === 'a') {
|
||||
data.link = CKEDITOR.tools.copy(element.parent.attributes);
|
||||
// Omit CKEditor-internal attributes.
|
||||
Object.keys(element.parent.attributes).forEach(attrName => {
|
||||
if (attrName.indexOf('data-cke-') !== -1) {
|
||||
delete data.link[attrName];
|
||||
}
|
||||
});
|
||||
}
|
||||
return element;
|
||||
},
|
||||
|
||||
destroy() {
|
||||
this._tearDownDynamicEditables();
|
||||
},
|
||||
|
||||
data(event) {
|
||||
if (this._previewNeedsServerSideUpdate()) {
|
||||
editor.fire('lockSnapshot');
|
||||
this._tearDownDynamicEditables();
|
||||
|
||||
this._loadPreview(widget => {
|
||||
widget._setUpDynamicEditables();
|
||||
editor.fire('unlockSnapshot');
|
||||
});
|
||||
}
|
||||
|
||||
// Allow entity_embed.editor.css to respond to changes (for example in alignment).
|
||||
this.element.setAttributes(this.data.attributes);
|
||||
// Convert data-align attribute to class so we're not applying styles
|
||||
// to data attributes.
|
||||
// @todo Consider removing this in https://www.drupal.org/project/drupal/issues/3072279
|
||||
if (this.data.attributes.hasOwnProperty('data-align')) {
|
||||
this.element
|
||||
.getParent()
|
||||
.addClass(`align-${this.data.attributes['data-align']}`);
|
||||
}
|
||||
|
||||
// Track the previous state to allow checking if preview needs
|
||||
// server side update.
|
||||
this.oldData = CKEDITOR.tools.clone(this.data);
|
||||
},
|
||||
|
||||
downcast() {
|
||||
const downcastElement = new CKEDITOR.htmlParser.element(
|
||||
'drupal-media',
|
||||
this.data.attributes,
|
||||
);
|
||||
if (this.data.link) {
|
||||
const link = new CKEDITOR.htmlParser.element('a', this.data.link);
|
||||
link.add(downcastElement);
|
||||
return link;
|
||||
}
|
||||
return downcastElement;
|
||||
},
|
||||
|
||||
_setUpDynamicEditables() {
|
||||
// Now that the caption is available in the DOM, make it editable.
|
||||
if (this.initEditable('caption', this.definition.editables.caption)) {
|
||||
const captionEditable = this.editables.caption;
|
||||
// @see core/modules/filter/css/filter.caption.css
|
||||
// @see ckeditor_ckeditor_css_alter()
|
||||
captionEditable.setAttribute(
|
||||
'data-placeholder',
|
||||
Drupal.t('Enter caption here'),
|
||||
);
|
||||
// Ensure that any changes made to the caption are persisted in the
|
||||
// widget's data-caption attribute.
|
||||
this.captionObserver = new MutationObserver(() => {
|
||||
const mediaAttributes = CKEDITOR.tools.clone(
|
||||
this.data.attributes,
|
||||
);
|
||||
mediaAttributes['data-caption'] = captionEditable.getData();
|
||||
this.setData('attributes', mediaAttributes);
|
||||
});
|
||||
this.captionObserver.observe(captionEditable.$, {
|
||||
characterData: true,
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_tearDownDynamicEditables() {
|
||||
// If we are watching for changes to the caption, stop doing that.
|
||||
if (this.captionObserver) {
|
||||
this.captionObserver.disconnect();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines if the preview needs to be re-rendered by the server.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_previewNeedsServerSideUpdate() {
|
||||
// When the widget is first loading, it of course needs to still get a preview!
|
||||
if (!this.ready) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this._hashData(this.oldData) !== this._hashData(this.data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Computes a hash of the data that can only be previewed by the server.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
_hashData(data) {
|
||||
const dataToHash = CKEDITOR.tools.clone(data);
|
||||
// The caption does not need rendering.
|
||||
delete dataToHash.attributes['data-caption'];
|
||||
// Changed link destinations do not affect the visual preview.
|
||||
if (dataToHash.link) {
|
||||
delete dataToHash.link.href;
|
||||
}
|
||||
return JSON.stringify(dataToHash);
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads an media embed preview and runs a callback after insertion.
|
||||
*
|
||||
* Note the absence of caching, that's because this uses a GET request (which is cacheable) and the server takes
|
||||
* special care to make the responses privately cacheable (i.e. per session) in the browser.
|
||||
*
|
||||
* @see \Drupal\media\Controller\MediaFilterController::preview()
|
||||
*
|
||||
* @param {function} callback
|
||||
* A callback function that will be called after the preview has
|
||||
* loaded. Receives the widget instance.
|
||||
*/
|
||||
_loadPreview(callback) {
|
||||
jQuery.get({
|
||||
url: Drupal.url(`media/${editor.config.drupal.format}/preview`),
|
||||
data: {
|
||||
text: this.downcast().getOuterHtml(),
|
||||
},
|
||||
dataType: 'html',
|
||||
success: previewHtml => {
|
||||
this.element.setHtml(previewHtml);
|
||||
callback(this);
|
||||
},
|
||||
error: () => {
|
||||
this.element.setHtml(Drupal.theme('mediaEmbedError'));
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
afterInit(editor) {
|
||||
linkCommandIntegrator(editor);
|
||||
},
|
||||
});
|
||||
})(jQuery, Drupal, CKEDITOR);
|
|
@ -0,0 +1,223 @@
|
|||
/**
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function (jQuery, Drupal, CKEDITOR) {
|
||||
function getFocusedWidget(editor) {
|
||||
var widget = editor.widgets.focused;
|
||||
|
||||
if (widget && widget.name === 'drupalmedia') {
|
||||
return widget;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function linkCommandIntegrator(editor) {
|
||||
if (!editor.plugins.drupallink) {
|
||||
return;
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.drupallink.registerLinkableWidget('drupalmedia');
|
||||
|
||||
editor.getCommand('drupalunlink').on('exec', function (evt) {
|
||||
var widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.setData('link', null);
|
||||
|
||||
this.refresh(editor, editor.elementPath());
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
|
||||
editor.getCommand('drupalunlink').on('refresh', function (evt) {
|
||||
var widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(widget.data.link ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED);
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
|
||||
if (editor.contextMenu) {
|
||||
editor.contextMenu.addListener(function () {
|
||||
var widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.data.link) {
|
||||
return {
|
||||
link: CKEDITOR.TRISTATE_OFF,
|
||||
unlink: CKEDITOR.TRISTATE_OFF
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Drupal.theme.mediaEmbedError = function () {
|
||||
var error = Drupal.t('An error occurred while trying to preview the media. Please save your work and reload this page.');
|
||||
return '<div class="media-embed-error media-embed-error--preview-error">' + error + '</div>';
|
||||
};
|
||||
|
||||
CKEDITOR.plugins.add('drupalmedia', {
|
||||
requires: 'widget',
|
||||
|
||||
beforeInit: function beforeInit(editor) {
|
||||
var dtd = CKEDITOR.dtd;
|
||||
|
||||
dtd['drupal-media'] = { '#': 1 };
|
||||
|
||||
Object.keys(dtd).forEach(function (tagName) {
|
||||
if (dtd[tagName].div) {
|
||||
dtd[tagName]['drupal-media'] = 1;
|
||||
}
|
||||
});
|
||||
dtd.a['drupal-media'] = 1;
|
||||
|
||||
editor.widgets.add('drupalmedia', {
|
||||
allowedContent: 'drupal-media[data-entity-type,data-entity-uuid,data-view-mode,data-align,data-caption,alt,title]',
|
||||
|
||||
requiredContent: 'drupal-media[data-entity-type,data-entity-uuid]',
|
||||
|
||||
pathName: Drupal.t('Embedded media'),
|
||||
|
||||
editables: {
|
||||
caption: {
|
||||
selector: 'figcaption',
|
||||
allowedContent: 'a[!href]; em strong cite code br',
|
||||
pathName: Drupal.t('Caption')
|
||||
}
|
||||
},
|
||||
|
||||
upcast: function upcast(element, data) {
|
||||
var attributes = element.attributes;
|
||||
|
||||
if (element.name !== 'drupal-media' || attributes['data-entity-type'] !== 'media' || attributes['data-entity-uuid'] === undefined) {
|
||||
return;
|
||||
}
|
||||
data.attributes = CKEDITOR.tools.copy(attributes);
|
||||
data.hasCaption = data.attributes.hasOwnProperty('data-caption');
|
||||
data.link = null;
|
||||
if (element.parent.name === 'a') {
|
||||
data.link = CKEDITOR.tools.copy(element.parent.attributes);
|
||||
|
||||
Object.keys(element.parent.attributes).forEach(function (attrName) {
|
||||
if (attrName.indexOf('data-cke-') !== -1) {
|
||||
delete data.link[attrName];
|
||||
}
|
||||
});
|
||||
}
|
||||
return element;
|
||||
},
|
||||
destroy: function destroy() {
|
||||
this._tearDownDynamicEditables();
|
||||
},
|
||||
data: function data(event) {
|
||||
if (this._previewNeedsServerSideUpdate()) {
|
||||
editor.fire('lockSnapshot');
|
||||
this._tearDownDynamicEditables();
|
||||
|
||||
this._loadPreview(function (widget) {
|
||||
widget._setUpDynamicEditables();
|
||||
editor.fire('unlockSnapshot');
|
||||
});
|
||||
}
|
||||
|
||||
this.element.setAttributes(this.data.attributes);
|
||||
|
||||
if (this.data.attributes.hasOwnProperty('data-align')) {
|
||||
this.element.getParent().addClass('align-' + this.data.attributes['data-align']);
|
||||
}
|
||||
|
||||
this.oldData = CKEDITOR.tools.clone(this.data);
|
||||
},
|
||||
downcast: function downcast() {
|
||||
var downcastElement = new CKEDITOR.htmlParser.element('drupal-media', this.data.attributes);
|
||||
if (this.data.link) {
|
||||
var link = new CKEDITOR.htmlParser.element('a', this.data.link);
|
||||
link.add(downcastElement);
|
||||
return link;
|
||||
}
|
||||
return downcastElement;
|
||||
},
|
||||
_setUpDynamicEditables: function _setUpDynamicEditables() {
|
||||
var _this = this;
|
||||
|
||||
if (this.initEditable('caption', this.definition.editables.caption)) {
|
||||
var captionEditable = this.editables.caption;
|
||||
|
||||
captionEditable.setAttribute('data-placeholder', Drupal.t('Enter caption here'));
|
||||
|
||||
this.captionObserver = new MutationObserver(function () {
|
||||
var mediaAttributes = CKEDITOR.tools.clone(_this.data.attributes);
|
||||
mediaAttributes['data-caption'] = captionEditable.getData();
|
||||
_this.setData('attributes', mediaAttributes);
|
||||
});
|
||||
this.captionObserver.observe(captionEditable.$, {
|
||||
characterData: true,
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
},
|
||||
_tearDownDynamicEditables: function _tearDownDynamicEditables() {
|
||||
if (this.captionObserver) {
|
||||
this.captionObserver.disconnect();
|
||||
}
|
||||
},
|
||||
_previewNeedsServerSideUpdate: function _previewNeedsServerSideUpdate() {
|
||||
if (!this.ready) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this._hashData(this.oldData) !== this._hashData(this.data);
|
||||
},
|
||||
_hashData: function _hashData(data) {
|
||||
var dataToHash = CKEDITOR.tools.clone(data);
|
||||
|
||||
delete dataToHash.attributes['data-caption'];
|
||||
|
||||
if (dataToHash.link) {
|
||||
delete dataToHash.link.href;
|
||||
}
|
||||
return JSON.stringify(dataToHash);
|
||||
},
|
||||
_loadPreview: function _loadPreview(callback) {
|
||||
var _this2 = this;
|
||||
|
||||
jQuery.get({
|
||||
url: Drupal.url('media/' + editor.config.drupal.format + '/preview'),
|
||||
data: {
|
||||
text: this.downcast().getOuterHtml()
|
||||
},
|
||||
dataType: 'html',
|
||||
success: function success(previewHtml) {
|
||||
_this2.element.setHtml(previewHtml);
|
||||
callback(_this2);
|
||||
},
|
||||
error: function error() {
|
||||
_this2.element.setHtml(Drupal.theme('mediaEmbedError'));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
afterInit: function afterInit(editor) {
|
||||
linkCommandIntegrator(editor);
|
||||
}
|
||||
});
|
||||
})(jQuery, Drupal, CKEDITOR);
|
|
@ -31,3 +31,9 @@ filter.caption:
|
|||
css/filter.caption.css: {}
|
||||
dependencies:
|
||||
- filter/caption
|
||||
|
||||
media_embed:
|
||||
version: VERSION
|
||||
css:
|
||||
component:
|
||||
css/filter.media_embed.css: {}
|
||||
|
|
|
@ -39,3 +39,12 @@ media.settings:
|
|||
_title: 'Media settings'
|
||||
requirements:
|
||||
_permission: 'administer media'
|
||||
|
||||
media.filter.preview:
|
||||
path: '/media/{filter_format}/preview'
|
||||
defaults:
|
||||
_controller: '\Drupal\media\Controller\MediaFilterController::preview'
|
||||
methods: [GET]
|
||||
requirements:
|
||||
_entity_access: 'filter_format.use'
|
||||
_custom_access: '\Drupal\media\Controller\MediaFilterController::formatUsesMediaEmbedFilter'
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\media\Controller;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Render\RendererInterface;
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Controller which renders a preview of the provided text.
|
||||
*
|
||||
* @internal
|
||||
* This is an internal part of the media system in Drupal core and may be
|
||||
* subject to change in minor releases. This class should not be
|
||||
* instantiated or extended by external code.
|
||||
*/
|
||||
class MediaFilterController implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The renderer service.
|
||||
*
|
||||
* @var \Drupal\Core\Render\RendererInterface
|
||||
*/
|
||||
protected $renderer;
|
||||
|
||||
/**
|
||||
* Constructs an MediaFilterController instance.
|
||||
*
|
||||
* @param \Drupal\Core\Render\RendererInterface $renderer
|
||||
* The renderer service.
|
||||
*/
|
||||
public function __construct(RendererInterface $renderer) {
|
||||
$this->renderer = $renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('renderer')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a HTML response containing a preview of the text after filtering.
|
||||
*
|
||||
* Applies all of the given text format's filters, not just the `media_embed`
|
||||
* filter, because for example `filter_align` and `filter_caption` may apply
|
||||
* to it as well.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||
* The request object.
|
||||
* @param \Drupal\filter\FilterFormatInterface $filter_format
|
||||
* The text format.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* The filtered text.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
* Throws an exception if 'text' parameter is not found in the query
|
||||
* string.
|
||||
*
|
||||
* @see \Drupal\editor\EditorController::getUntransformedText
|
||||
*/
|
||||
public function preview(Request $request, FilterFormatInterface $filter_format) {
|
||||
$text = $request->query->get('text');
|
||||
if ($text == '') {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
$build = [
|
||||
'#type' => 'processed_text',
|
||||
'#text' => $text,
|
||||
'#format' => $filter_format->id(),
|
||||
];
|
||||
$html = $this->renderer->renderPlain($build);
|
||||
|
||||
// Note that we intentionally do not use:
|
||||
// - \Drupal\Core\Cache\CacheableResponse because caching it on the server
|
||||
// side is wasteful, hence there is no need for cacheability metadata.
|
||||
// - \Drupal\Core\Render\HtmlResponse because there is no need for
|
||||
// attachments nor cacheability metadata.
|
||||
return (new Response($html))
|
||||
// Do not allow any intermediary to cache the response, only the end user.
|
||||
->setPrivate()
|
||||
// Allow the end user to cache it for up to 5 minutes.
|
||||
->setMaxAge(300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks access based on media_embed filter status on the text format.
|
||||
*
|
||||
* @param \Drupal\filter\FilterFormatInterface $filter_format
|
||||
* The text format for which to check access.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResultInterface
|
||||
* The access result.
|
||||
*/
|
||||
public static function formatUsesMediaEmbedFilter(FilterFormatInterface $filter_format) {
|
||||
$filters = $filter_format->filters();
|
||||
return AccessResult::allowedIf($filters->has('media_embed') && $filters->get('media_embed')->status)
|
||||
->addCacheableDependency($filter_format);
|
||||
}
|
||||
|
||||
}
|
|
@ -29,8 +29,9 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|||
* of an iframe.
|
||||
*
|
||||
* @internal
|
||||
* This is an internal part of the oEmbed system and should only be used by
|
||||
* oEmbed-related code in Drupal core.
|
||||
* This is an internal part of the media system in Drupal core and may be
|
||||
* subject to change in minor releases. This class should not be
|
||||
* instantiated or extended by external code.
|
||||
*/
|
||||
class OEmbedIframeController implements ContainerInjectionInterface {
|
||||
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\ckeditor\CKEditorPluginContextualInterface;
|
||||
use Drupal\ckeditor\CKEditorPluginCssInterface;
|
||||
use Drupal\Core\Extension\ModuleExtensionList;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Drupal\Core\Plugin\PluginBase;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines the "drupalmedia" plugin.
|
||||
*
|
||||
* @CKEditorPlugin(
|
||||
* id = "drupalmedia",
|
||||
* label = @Translation("Media Embed"),
|
||||
* )
|
||||
*
|
||||
* @internal
|
||||
* This is an internal part of the media system in Drupal core and may be
|
||||
* subject to change in minor releases. This class should not be
|
||||
* instantiated or extended by external code.
|
||||
*/
|
||||
class DrupalMedia extends PluginBase implements ContainerFactoryPluginInterface, CKEditorPluginContextualInterface, CKEditorPluginCssInterface {
|
||||
|
||||
/**
|
||||
* The module extension list.
|
||||
*
|
||||
* @var \Drupal\Core\Extension\ModuleExtensionList
|
||||
*/
|
||||
protected $moduleExtensionList;
|
||||
|
||||
/**
|
||||
* Constructs a new DrupalMedia plugin object.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin_id for the plugin instance.
|
||||
* @param array $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Extension\ModuleExtensionList $extension_list_module
|
||||
* The module extension list.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, ModuleExtensionList $extension_list_module) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
$this->moduleExtensionList = $extension_list_module;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('extension.list.module')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isInternal() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDependencies(Editor $editor) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLibraries(Editor $editor) {
|
||||
return [
|
||||
'core/jquery',
|
||||
'core/drupal',
|
||||
'core/drupal.ajax',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFile() {
|
||||
return $this->moduleExtensionList->getPath('media') . '/js/plugins/drupalmedia/plugin.js';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfig(Editor $editor) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isEnabled(Editor $editor) {
|
||||
if (!$editor->hasAssociatedFilterFormat()) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Automatically enable this plugin if the text format associated with this
|
||||
// text editor uses the media_embed filter.
|
||||
$filters = $editor->getFilterFormat()->filters();
|
||||
return $filters->has('media_embed') && $filters->get('media_embed')->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @todo Improve this in https://www.drupal.org/project/drupal/issues/3072063
|
||||
*/
|
||||
public function getCssFiles(Editor $editor) {
|
||||
return [
|
||||
$this->moduleExtensionList->getPath('media') . '/css/filter.media_embed.css',
|
||||
$this->moduleExtensionList->getPath('media') . '/css/plugins/drupalmedia/ckeditor.drupalmedia.css',
|
||||
$this->moduleExtensionList->getPath('system') . '/css/components/hidden.module.css',
|
||||
];
|
||||
}
|
||||
|
||||
}
|
|
@ -187,13 +187,13 @@ class MediaEmbed extends FilterBase implements ContainerFactoryPluginInterface,
|
|||
|
||||
// There are a few concerns when rendering an embedded media entity:
|
||||
// - entity access checking happens not during rendering but during routing,
|
||||
// and therefore we have to do it explicitly here for the embedded entity;
|
||||
// and therefore we have to do it explicitly here for the embedded entity.
|
||||
$build['#access'] = $media->access('view', NULL, TRUE);
|
||||
// - caching an embedded media entity separately is unnecessary; the host
|
||||
// entity is already render cached;
|
||||
// entity is already render cached.
|
||||
unset($build['#cache']['keys']);
|
||||
// - Contextual Links do not make sense for embedded entities; we only allow
|
||||
// the host entity to be contextually managed;
|
||||
// the host entity to be contextually managed.
|
||||
$build['#pre_render'][] = static::class . '::disableContextualLinks';
|
||||
// - default styling may break captioned media embeds; attach asset library
|
||||
// to ensure captions behave as intended. Do not set this at the root
|
||||
|
@ -206,19 +206,29 @@ class MediaEmbed extends FilterBase implements ContainerFactoryPluginInterface,
|
|||
}
|
||||
|
||||
/**
|
||||
* Builds the render array for a missing media entity.
|
||||
* Builds the render array for the indicator when media cannot be loaded.
|
||||
*
|
||||
* @return array
|
||||
* A render array.
|
||||
*
|
||||
* @todo Make this themeable in https://www.drupal.org/project/drupal/issues/3071713
|
||||
*/
|
||||
protected function renderMissingMedia() {
|
||||
protected function renderMissingMediaIndicator() {
|
||||
return [
|
||||
'#theme' => 'image',
|
||||
'#uri' => file_url_transform_relative(file_create_url('core/modules/media/images/icons/no-thumbnail.png')),
|
||||
'#width' => 180,
|
||||
'#height' => 180,
|
||||
'#alt' => $this->t('Missing media.'),
|
||||
'#title' => $this->t('Missing media.'),
|
||||
'#type' => 'html_tag',
|
||||
'#tag' => 'div',
|
||||
'#value' => $this->t('The referenced media source is missing and needs to be re-embedded.'),
|
||||
'#attached' => [
|
||||
'library' => [
|
||||
'media/media_embed',
|
||||
],
|
||||
],
|
||||
'#attributes' => [
|
||||
'class' => [
|
||||
'media-embed-error',
|
||||
'media-embed-error--missing-source',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -263,14 +273,27 @@ class MediaEmbed extends FilterBase implements ContainerFactoryPluginInterface,
|
|||
|
||||
$build = $media && $view_mode
|
||||
? $this->renderMedia($media, $view_mode_id, $langcode)
|
||||
: $this->renderMissingMedia();
|
||||
: $this->renderMissingMediaIndicator();
|
||||
|
||||
if (empty($build['#attributes']['class'])) {
|
||||
$build['#attributes']['class'] = [];
|
||||
}
|
||||
// Any attributes not consumed by the filter should be carried over to the
|
||||
// rendered embedded entity. For example, `data-align` and `data-caption`
|
||||
// should be carried over, so that even when embedded media goes missing,
|
||||
// at least the caption and visual structure won't get lost.
|
||||
foreach ($node->attributes as $attribute) {
|
||||
$build['#attributes'][$attribute->nodeName] = $attribute->nodeValue;
|
||||
if ($attribute->nodeName == 'class') {
|
||||
// We don't want to overwrite the existing CSS class of the embedded
|
||||
// media (or if the media entity can't be loaded, the missing media
|
||||
// indicator). But, we need to merge in CSS classes added by other
|
||||
// filters, such as filter_align, in order for those filters to work
|
||||
// properly.
|
||||
$build['#attributes']['class'] = array_unique(array_merge($build['#attributes']['class'], explode(' ', $attribute->nodeValue)));
|
||||
}
|
||||
else {
|
||||
$build['#attributes'][$attribute->nodeName] = $attribute->nodeValue;
|
||||
}
|
||||
}
|
||||
|
||||
$this->renderIntoDomNode($build, $node, $result);
|
||||
|
@ -303,7 +326,7 @@ class MediaEmbed extends FilterBase implements ContainerFactoryPluginInterface,
|
|||
* Renders the given render array into the given DOM node.
|
||||
*
|
||||
* @param array $build
|
||||
* The render array to render in isolation
|
||||
* The render array to render in isolation.
|
||||
* @param \DOMNode $node
|
||||
* The DOM node to render into.
|
||||
* @param \Drupal\filter\FilterProcessResult $result
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
name: Media CKEditor plugin test
|
||||
description: 'Provides functionality to test the Media Embed CKEditor integration.'
|
||||
type: module
|
||||
package: Testing
|
||||
version: VERSION
|
||||
core: 8.x
|
||||
dependencies:
|
||||
- drupal:media
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Helper module for the Media Embed CKEditor plugin tests.
|
||||
*/
|
||||
|
||||
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
|
||||
/**
|
||||
* Implements hook_entity_view_alter().
|
||||
*/
|
||||
function media_test_ckeditor_entity_view_alter(&$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
|
||||
// @see \Drupal\Tests\media\FunctionalJavascript\CKEditorIntegrationTest::testPreviewUsesDefaultThemeAndIsClientCacheable()
|
||||
$build['#attributes']['data-media-embed-test-active-theme'] = \Drupal::theme()->getActiveTheme()->getName();
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
services:
|
||||
media_test_ckeditor.route_subscriber:
|
||||
class: Drupal\media_test_ckeditor\Routing\RouteSubscriber
|
||||
tags:
|
||||
- { name: event_subscriber }
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\media_test_ckeditor\Controller;
|
||||
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
use Drupal\media\Controller\MediaFilterController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Controller to allow testing of error handling in drupalmedia plugin.js.
|
||||
*/
|
||||
class TestMediaFilterController extends MediaFilterController {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function preview(Request $request, FilterFormatInterface $filter_format) {
|
||||
if (\Drupal::state()->get('test_media_filter_controller_throw_error', FALSE)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
return parent::preview($request, $filter_format);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\media_test_ckeditor\Routing;
|
||||
|
||||
use Drupal\Core\Routing\RouteSubscriberBase;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Listens to the dynamic route events.
|
||||
*/
|
||||
class RouteSubscriber extends RouteSubscriberBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function alterRoutes(RouteCollection $collection) {
|
||||
if ($route = $collection->get('media.filter.preview')) {
|
||||
$route->setDefault('_controller', '\Drupal\media_test_ckeditor\Controller\TestMediaFilterController::preview');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,845 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\media\FunctionalJavascript;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||
use Drupal\media\Entity\Media;
|
||||
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
|
||||
use Drupal\Tests\TestFileCreationTrait;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\media\Plugin\CKEditorPlugin\DrupalMedia
|
||||
* @group media
|
||||
*/
|
||||
class CKEditorIntegrationTest extends WebDriverTestBase {
|
||||
|
||||
use MediaTypeCreationTrait;
|
||||
use TestFileCreationTrait;
|
||||
|
||||
/**
|
||||
* The user to use during testing.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* The sample Media entity to embed.
|
||||
*
|
||||
* @var \Drupal\media\MediaInterface
|
||||
*/
|
||||
protected $media;
|
||||
|
||||
/**
|
||||
* A host entity with a body field to embed media in.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface
|
||||
*/
|
||||
protected $host;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'ckeditor',
|
||||
'media',
|
||||
'node',
|
||||
'text',
|
||||
'media_test_ckeditor',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
FilterFormat::create([
|
||||
'format' => 'test_format',
|
||||
'name' => 'Test format',
|
||||
'filters' => [
|
||||
'filter_align' => ['status' => TRUE],
|
||||
'filter_caption' => ['status' => TRUE],
|
||||
'media_embed' => ['status' => TRUE],
|
||||
],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'editor' => 'ckeditor',
|
||||
'format' => 'test_format',
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'rows' => [
|
||||
[
|
||||
[
|
||||
'name' => 'All the things',
|
||||
'items' => [
|
||||
'Source',
|
||||
'Bold',
|
||||
'Italic',
|
||||
'DrupalLink',
|
||||
'DrupalUnlink',
|
||||
'DrupalImage',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
|
||||
// Note that media_install() grants 'view media' to all users by default.
|
||||
$this->adminUser = $this->drupalCreateUser([
|
||||
'use text format test_format',
|
||||
'bypass node access',
|
||||
]);
|
||||
|
||||
// Create a sample media entity to be embedded.
|
||||
$this->createMediaType('image', ['id' => 'image']);
|
||||
File::create([
|
||||
'uri' => $this->getTestFiles('image')[0]->uri,
|
||||
])->save();
|
||||
$this->media = Media::create([
|
||||
'bundle' => 'image',
|
||||
'name' => 'Screaming hairy armadillo',
|
||||
'field_media_image' => [
|
||||
[
|
||||
'target_id' => 1,
|
||||
'alt' => 'default alt',
|
||||
'title' => 'default title',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$this->media->save();
|
||||
|
||||
// Create a sample host entity to embed media in.
|
||||
$this->drupalCreateContentType(['type' => 'blog']);
|
||||
$this->host = $this->createNode([
|
||||
'type' => 'blog',
|
||||
'title' => 'Animals with strange names',
|
||||
'body' => [
|
||||
'value' => '<drupal-media data-caption="baz" data-entity-type="media" data-entity-uuid="' . $this->media->uuid() . '"></drupal-media>',
|
||||
'format' => 'test_format',
|
||||
],
|
||||
]);
|
||||
$this->host->save();
|
||||
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that only <drupal-media> tags are processed.
|
||||
*
|
||||
* @see \Drupal\Tests\media\Kernel\MediaEmbedFilterTest::testOnlyDrupalMediaTagProcessed()
|
||||
*/
|
||||
public function testOnlyDrupalMediaTagProcessed() {
|
||||
$original_value = $this->host->body->value;
|
||||
$this->host->body->value = str_replace('drupal-media', 'p', $original_value);
|
||||
$this->host->save();
|
||||
|
||||
// Assert that `<p data-* …>` is not upcast into a CKEditor Widget.
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$this->assignNameToCkeditorIframe();
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$assert_session = $this->assertSession();
|
||||
$this->assertEmpty($assert_session->waitForElementVisible('css', 'img[src*="image-test.png"]', 1000));
|
||||
$assert_session->elementNotExists('css', 'figure');
|
||||
|
||||
$this->host->body->value = $original_value;
|
||||
$this->host->save();
|
||||
|
||||
// Assert that `<drupal-media data-* …>` is upcast into a CKEditor Widget.
|
||||
$this->getSession()->reload();
|
||||
$this->waitForEditor();
|
||||
$this->assignNameToCkeditorIframe();
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'img[src*="image-test.png"]'));
|
||||
$assert_session->elementExists('css', 'figure');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that failed media embed preview requests inform the end user.
|
||||
*/
|
||||
public function testPreviewFailure() {
|
||||
// Assert that a request to the `media.filter.preview` route that does not
|
||||
// result in a 200 response (due to server error or network error) is
|
||||
// handled in the JavaScript by displaying the expected error message.
|
||||
$this->container->get('state')->set('test_media_filter_controller_throw_error', TRUE);
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$this->assignNameToCkeditorIframe();
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$assert_session = $this->assertSession();
|
||||
$this->assertEmpty($assert_session->waitForElementVisible('css', 'img[src*="image-test.png"]', 1000));
|
||||
$assert_session->elementNotExists('css', 'figure');
|
||||
$error_message = $assert_session->elementExists('css', '.media-embed-error.media-embed-error--preview-error')
|
||||
->getText();
|
||||
$this->assertSame('An error occurred while trying to preview the media. Please save your work and reload this page.', $error_message);
|
||||
// Now assert that the error doesn't appear when the override to force an
|
||||
// error is removed.
|
||||
$this->container->get('state')->set('test_media_filter_controller_throw_error', FALSE);
|
||||
$this->getSession()->reload();
|
||||
$this->waitForEditor();
|
||||
$this->assignNameToCkeditorIframe();
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'img[src*="image-test.png"]'));
|
||||
}
|
||||
|
||||
/**
|
||||
* The CKEditor Widget must load a preview generated using the default theme.
|
||||
*/
|
||||
public function testPreviewUsesDefaultThemeAndIsClientCacheable() {
|
||||
// Make the node edit form use the admin theme, like on most Drupal sites.
|
||||
$this->config('node.settings')
|
||||
->set('use_admin_theme', TRUE)
|
||||
->save();
|
||||
$this->container->get('router.builder')->rebuild();
|
||||
|
||||
// Allow the test user to view the admin theme.
|
||||
$this->adminUser->addRole($this->drupalCreateRole(['view the administration theme']));
|
||||
$this->adminUser->save();
|
||||
|
||||
// Configure a different default and admin theme, like on most Drupal sites.
|
||||
$this->config('system.theme')
|
||||
->set('default', 'stable')
|
||||
->set('admin', 'classy')
|
||||
->save();
|
||||
|
||||
// Assert that when looking at an embedded entity in the CKEditor Widget,
|
||||
// the preview is generated using the default theme, not the admin theme.
|
||||
// @see media_test_ckeditor_entity_view_alter()
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$this->assignNameToCkeditorIframe();
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$assert_session = $this->assertSession();
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'img[src*="image-test.png"]'));
|
||||
$element = $assert_session->elementExists('css', '[data-media-embed-test-active-theme]');
|
||||
$this->assertSame('stable', $element->getAttribute('data-media-embed-test-active-theme'));
|
||||
|
||||
// Assert that the first preview request transferred >500 B over the wire.
|
||||
// Then toggle source mode on and off. This causes the CKEditor widget to be
|
||||
// destroyed and then reconstructed. Assert that during this reconstruction,
|
||||
// a second request is sent. This second request should have transferred 0
|
||||
// bytes: the browser should have cached the response, thus resulting in a
|
||||
// much better user experience.
|
||||
$this->assertGreaterThan(500, $this->getLastPreviewRequestTransferSize());
|
||||
$this->pressEditorButton('source');
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', 'textarea.cke_source'));
|
||||
$this->pressEditorButton('source');
|
||||
$this->assignNameToCkeditorIframe();
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'img[src*="image-test.png"]'));
|
||||
$this->assertSame(0, $this->getLastPreviewRequestTransferSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests caption editing in the CKEditor widget.
|
||||
*/
|
||||
public function testEditableCaption() {
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$this->assignNameToCkeditorIframe();
|
||||
|
||||
// Type in the widget's editable for the caption.
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$assert_session = $this->assertSession();
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', 'figcaption'));
|
||||
$this->setCaption('Caught in a <strong>landslide</strong>! No escape from <em>reality</em>!');
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$assert_session->elementExists('css', 'figcaption > em');
|
||||
$assert_session->elementExists('css', 'figcaption > strong')->click();
|
||||
|
||||
// Select the <strong> element and unbold it.
|
||||
$this->clickPathLinkByTitleAttribute("strong element");
|
||||
$this->pressEditorButton('bold');
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$assert_session->elementExists('css', 'figcaption > em');
|
||||
$assert_session->elementNotExists('css', 'figcaption > strong');
|
||||
|
||||
// Select the <em> element and unitalicize it.
|
||||
$assert_session->elementExists('css', 'figcaption > em')->click();
|
||||
$this->clickPathLinkByTitleAttribute("em element");
|
||||
$this->pressEditorButton('italic');
|
||||
|
||||
// The "source" button should reveal the HTML source in a state matching
|
||||
// what is shown in the CKEditor widget.
|
||||
$this->pressEditorButton('source');
|
||||
$source = $assert_session->elementExists('css', 'textarea.cke_source');
|
||||
$value = $source->getValue();
|
||||
$dom = Html::load($value);
|
||||
$xpath = new \DOMXPath($dom);
|
||||
$drupal_media = $xpath->query('//drupal-media')[0];
|
||||
$this->assertSame('Caught in a landslide! No escape from reality!', $drupal_media->getAttribute('data-caption'));
|
||||
|
||||
// Change the caption by modifying the HTML source directly. When exiting
|
||||
// "source" mode, this should be respected.
|
||||
$poor_boy_text = "I'm just a <strong>poor boy</strong>, I need no sympathy!";
|
||||
$drupal_media->setAttribute("data-caption", $poor_boy_text);
|
||||
$source->setValue(Html::serialize($dom));
|
||||
$this->pressEditorButton('source');
|
||||
$this->assignNameToCkeditorIframe();
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$figcaption = $assert_session->waitForElement('css', 'figcaption');
|
||||
$this->assertNotEmpty($figcaption);
|
||||
$this->assertSame($poor_boy_text, $figcaption->getHtml());
|
||||
|
||||
// Select the <strong> element that we just set in "source" mode. This
|
||||
// proves that it was indeed rendered by the CKEditor widget.
|
||||
$strong = $figcaption->find('css', 'strong');
|
||||
$this->assertNotEmpty($strong);
|
||||
$strong->click();
|
||||
$this->pressEditorButton('bold');
|
||||
|
||||
// Insert a link into the caption.
|
||||
$this->clickPathLinkByTitleAttribute("Caption element");
|
||||
$this->pressEditorButton('drupallink');
|
||||
$field = $assert_session->waitForElementVisible('xpath', '//input[@name="attributes[href]"]');
|
||||
$this->assertNotEmpty($field);
|
||||
$field->setValue('https://www.drupal.org');
|
||||
$assert_session->elementExists('css', 'button.form-submit')->press();
|
||||
|
||||
// Wait for the live preview in the CKEditor widget to finish loading, then
|
||||
// edit the link; no `data-cke-saved-href` attribute should exist on it.
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$figcaption = $assert_session->waitForElement('css', 'figcaption');
|
||||
$page = $this->getSession()->getPage();
|
||||
// Wait for AJAX refresh.
|
||||
$page->waitFor(10, function () use ($figcaption) {
|
||||
return $figcaption->find('xpath', '//a[@href="https://www.drupal.org"]');
|
||||
});
|
||||
$assert_session->elementExists('css', 'a', $figcaption)->click();
|
||||
$this->clickPathLinkByTitleAttribute("a element");
|
||||
$this->pressEditorButton('drupallink');
|
||||
$field = $assert_session->waitForElementVisible('xpath', '//input[@name="attributes[href]"]');
|
||||
$this->assertNotEmpty($field);
|
||||
$field->setValue('https://www.drupal.org/project/drupal');
|
||||
$assert_session->elementExists('css', 'button.form-submit')->press();
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$figcaption = $assert_session->waitForElement('css', 'figcaption');
|
||||
$page = $this->getSession()->getPage();
|
||||
// Wait for AJAX refresh.
|
||||
$page->waitFor(10, function () use ($figcaption) {
|
||||
return $figcaption->find('xpath', '//a[@href="https://www.drupal.org/project/drupal"]');
|
||||
});
|
||||
$this->pressEditorButton('source');
|
||||
$source = $assert_session->elementExists('css', "textarea.cke_source");
|
||||
$value = $source->getValue();
|
||||
$this->assertContains('https://www.drupal.org/project/drupal', $value);
|
||||
$this->assertNotContains('data-cke-saved-href', $value);
|
||||
|
||||
// Save the entity.
|
||||
$assert_session->buttonExists('Save')->press();
|
||||
|
||||
// Verify the saved entity when viewed also contains the captioned media.
|
||||
$link = $assert_session->elementExists('css', 'figcaption > a');
|
||||
$this->assertSame('https://www.drupal.org/project/drupal', $link->getAttribute('href'));
|
||||
$this->assertSame("I'm just a poor boy, I need no sympathy!", $link->getText());
|
||||
|
||||
// Edit it again, type a different caption in the widget.
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$this->assignNameToCkeditorIframe();
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$this->assertNotEmpty($assert_session->waitForElementVisible('css', 'figcaption'));
|
||||
$this->setCaption('Scaramouch, <em>Scaramouch</em>, will you do the <strong>Fandango</strong>?');
|
||||
|
||||
// Erase the caption in the CKEditor Widget, verify the <figcaption> still
|
||||
// exists and contains placeholder text, then type something else.
|
||||
$this->setCaption('');
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$assert_session->elementContains('css', 'figcaption', '');
|
||||
$assert_session->elementAttributeContains('css', 'figcaption', 'data-placeholder', 'Enter caption here');
|
||||
$this->setCaption('Fin.');
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$assert_session->elementContains('css', 'figcaption', 'Fin.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests linkability of the CKEditor widget.
|
||||
*
|
||||
* @dataProvider linkabilityProvider
|
||||
*/
|
||||
public function testLinkability($drupalimage_is_enabled) {
|
||||
if (!$drupalimage_is_enabled) {
|
||||
// Remove the `drupalimage` plugin's `DrupalImage` button.
|
||||
$editor = Editor::load('test_format');
|
||||
$settings = $editor->getSettings();
|
||||
$rows = $settings['toolbar']['rows'];
|
||||
foreach ($rows as $row_key => $row) {
|
||||
foreach ($row as $group_key => $group) {
|
||||
foreach ($group['items'] as $item_key => $item) {
|
||||
if ($item === 'DrupalImage') {
|
||||
unset($settings['toolbar']['rows'][$row_key][$group_key]['items'][$item_key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$editor->setSettings($settings);
|
||||
$editor->save();
|
||||
}
|
||||
|
||||
$this->host->body->value .= '<p>The pirate is irate.</p><p>';
|
||||
if ($drupalimage_is_enabled) {
|
||||
// Add an image with a link wrapped around it.
|
||||
$uri = $this->media->field_media_image->entity->getFileUri();
|
||||
$src = file_url_transform_relative(file_create_url($uri));
|
||||
$this->host->body->value .= '<a href="http://www.drupal.org/association"><img alt="drupalimage test image" data-entity-type="" data-entity-uuid="" src="' . $src . '" /></a></p>';
|
||||
}
|
||||
$this->host->save();
|
||||
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$this->assignNameToCkeditorIframe();
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// Select the CKEditor Widget.
|
||||
$drupalmedia = $assert_session->waitForElementVisible('css', 'drupal-media');
|
||||
$this->assertNotEmpty($drupalmedia);
|
||||
$drupalmedia->click();
|
||||
|
||||
// While the CKEditor Widget is selected, assert the context menu does not
|
||||
// contain link-related context menu items.
|
||||
$this->openContextMenu();
|
||||
$this->assignNameToCkeditorPanelIframe();
|
||||
$this->getSession()->switchToIFrame('panel');
|
||||
$this->assertContextMenuItemNotExists('Edit Link');
|
||||
$this->assertContextMenuItemNotExists('Unlink');
|
||||
$this->closeContextMenu();
|
||||
|
||||
// While the CKEditor Widget is selected, click the "link" button.
|
||||
$this->pressEditorButton('drupallink');
|
||||
$assert_session->waitForId('drupal-modal');
|
||||
|
||||
// Enter a link in the link dialog and save.
|
||||
$field = $assert_session->waitForElementVisible('xpath', '//input[@name="attributes[href]"]');
|
||||
$this->assertNotEmpty($field);
|
||||
$field->setValue('https://www.drupal.org');
|
||||
$assert_session->elementExists('css', 'button.form-submit')->press();
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$link = $assert_session->waitForElementVisible('css', 'a[href="https://www.drupal.org"]');
|
||||
$this->assertNotEmpty($link);
|
||||
|
||||
// Select the CKEditor Widget again and assert the context menu now does
|
||||
// contain link-related context menu items.
|
||||
$drupalmedia = $assert_session->waitForElementVisible('css', 'drupal-media');
|
||||
$this->assertNotEmpty($drupalmedia);
|
||||
$drupalmedia->click();
|
||||
$this->openContextMenu();
|
||||
$this->getSession()->switchToIFrame('panel');
|
||||
$this->assertContextMenuItemExists('Edit Link');
|
||||
$this->assertContextMenuItemExists('Unlink');
|
||||
$this->closeContextMenu();
|
||||
|
||||
// Save the entity.
|
||||
$this->getSession()->switchToIFrame();
|
||||
$assert_session->buttonExists('Save')->press();
|
||||
|
||||
// Verify the saved entity when viewed also contains the linked media.
|
||||
$assert_session->elementExists('css', 'figure > a[href="https://www.drupal.org"] > .media--type-image > .field--type-image > img[src*="image-test.png"]');
|
||||
|
||||
// Test that `drupallink` also still works independently: inserting a link
|
||||
// is possible.
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$this->pressEditorButton('drupallink');
|
||||
$assert_session->waitForId('drupal-modal');
|
||||
$field = $assert_session->waitForElementVisible('xpath', '//input[@name="attributes[href]"]');
|
||||
$this->assertNotEmpty($field);
|
||||
$field->setValue('https://wikipedia.org');
|
||||
$assert_session->elementExists('css', 'button.form-submit')->press();
|
||||
$this->assignNameToCkeditorIframe();
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$link = $assert_session->waitForElementVisible('css', 'body > a[href="https://wikipedia.org"]');
|
||||
$this->assertNotEmpty($link);
|
||||
$assert_session->elementExists('css', 'body > .cke_widget_drupalmedia > drupal-media > figure > a[href="https://www.drupal.org"]');
|
||||
|
||||
// Select the CKEditor Widget again and assert the `drupalunlink` button is
|
||||
// enabled. Also assert the context menu again contains link-related context
|
||||
// menu items.
|
||||
$drupalmedia = $assert_session->waitForElementVisible('css', 'drupal-media');
|
||||
$this->assertNotEmpty($drupalmedia);
|
||||
$drupalmedia->click();
|
||||
$this->openContextMenu();
|
||||
$this->getSession()->switchToIFrame();
|
||||
$this->assertEditorButtonEnabled('drupalunlink');
|
||||
$this->assignNameToCkeditorPanelIframe();
|
||||
$this->getSession()->switchToIFrame('panel');
|
||||
$this->assertContextMenuItemExists('Edit Link');
|
||||
$this->assertContextMenuItemExists('Unlink');
|
||||
|
||||
// Test that moving focus to another element causes the `drupalunlink`
|
||||
// button to become disabled and causes link-related context menu items to
|
||||
// disappear.
|
||||
$this->getSession()->switchToIFrame();
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$p = $assert_session->waitForElementVisible('xpath', "//p[contains(text(), 'The pirate is irate')]");
|
||||
$this->assertNotEmpty($p);
|
||||
$p->click();
|
||||
$this->assertEditorButtonDisabled('drupalunlink');
|
||||
$this->getSession()->switchToIFrame('panel');
|
||||
$this->assertContextMenuItemExists('Edit Link');
|
||||
$this->assertContextMenuItemExists('Unlink');
|
||||
|
||||
// To switch from the context menu iframe ("panel") back to the CKEditor
|
||||
// iframe, we first have to reset to top frame.
|
||||
$this->getSession()->switchToIFrame();
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
|
||||
// Test that moving focus to the `drupalimage` CKEditor Widget enables the
|
||||
// `drupalunlink` button again, because it is a linked image.
|
||||
if ($drupalimage_is_enabled) {
|
||||
$drupalimage = $assert_session->waitForElementVisible('xpath', '//img[@alt="drupalimage test image"]');
|
||||
$this->assertNotEmpty($drupalimage);
|
||||
$drupalimage->click();
|
||||
$this->assertEditorButtonEnabled('drupalunlink');
|
||||
$this->getSession()->switchToIFrame('panel');
|
||||
$this->assertContextMenuItemExists('Edit Link');
|
||||
$this->assertContextMenuItemExists('Unlink');
|
||||
$this->getSession()->switchToIFrame();
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
}
|
||||
|
||||
// Tests the `drupalunlink` button for the `drupalmedia` CKEditor Widget.
|
||||
$drupalmedia->click();
|
||||
$this->assertEditorButtonEnabled('drupalunlink');
|
||||
$this->getSession()->switchToIFrame('panel');
|
||||
$this->assertContextMenuItemExists('Edit Link');
|
||||
$this->assertContextMenuItemExists('Unlink');
|
||||
$this->pressEditorButton('drupalunlink');
|
||||
$this->assertEditorButtonDisabled('drupalunlink');
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$assert_session->elementNotExists('css', 'figure > a[href="https://www.drupal.org"] > .media--type-image > .field--type-image > img[src*="image-test.png"]');
|
||||
$assert_session->elementExists('css', 'figure .media--type-image > .field--type-image > img[src*="image-test.png"]');
|
||||
if ($drupalimage_is_enabled) {
|
||||
// Tests the `drupalunlink` button for the `drupalimage` CKEditor Widget.
|
||||
$drupalimage->click();
|
||||
$this->assertEditorButtonEnabled('drupalunlink');
|
||||
$this->pressEditorButton('drupalunlink');
|
||||
$this->assertEditorButtonDisabled('drupalunlink');
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$assert_session->elementNotExists('css', 'p > a[href="https://www.drupal.org/association"] > img[src*="image-test.png"]');
|
||||
$assert_session->elementExists('css', 'p > img[src*="image-test.png"]');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data Provider for ::testLinkability.
|
||||
*/
|
||||
public function linkabilityProvider() {
|
||||
return [
|
||||
'linkability when `drupalimage` is enabled' => [
|
||||
TRUE,
|
||||
],
|
||||
'linkability when `drupalimage` is disabled' => [
|
||||
FALSE,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests preview route access.
|
||||
*
|
||||
* @param bool $media_embed_enabled
|
||||
* Whether to test with media_embed filter enabled on the text format.
|
||||
* @param bool $can_use_format
|
||||
* Whether the logged in user is allowed to use the text format.
|
||||
*
|
||||
* @dataProvider previewAccessProvider
|
||||
*/
|
||||
public function testEmbedPreviewAccess($media_embed_enabled, $can_use_format) {
|
||||
$format = FilterFormat::create([
|
||||
'format' => $this->randomMachineName(),
|
||||
'name' => $this->randomString(),
|
||||
'filters' => [
|
||||
'filter_align' => ['status' => TRUE],
|
||||
'filter_caption' => ['status' => TRUE],
|
||||
'media_embed' => ['status' => $media_embed_enabled],
|
||||
],
|
||||
]);
|
||||
$format->save();
|
||||
|
||||
$permissions = [
|
||||
'bypass node access',
|
||||
];
|
||||
if ($can_use_format) {
|
||||
$permissions[] = $format->getPermissionName();
|
||||
}
|
||||
$this->drupalLogin($this->drupalCreateUser($permissions));
|
||||
|
||||
$text = '<drupal-media data-caption="baz" data-entity-type="media" data-entity-uuid="' . $this->media->uuid() . '"></drupal-media>';
|
||||
$route_parameters = ['filter_format' => $format->id()];
|
||||
$options = ['query' => ['text' => $text]];
|
||||
$this->drupalGet(Url::fromRoute('media.filter.preview', $route_parameters, $options));
|
||||
|
||||
$assert_session = $this->assertSession();
|
||||
if ($media_embed_enabled && $can_use_format) {
|
||||
$assert_session->elementExists('css', 'img');
|
||||
$assert_session->responseContains('baz');
|
||||
}
|
||||
else {
|
||||
$assert_session->responseContains('You are not authorized to access this page.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for ::testEmbedPreviewAccess.
|
||||
*/
|
||||
public function previewAccessProvider() {
|
||||
return [
|
||||
'media_embed filter enabled' => [
|
||||
TRUE,
|
||||
TRUE,
|
||||
],
|
||||
'media_embed filter disabled' => [
|
||||
FALSE,
|
||||
TRUE,
|
||||
],
|
||||
'media_embed filter enabled, user not allowed to use text format' => [
|
||||
TRUE,
|
||||
FALSE,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that alignment is reflected onto the CKEditor Widget wrapper.
|
||||
*/
|
||||
public function testAlignmentClasses() {
|
||||
$alignments = [
|
||||
'right',
|
||||
'left',
|
||||
'center',
|
||||
];
|
||||
$assert_session = $this->assertSession();
|
||||
foreach ($alignments as $alignment) {
|
||||
$this->host->body->value = '<drupal-media data-align="' . $alignment . '" data-entity-type="media" data-entity-uuid="' . $this->media->uuid() . '"></drupal-media>';
|
||||
$this->host->save();
|
||||
|
||||
// The upcasted CKEditor Widget's wrapper must get an `align-*` class.
|
||||
$this->drupalGet($this->host->toUrl('edit-form'));
|
||||
$this->waitForEditor();
|
||||
$this->assignNameToCkeditorIframe();
|
||||
$this->getSession()->switchToIFrame('ckeditor');
|
||||
$wrapper = $assert_session->waitForElementVisible('css', '.cke_widget_drupalmedia', 2000);
|
||||
$this->assertNotEmpty($wrapper);
|
||||
$this->assertTrue($wrapper->hasClass('align-' . $alignment));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the transfer size of the last preview request.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function getLastPreviewRequestTransferSize() {
|
||||
$this->getSession()->switchToIFrame();
|
||||
$javascript = <<<JS
|
||||
(function(){
|
||||
return window.performance
|
||||
.getEntries()
|
||||
.filter(function (entry) {
|
||||
return entry.initiatorType == 'xmlhttprequest' && entry.name.indexOf('/media/test_format/preview') !== -1;
|
||||
})
|
||||
.pop()
|
||||
.transferSize;
|
||||
})()
|
||||
JS;
|
||||
return $this->getSession()->evaluateScript($javascript);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the text of the editable caption to the given text.
|
||||
*
|
||||
* @param string $text
|
||||
* The text to set in the caption.
|
||||
*/
|
||||
protected function setCaption($text) {
|
||||
$this->getSession()->switchToIFrame();
|
||||
$select_and_edit_caption = "var editor = CKEDITOR.instances['edit-body-0-value'];
|
||||
var figcaption = editor.widgets.getByElement(editor.editable().findOne('figcaption'));
|
||||
figcaption.editables.caption.setData('" . $text . "')";
|
||||
$this->getSession()->executeScript($select_and_edit_caption);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a name to the CKEditor iframe.
|
||||
*
|
||||
* @see \Behat\Mink\Session::switchToIFrame()
|
||||
*/
|
||||
protected function assignNameToCkeditorIframe() {
|
||||
$javascript = <<<JS
|
||||
(function(){
|
||||
document.getElementsByClassName('cke_wysiwyg_frame')[0].id = 'ckeditor';
|
||||
})()
|
||||
JS;
|
||||
$this->getSession()->evaluateScript($javascript);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a name to the CKEditor context menu iframe.
|
||||
*
|
||||
* Note that this iframe doesn't appear until context menu appears.
|
||||
*
|
||||
* @see \Behat\Mink\Session::switchToIFrame()
|
||||
*/
|
||||
protected function assignNameToCkeditorPanelIframe() {
|
||||
$javascript = <<<JS
|
||||
(function(){
|
||||
document.getElementsByClassName('cke_panel_frame')[0].id = 'panel';
|
||||
})()
|
||||
JS;
|
||||
$this->getSession()->evaluateScript($javascript);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks a CKEditor button.
|
||||
*
|
||||
* @param string $name
|
||||
* The name of the button, such as drupalink, source, etc.
|
||||
*/
|
||||
protected function pressEditorButton($name) {
|
||||
$this->getSession()->switchToIFrame();
|
||||
$button = $this->assertSession()->waitForElementVisible('css', 'a.cke_button__' . $name);
|
||||
$this->assertNotEmpty($button);
|
||||
$button->click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a CKEditor button and returns it when available and visible.
|
||||
*
|
||||
* @param string $name
|
||||
* The name of the button, such as drupalink, source, etc.
|
||||
*
|
||||
* @return \Behat\Mink\Element\NodeElement|null
|
||||
* The page element node if found, NULL if not.
|
||||
*/
|
||||
protected function getEditorButton($name) {
|
||||
$this->getSession()->switchToIFrame();
|
||||
$button = $this->assertSession()->waitForElementVisible('css', 'a.cke_button__' . $name);
|
||||
$this->assertNotEmpty($button);
|
||||
|
||||
return $button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a CKEditor button is disabled.
|
||||
*
|
||||
* @param string $name
|
||||
* The name of the button, such as `drupallink`, `source`, etc.
|
||||
*/
|
||||
protected function assertEditorButtonDisabled($name) {
|
||||
$button = $this->getEditorButton($name);
|
||||
$this->assertTrue($button->hasClass('cke_button_disabled'));
|
||||
$this->assertSame('true', $button->getAttribute('aria-disabled'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a CKEditor button is enabled.
|
||||
*
|
||||
* @param string $name
|
||||
* The name of the button, such as `drupallink`, `source`, etc.
|
||||
*/
|
||||
protected function assertEditorButtonEnabled($name) {
|
||||
$button = $this->getEditorButton($name);
|
||||
$this->assertFalse($button->hasClass('cke_button_disabled'));
|
||||
$this->assertSame('false', $button->getAttribute('aria-disabled'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for CKEditor to initialize.
|
||||
*
|
||||
* @param string $instance_id
|
||||
* The CKEditor instance ID.
|
||||
* @param int $timeout
|
||||
* (optional) Timeout in milliseconds, defaults to 10000.
|
||||
*/
|
||||
protected function waitForEditor($instance_id = 'edit-body-0-value', $timeout = 10000) {
|
||||
$condition = <<<JS
|
||||
(function() {
|
||||
return (
|
||||
typeof CKEDITOR !== 'undefined'
|
||||
&& typeof CKEDITOR.instances["$instance_id"] !== 'undefined'
|
||||
&& CKEDITOR.instances["$instance_id"].instanceReady
|
||||
);
|
||||
}());
|
||||
JS;
|
||||
|
||||
$this->getSession()->wait($timeout, $condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the context menu for the currently selected widget.
|
||||
*
|
||||
* @param string $instance_id
|
||||
* The CKEditor instance ID.
|
||||
*/
|
||||
protected function openContextMenu($instance_id = 'edit-body-0-value') {
|
||||
$this->getSession()->switchToIFrame();
|
||||
$script = <<<JS
|
||||
(function() {
|
||||
var editor = CKEDITOR.instances["$instance_id"];
|
||||
editor.contextMenu.open(editor.widgets.selected[0].element);
|
||||
}());
|
||||
JS;
|
||||
$this->getSession()->executeScript($script);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a context menu item exists by aria-label attribute.
|
||||
*
|
||||
* @param string $label
|
||||
* The `aria-label` attribute value of the context menu item.
|
||||
*/
|
||||
protected function assertContextMenuItemExists($label) {
|
||||
$this->assertSession()->elementExists('xpath', '//a[@aria-label="' . $label . '"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a context menu item does not exist by aria-label attribute.
|
||||
*
|
||||
* @param string $label
|
||||
* The `aria-label` attribute value of the context menu item.
|
||||
*/
|
||||
protected function assertContextMenuItemNotExists($label) {
|
||||
$this->assertSession()->elementNotExists('xpath', '//a[@aria-label="' . $label . '"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the open context menu.
|
||||
*
|
||||
* @param string $instance_id
|
||||
* The CKEditor instance ID.
|
||||
*/
|
||||
protected function closeContextMenu($instance_id = 'edit-body-0-value') {
|
||||
$this->getSession()->switchToIFrame();
|
||||
$script = <<<JS
|
||||
(function() {
|
||||
var editor = CKEDITOR.instances["$instance_id"];
|
||||
editor.contextMenu.hide();
|
||||
}());
|
||||
JS;
|
||||
$this->getSession()->executeScript($script);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks a link in the editor's path links with the given title text.
|
||||
*
|
||||
* @param string $text
|
||||
* The title attribute of the link to click.
|
||||
*
|
||||
* @throws \Behat\Mink\Exception\ElementNotFoundException
|
||||
*/
|
||||
protected function clickPathLinkByTitleAttribute($text) {
|
||||
$this->getSession()->switchToIFrame();
|
||||
$selector = '//span[@id="cke_1_path"]//a[@title="' . $text . '"]';
|
||||
$this->assertSession()->elementExists('xpath', $selector)->click();
|
||||
}
|
||||
|
||||
}
|
|
@ -87,7 +87,7 @@ class MediaAccessControlHandlerTest extends MediaKernelTestBase {
|
|||
* @param string[] $expected_cache_contexts
|
||||
* Expected contexts.
|
||||
* @param string[] $expected_cache_tags
|
||||
* Expected cache tags
|
||||
* Expected cache tags.
|
||||
* @param \Drupal\Core\Access\AccessResultInterface $actual
|
||||
* The actual access result.
|
||||
*/
|
||||
|
|
|
@ -258,30 +258,32 @@ class MediaEmbedFilterTest extends MediaEmbedFilterTestBase {
|
|||
*
|
||||
* @dataProvider providerMissingEntityIndicator
|
||||
*/
|
||||
public function testMissingEntityIndicator($uuid) {
|
||||
public function testMissingEntityIndicator($uuid, array $filter_ids, array $additional_attributes) {
|
||||
$content = $this->createEmbedCode([
|
||||
'data-entity-type' => 'media',
|
||||
'data-entity-uuid' => $uuid,
|
||||
'data-view-mode' => 'foobar',
|
||||
]);
|
||||
] + $additional_attributes);
|
||||
|
||||
// If the UUID being used in the embed is that of the sample entity, first
|
||||
// assert that it currently results in a functional embed, then delete it.
|
||||
if ($uuid === static::EMBEDDED_ENTITY_UUID) {
|
||||
$this->applyFilter($content);
|
||||
$result = $this->processText($content, 'en', $filter_ids);
|
||||
$this->setRawContent($result->getProcessedText());
|
||||
$this->assertCount(1, $this->cssSelect('div[data-media-embed-test-view-mode="foobar"]'));
|
||||
$this->embeddedEntity->delete();
|
||||
}
|
||||
|
||||
$this->applyFilter($content);
|
||||
$result = $this->processText($content, 'en', $filter_ids);
|
||||
$this->setRawContent($result->getProcessedText());
|
||||
$this->assertCount(0, $this->cssSelect('div[data-media-embed-test-view-mode="foobar"]'));
|
||||
$deleted_embed_warning = $this->cssSelect('img')[0];
|
||||
$this->assertNotEmpty($deleted_embed_warning);
|
||||
$this->assertHasAttributes($deleted_embed_warning, [
|
||||
'alt' => 'Missing media.',
|
||||
'src' => file_url_transform_relative(file_create_url('core/modules/media/images/icons/no-thumbnail.png')),
|
||||
'title' => 'Missing media.',
|
||||
]);
|
||||
$this->assertCount(1, $this->cssSelect('div.media-embed-error'));
|
||||
$error_element = $this->cssSelect('div.media-embed-error')[0];
|
||||
$expected_class = 'media-embed-error media-embed-error--missing-source';
|
||||
if (in_array('filter_align', $filter_ids, TRUE) && !empty($additional_attributes['data-align'])) {
|
||||
$expected_class .= ' align-' . $additional_attributes['data-align'];
|
||||
}
|
||||
$this->assertSame($expected_class, (string) $error_element['class']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -289,17 +291,57 @@ class MediaEmbedFilterTest extends MediaEmbedFilterTestBase {
|
|||
*/
|
||||
public function providerMissingEntityIndicator() {
|
||||
return [
|
||||
'valid UUID but for a deleted entity' => [
|
||||
static::EMBEDDED_ENTITY_UUID,
|
||||
'invalid UUID' => [
|
||||
'uuid' => 'invalidUUID',
|
||||
'filter_ids' => [
|
||||
'filter_align',
|
||||
'filter_caption',
|
||||
'media_embed',
|
||||
],
|
||||
'additional_attributes' => [],
|
||||
],
|
||||
'node; invalid UUID' => [
|
||||
'invalidUUID',
|
||||
'valid UUID but for a deleted entity' => [
|
||||
'uuid' => static::EMBEDDED_ENTITY_UUID,
|
||||
'filter_ids' => [
|
||||
'filter_align',
|
||||
'filter_caption',
|
||||
'media_embed',
|
||||
],
|
||||
'additional_attributes' => [],
|
||||
],
|
||||
'invalid UUID; data-align attribute without filter_align enabled' => [
|
||||
'uuid' => 'invalidUUID',
|
||||
'filter_ids' => [
|
||||
'filter_caption',
|
||||
'media_embed',
|
||||
],
|
||||
'additional_attributes' => ['data-align' => 'right'],
|
||||
],
|
||||
'invalid UUID; data-align attribute with filter_align enabled' => [
|
||||
'uuid' => 'invalidUUID',
|
||||
'filter_ids' => [
|
||||
'filter_align',
|
||||
'filter_caption',
|
||||
'media_embed',
|
||||
],
|
||||
'additional_attributes' => ['data-align' => 'left'],
|
||||
],
|
||||
'valid UUID but for a deleted entity; data-align attribute with filter_align enabled' => [
|
||||
'uuid' => static::EMBEDDED_ENTITY_UUID,
|
||||
'filter_ids' => [
|
||||
'filter_align',
|
||||
'filter_caption',
|
||||
'media_embed',
|
||||
],
|
||||
'additional_attributes' => ['data-align' => 'center'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that only <drupal-media> tags are processed.
|
||||
*
|
||||
* @see \Drupal\Tests\media\FunctionalJavascript\CKEditorIntegrationTest::testOnlyDrupalMediaTagProcessed()
|
||||
*/
|
||||
public function testOnlyDrupalMediaTagProcessed() {
|
||||
$content = $this->createEmbedCode([
|
||||
|
@ -339,7 +381,7 @@ class MediaEmbedFilterTest extends MediaEmbedFilterTestBase {
|
|||
* @covers \Drupal\filter\Plugin\Filter\FilterCaption
|
||||
* @dataProvider providerFilterIntegration
|
||||
*/
|
||||
public function testFilterIntegration(array $filter_ids, array $additional_attributes, $verification_selector, $expected_verification_success, array $expected_asset_libraries, $prefix = '', $suffix = '') {
|
||||
public function testFilterIntegration(array $filter_ids, array $additional_attributes, $verification_selector, $expected_verification_success, array $expected_asset_libraries = [], $prefix = '', $suffix = '') {
|
||||
$content = $this->createEmbedCode([
|
||||
'data-entity-type' => 'media',
|
||||
'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
|
||||
|
|
|
@ -174,7 +174,7 @@ abstract class MediaEmbedFilterTestBase extends KernelTestBase {
|
|||
* The filtered text, wrapped in a FilterProcessResult object, and possibly
|
||||
* with associated assets, cacheability metadata and placeholders.
|
||||
*
|
||||
* @see \Drupal\Tests\entity_embed\Kernel\EntityEmbedFilterTestBase::createEmbedCode()
|
||||
* @see \Drupal\Tests\media\Kernel\MediaEmbedFilterTestBase::createEmbedCode()
|
||||
* @see \Drupal\KernelTests\AssertContentTrait::setRawContent()
|
||||
*/
|
||||
protected function applyFilter($text, $langcode = 'en') {
|
||||
|
|
|
@ -8,3 +8,9 @@
|
|||
float: none;
|
||||
margin: unset;
|
||||
}
|
||||
|
||||
/* Undo margin added to figure in core/assets/vendor/normalize-css/normalize.css */
|
||||
figure.caption-drupal-media.align-center {
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* @file
|
||||
* Media Embed filter: default styling for media embed errors.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The caption filter's styling overrides ours, so add a more specific selector
|
||||
* to account for that.
|
||||
*/
|
||||
.media-embed-error,
|
||||
.caption > .media-embed-error {
|
||||
max-width: 200px;
|
||||
padding: 100px 20px 20px;
|
||||
text-align: center;
|
||||
background-color: #ebebeb;
|
||||
background-image: url(../../../../modules/media/images/icons/no-thumbnail.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center top;
|
||||
background-size: 100px 100px;
|
||||
}
|
|
@ -158,6 +158,11 @@ libraries-override:
|
|||
component:
|
||||
css/oembed.frame.css: css/media/oembed.frame.css
|
||||
|
||||
media/media_embed:
|
||||
css:
|
||||
component:
|
||||
css/filter.media_embed.css: css/media/filter.media_embed.css
|
||||
|
||||
menu_ui/drupal.menu_ui.adminforms:
|
||||
css:
|
||||
theme:
|
||||
|
|
Loading…
Reference in New Issue