diff --git a/core/modules/ckeditor/js/plugins/drupalimage/plugin.js b/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
index 80921c250793365ff07920cd7b2f5620f83bbf4f..c481264473cf43bd0d56c0d41de58c377800757a 100644
--- a/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
+++ b/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
@@ -65,14 +65,11 @@
}
widgetDefinition.allowedContent = new CKEDITOR.style(allowedContentDefinition);
- // Override the 'link' part, to completely disable image2's link
- // support: http://dev.ckeditor.com/ticket/11341.
- widgetDefinition.parts.link = 'This is a nonsensical selector to disable this functionality completely';
-
// Override downcast(): since we only accept in our upcast method,
// the element is already correct. We only need to update the element's
// data-entity-uuid attribute.
widgetDefinition.downcast = function (element) {
+ element.attributes['data-entity-type'] = this.data['data-entity-type'];
element.attributes['data-entity-uuid'] = this.data['data-entity-uuid'];
};
@@ -176,6 +173,18 @@
return widget;
};
};
+
+ var originalInit = widgetDefinition.init;
+ widgetDefinition.init = function () {
+ originalInit.call(this);
+
+ // Update data.link object with attributes if the link has been
+ // discovered.
+ // @see plugins/image2/plugin.js/init() in CKEditor; this is similar.
+ if (this.parts.link) {
+ this.setData('link', CKEDITOR.plugins.link.parseLinkAttributes(editor, this.parts.link));
+ }
+ };
});
// Add a widget#edit listener to every instance of image2 widget in order
@@ -233,25 +242,86 @@
}
},
- // Disable image2's integration with the link/drupallink plugins: don't
- // allow the widget itself to become a link. Support for that may be added
- // by an text filter that adds a data- attribute specifically for that.
afterInit: function (editor) {
- if (editor.plugins.drupallink) {
- var cmd = editor.getCommand('drupallink');
- // Needs to be refreshed on selection changes.
- cmd.contextSensitive = 1;
- // Disable command and cancel event when the image widget is selected.
- cmd.on('refresh', function (evt) {
- var widget = editor.widgets.focused;
- if (widget && widget.name === 'image') {
- this.setState(CKEDITOR.TRISTATE_DISABLED);
- evt.cancel();
- }
- });
- }
+ linkCommandIntegrator(editor);
}
});
+ /**
+ * Integrates the drupalimage widget with the drupallink plugin.
+ *
+ * Makes images linkable.
+ *
+ * @param {CKEDITOR.editor} editor
+ * A CKEditor instance.
+ */
+ function linkCommandIntegrator(editor) {
+ // Nothing to integrate with if the drupallink plugin is not loaded.
+ if (!editor.plugins.drupallink) {
+ return;
+ }
+
+ // Override default behaviour of 'drupalunlink' command.
+ editor.getCommand('drupalunlink').on('exec', function (evt) {
+ var widget = getFocusedWidget(editor);
+
+ // Override 'drupalunlink' only when link truly belongs to the widget. If
+ // wrapped inline widget in a link, let default unlink work.
+ // @see https://dev.ckeditor.com/ticket/11814
+ if (!widget || !widget.parts.link) {
+ return;
+ }
+
+ widget.setData('link', null);
+
+ // Selection (which is fake) may not change if unlinked image in focused
+ // widget, i.e. if captioned image. Let's refresh command state manually
+ // here.
+ this.refresh(editor, editor.elementPath());
+
+ evt.cancel();
+ });
+
+ // Override default refresh of 'drupalunlink' command.
+ editor.getCommand('drupalunlink').on('refresh', function (evt) {
+ var widget = getFocusedWidget(editor);
+
+ if (!widget) {
+ return;
+ }
+
+ // Note that widget may be wrapped in a link, which
+ // does not belong to that widget (#11814).
+ this.setState(widget.data.link || widget.wrapper.getAscendant('a') ?
+ CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED);
+
+ evt.cancel();
+ });
+ }
+
+ /**
+ * 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 image2 widget instance, or null.
+ */
+ function getFocusedWidget(editor) {
+ var widget = editor.widgets.focused;
+
+ if (widget && widget.name === 'image') {
+ return widget;
+ }
+
+ return null;
+ }
+
+ // Expose an API for other plugins to interact with drupalimage widgets.
+ CKEDITOR.plugins.drupalimage = {
+ getFocusedWidget: getFocusedWidget
+ };
+
})(jQuery, Drupal, CKEDITOR);
diff --git a/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js b/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js
index 8dd91b17048c462a2147b8a6d82e81b59b76094f..9fba103c308bc4b22f2c30f3443679541298746e 100644
--- a/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js
+++ b/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js
@@ -71,10 +71,9 @@
// data-caption attributes.
var originalDowncast = widgetDefinition.downcast;
widgetDefinition.downcast = function (element) {
- var img = originalDowncast.call(this, element);
- if (!img) {
- img = findElementByName(element, 'img');
- }
+ var img = findElementByName(element, 'img');
+ originalDowncast.call(this, img);
+
var caption = this.editables.caption;
var captionHtml = caption && caption.getData();
var attrs = img.attributes;
@@ -91,10 +90,14 @@
attrs['data-align'] = this.data.align;
}
}
- attrs['data-entity-type'] = this.data['data-entity-type'];
- attrs['data-entity-uuid'] = this.data['data-entity-uuid'];
- return img;
+ // If img is wrapped with a link, we want to return that link.
+ if (img.parent.name === 'a') {
+ return img.parent;
+ }
+ else {
+ return img;
+ }
};
// We want to upcast elements to a DOM structure required by the
@@ -115,6 +118,11 @@
element = originalUpcast.call(this, element, data);
var attrs = element.attributes;
+
+ if (element.parent.name === 'a') {
+ element = element.parent;
+ }
+
var retElement = element;
var caption;
diff --git a/core/modules/ckeditor/js/plugins/drupallink/plugin.js b/core/modules/ckeditor/js/plugins/drupallink/plugin.js
index e9fb555b8eda2bff4100568b7989a55a75f02822..2cc9bc10069f5409be8ee9d7804434ea9c35d3fa 100644
--- a/core/modules/ckeditor/js/plugins/drupallink/plugin.js
+++ b/core/modules/ckeditor/js/plugins/drupallink/plugin.js
@@ -31,6 +31,8 @@
modes: {wysiwyg: 1},
canUndo: true,
exec: function (editor) {
+ var drupalImageUtils = CKEDITOR.plugins.drupalimage;
+ var focusedImageWidget = drupalImageUtils && drupalImageUtils.getFocusedWidget(editor);
var linkElement = getSelectedLink(editor);
var linkDOMElement = null;
@@ -56,9 +58,30 @@
existingValues[attributeName] = linkElement.data('cke-saved-' + attributeName) || attribute.nodeValue;
}
}
+ // Or, if an image widget is focused, we're editing a link wrapping
+ // an image widget.
+ else if (focusedImageWidget && focusedImageWidget.data.link) {
+ var url = focusedImageWidget.data.link.url;
+ existingValues.href = url.protocol + url.url;
+ }
// Prepare a save callback to be used upon saving the dialog.
var 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) {
+ var urlMatch = returnValues.attributes.href.match(urlRegex);
+ focusedImageWidget.setData('link', {
+ type: 'url',
+ url: {
+ protocol: urlMatch[1],
+ url: urlMatch[2]
+ }
+ });
+ editor.fire('saveSnapshot');
+ return;
+ }
+
editor.fire('saveSnapshot');
// Create a new link element if needed.
@@ -256,4 +279,57 @@
return null;
}
+ var urlRegex = /^((?:http|https):\/\/)?(.*)$/;
+
+ /**
+ * The image2 plugin is currently tightly coupled to the link plugin: it
+ * calls CKEDITOR.plugins.link.parseLinkAttributes().
+ *
+ * Drupal 8's CKEditor build doesn't include the 'link' plugin. Because it
+ * includes its own link plugin that integrates with Drupal's dialog system.
+ * So, to allow images to be linked, we need to duplicate the necessary subset
+ * of the logic.
+ *
+ * @todo Remove once we update to CKEditor 4.5.5.
+ * @see https://dev.ckeditor.com/ticket/13885
+ */
+ CKEDITOR.plugins.link = CKEDITOR.plugins.link || {
+ parseLinkAttributes: function (editor, element) {
+ var href = (element && (element.data('cke-saved-href') || element.getAttribute('href'))) || '';
+ var urlMatch = href.match(urlRegex);
+ return {
+ type: 'url',
+ url: {
+ protocol: urlMatch[1],
+ url: urlMatch[2]
+ }
+ };
+ },
+ getLinkAttributes: function (editor, data) {
+ var set = {};
+
+ var protocol = (data.url && typeof data.url.protocol !== 'undefined') ? data.url.protocol : 'http://';
+ var url = (data.url && CKEDITOR.tools.trim(data.url.url)) || '';
+ set['data-cke-saved-href'] = (url.indexOf('/') === 0) ? url : protocol + url;
+
+ // Browser need the "href" fro copy/paste link to work. (#6641)
+ if (set['data-cke-saved-href']) {
+ set.href = set['data-cke-saved-href'];
+ }
+
+ // Remove all attributes which are not currently set.
+ var removed = {};
+ for (var s in set) {
+ if (set.hasOwnProperty(s)) {
+ delete removed[s];
+ }
+ }
+
+ return {
+ set: set,
+ removed: CKEDITOR.tools.objectKeys(removed)
+ };
+ }
+ };
+
})(jQuery, Drupal, drupalSettings, CKEDITOR);
diff --git a/core/modules/filter/src/Plugin/Filter/FilterCaption.php b/core/modules/filter/src/Plugin/Filter/FilterCaption.php
index 37c18cc3bc76d66fc9a7d9838f556b16ca3325b4..76a11258303e28f35eeb12d8bd96a54ecdddd090 100644
--- a/core/modules/filter/src/Plugin/Filter/FilterCaption.php
+++ b/core/modules/filter/src/Plugin/Filter/FilterCaption.php
@@ -55,15 +55,17 @@ public function process($text, $langcode) {
// Given the updated node and caption: re-render it with a caption, but
// bubble up the value of the class attribute of the captioned element,
// this allows it to collaborate with e.g. the filter_align filter.
+ $tag = $node->tagName;
$classes = $node->getAttribute('class');
$node->removeAttribute('class');
+ $node = ($node->parentNode->tagName === 'a') ? $node->parentNode : $node;
$filter_caption = array(
'#theme' => 'filter_caption',
// We pass the unsanitized string because this is a text format
// filter, and after filtering, we always assume the output is safe.
// @see \Drupal\filter\Element\ProcessedText::preRenderText()
'#node' => FilteredMarkup::create($node->C14N()),
- '#tag' => $node->tagName,
+ '#tag' => $tag,
'#caption' => $caption,
'#classes' => $classes,
);
@@ -78,7 +80,7 @@ public function process($text, $langcode) {
// Import the updated node from the new DOMDocument into the original
// one, importing also the child nodes of the updated node.
$updated_node = $dom->importNode($updated_node, TRUE);
- // Finally, replace the original image node with the new image node!
+ // Finally, replace the original node with the new node.
$node->parentNode->replaceChild($updated_node, $node);
}
diff --git a/core/modules/filter/src/Tests/FilterUnitTest.php b/core/modules/filter/src/Tests/FilterUnitTest.php
index 54a738dd027f1781d855fb76fe5fff295a8bebb7..801db4e2fc0bd47dd6459a2a49f934c40f907b22 100644
--- a/core/modules/filter/src/Tests/FilterUnitTest.php
+++ b/core/modules/filter/src/Tests/FilterUnitTest.php
@@ -184,6 +184,13 @@ function testCaptionFilter() {
$this->assertIdentical($expected, $output->getProcessedText());
$this->assertIdentical($attached_library, $output->getAttachments());
+ // Ensure the caption filter works for linked images.
+ $input = '';
+ $expected = '';
+ $output = $test($input);
+ $this->assertIdentical($expected, $output->getProcessedText());
+ $this->assertIdentical($attached_library, $output->getAttachments());
+
// So far we've tested that the caption filter works correctly. But we also
// want to make sure that it works well in tandem with the "Limit allowed
// HTML tags" filter, which it is typically used with.
@@ -301,6 +308,13 @@ function testAlignAndCaptionFilters() {
$output = $test($input);
$this->assertIdentical($expected, $output->getProcessedText());
$this->assertIdentical($attached_library, $output->getAttachments());
+
+ // Ensure both filters together work for linked images.
+ $input = '';
+ $expected = '';
+ $output = $test($input);
+ $this->assertIdentical($expected, $output->getProcessedText());
+ $this->assertIdentical($attached_library, $output->getAttachments());
}
/**