core/modules/ckeditor/ckeditor.module | 31 +++
.../ckeditor/js/plugins/drupalimage/plugin.js | 142 ++++++++------
.../js/plugins/drupalimagecaption/plugin.js | 205 ++++++++++++++++++++
.../js/plugins/drupalimagecaption/theme.js | 202 +++++++++++++++++++
.../ckeditor/js/plugins/drupallink/plugin.js | 8 +-
.../Plugin/CKEditorPlugin/DrupalImageCaption.php | 90 +++++++++
.../lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php | 2 +-
.../lib/Drupal/editor/Form/EditorImageDialog.php | 30 +++
core/modules/system/css/system.theme.css | 6 +
core/themes/bartik/bartik.info.yml | 2 +
core/themes/bartik/css/ckeditor-iframe.css | 32 +++
11 files changed, 690 insertions(+), 60 deletions(-)
diff --git a/core/modules/ckeditor/ckeditor.module b/core/modules/ckeditor/ckeditor.module
index 6a1b633..6c8906b 100644
--- a/core/modules/ckeditor/ckeditor.module
+++ b/core/modules/ckeditor/ckeditor.module
@@ -5,6 +5,8 @@
* Provides integration with the CKEditor WYSIWYG editor.
*/
+use Drupal\editor\Plugin\Core\Entity\Editor;
+
/**
* Implements hook_library_info().
*/
@@ -71,6 +73,16 @@ function ckeditor_library_info() {
array('system', 'drupal.debounce'),
),
);
+ $libraries['drupal.ckeditor.drupalimagecaption-theme'] = array(
+ 'title' => 'Theming support for the imagecaption plugin.',
+ 'version' => VERSION,
+ 'js' => array(
+ $module_path . '/js/plugins/drupalimagecaption/theme.js' => array(),
+ ),
+ 'dependencies' => array(
+ array('ckeditor', 'ckeditor'),
+ ),
+ );
$libraries['ckeditor'] = array(
'title' => 'Loads the main CKEditor library.',
'version' => '4.1',
@@ -97,6 +109,25 @@ function ckeditor_theme() {
}
/**
+ * Implements hook_ckeditor_css_alter().
+ */
+function ckeditor_ckeditor_css_alter(array &$css, Editor $editor) {
+ $filters = array();
+ if (!empty($editor->format)) {
+ $filters = filter_format_load($editor->format)
+ ->filters()
+ ->getAll();
+ }
+
+ // Add the filter caption CSS if the text format associated with this text
+ // editor uses the filter_caption filter. This is used by the included
+ // CKEditor DrupalImageCaption plugin.
+ if (isset($filters['filter_caption']) && $filters['filter_caption']->status) {
+ $css[] = drupal_get_path('module', 'filter') . '/css/filter.caption.css';
+ }
+}
+
+/**
* Retrieves the default theme's CKEditor stylesheets defined in the .info file.
*
* Themes may specify iframe-specific CSS files for use with CKEditor by
diff --git a/core/modules/ckeditor/js/plugins/drupalimage/plugin.js b/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
index 23efdea..05b017a 100644
--- a/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
+++ b/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
@@ -15,60 +15,105 @@ CKEDITOR.plugins.add('drupalimage', {
requiredContent: 'img[alt,src,width,height]',
modes: { wysiwyg : 1 },
canUndo: true,
- exec: function (editor) {
- var imageElement = getSelectedImage(editor);
+ exec: function (editor, override) {
var imageDOMElement = null;
var existingValues = {};
- if (imageElement && imageElement.$) {
- imageDOMElement = imageElement.$;
-
- // Width and height are populated by actual dimensions.
- existingValues.width = imageDOMElement ? imageDOMElement.width : '';
- existingValues.height = imageDOMElement ? imageDOMElement.height : '';
- // Populate all other attributes by their specified attribute values.
- var attribute = null;
- for (var key = 0; key < imageDOMElement.attributes.length; key++) {
- attribute = imageDOMElement.attributes.item(key);
- existingValues[attribute.nodeName.toLowerCase()] = attribute.nodeValue;
- }
- }
+ var saveCallback = null;
+ var dialogTitle;
- function saveCallback (returnValues) {
- // Save snapshot for undo support.
- editor.fire('saveSnapshot');
+ // Allow CKEditor Widget plugins to execute DrupalImage's 'image'
+ // command. In this case, they need to provide the DOM element for the
+ // image (because this plugin wouldn't know where to find it), its
+ // existing values (because they're stored within the Widget in whatever
+ // way it sees fit) and a save callback (again because the Widget may
+ // store the returned values in whatever way it sees fit).
+ if (override) {
+ imageDOMElement = override.imageDOMElement;
+ existingValues = override.existingValues;
+ saveCallback = override.saveCallback;
+ dialogTitle = override.dialogTitle;
+ }
+ // Otherwise, retrieve the selected image and allow it to be edited, or
+ // if no image is selected: insert a new one.
+ else {
+ var selection = editor.getSelection();
+ var imageElement = selection.getSelectedElement();
- // Create a new image element if needed.
- if (!imageElement && returnValues.attributes.src) {
- imageElement = editor.document.createElement('img');
- imageElement.setAttribute('alt', '');
- editor.insertElement(imageElement);
+ // If the 'image' command is being applied to a CKEditor widget, then
+ // edit that Widget instead.
+ if (imageElement && imageElement.type === CKEDITOR.NODE_ELEMENT && imageElement.hasAttribute('data-widget-wrapper')) {
+ editor.widgets.focused.edit();
+ return;
}
- // Delete the image if the src was removed.
- if (imageElement && !returnValues.attributes.src) {
- imageElement.remove();
+ // Otherwise, check if the 'image' command is being applied to an
+ // existing image tag, and then open a dialog to edit it.
+ else if (isImage(imageElement) && imageElement.$) {
+ imageDOMElement = imageElement.$;
+
+ // Width and height are populated by actual dimensions.
+ existingValues.width = imageDOMElement ? imageDOMElement.width : '';
+ existingValues.height = imageDOMElement ? imageDOMElement.height : '';
+ // Populate all other attributes by their specified attribute values.
+ var attribute = null;
+ for (var key = 0; key < imageDOMElement.attributes.length; key++) {
+ attribute = imageDOMElement.attributes.item(key);
+ existingValues[attribute.nodeName.toLowerCase()] = attribute.nodeValue;
+ }
+
+ dialogTitle = editor.config.drupalImage_dialogTitleEdit;
}
- // Update the image properties.
+ // The 'image' command is being executed to add a new image.
else {
- for (var key in returnValues.attributes) {
- if (returnValues.attributes.hasOwnProperty(key)) {
- // Update the property if a value is specified.
- if (returnValues.attributes[key].length > 0) {
- imageElement.setAttribute(key, returnValues.attributes[key]);
- }
- // Delete the property if set to an empty string.
- else {
- imageElement.removeAttribute(key);
+ dialogTitle = editor.config.drupalImage_dialogTitleAdd;
+ // Allow other plugins to override the image insertion: they must
+ // listen to this event and cancel the event to do so.
+ if (!editor.fire('drupalimageinsert')) {
+ return;
+ }
+ }
+
+ saveCallback = function (returnValues) {
+ // Save snapshot for undo support.
+ editor.fire('saveSnapshot');
+
+ // Create a new image element if needed.
+ if (!imageElement && returnValues.attributes.src) {
+ imageElement = editor.document.createElement('img');
+ imageElement.setAttribute('alt', '');
+ editor.insertElement(imageElement);
+
+ //
+ // @todo The above inserts the new image, but it does not get
+ // picked up by widgets!
+ //
+ }
+ // Delete the image if the src was removed.
+ if (imageElement && !returnValues.attributes.src) {
+ imageElement.remove();
+ }
+ // Update the image properties.
+ else {
+ for (var key in returnValues.attributes) {
+ if (returnValues.attributes.hasOwnProperty(key)) {
+ // Update the property if a value is specified.
+ if (returnValues.attributes[key].length > 0) {
+ imageElement.setAttribute(key, returnValues.attributes[key]);
+ }
+ // Delete the property if set to an empty string.
+ else {
+ imageElement.removeAttribute(key);
+ }
}
}
}
- }
+ };
}
// Drupal.t() will not work inside CKEditor plugins because CKEditor
// loads the JavaScript file instead of Drupal. Pull translated strings
// from the plugin settings that are translated server-side.
var dialogSettings = {
- title: imageDOMElement ? editor.config.drupalImage_dialogTitleEdit : editor.config.drupalImage_dialogTitleAdd,
+ title: dialogTitle,
dialogClass: 'editor-image-dialog'
};
@@ -98,7 +143,7 @@ CKEDITOR.plugins.add('drupalimage', {
if (editor.addMenuItems) {
editor.addMenuItems({
image: {
- label: editor.lang.image.menu,
+ label: Drupal.t('Image'),
command : 'image',
group: 'image'
}
@@ -108,7 +153,7 @@ CKEDITOR.plugins.add('drupalimage', {
// If the "contextmenu" plugin is loaded, register the listeners.
if (editor.contextMenu) {
editor.contextMenu.addListener(function (element, selection) {
- if (getSelectedImage(editor, element)) {
+ if (isImage(element)) {
return { image: CKEDITOR.TRISTATE_OFF };
}
});
@@ -116,21 +161,8 @@ CKEDITOR.plugins.add('drupalimage', {
}
});
-/**
- * Finds an img tag anywhere in the current editor selection.
- */
-function getSelectedImage (editor, element) {
- if (!element) {
- var sel = editor.getSelection();
- var selectedText = sel.getSelectedText().replace(/^\s\s*/, '').replace(/\s\s*$/, '');
- var isElement = sel.getType() === CKEDITOR.SELECTION_ELEMENT;
- var isEmptySelection = sel.getType() === CKEDITOR.SELECTION_TEXT && selectedText.length === 0;
- element = (isElement || isEmptySelection) && sel.getSelectedElement();
- }
-
- if (element && element.is('img') && !element.data('cke-realelement') && !element.isReadOnly()) {
- return element;
- }
+function isImage (element) {
+ return element && element.is('img') && !element.data('cke-realelement') && !element.isReadOnly();
}
})(jQuery, Drupal, drupalSettings, CKEDITOR);
diff --git a/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js b/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js
new file mode 100644
index 0000000..751044a
--- /dev/null
+++ b/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js
@@ -0,0 +1,205 @@
+(function (CKEDITOR) {
+
+"use strict";
+
+CKEDITOR.plugins.add('drupalimagecaption', {
+ requires: 'widget',
+ init: function (editor) {
+
+ /**
+ * Override drupalimage plugin's image insertion mechanism with our own, to
+ * ensure a widget is inserted, rather than a simple image (Widget's auto-
+ * discovery only runs upon init).
+ */
+ editor.on('drupalimageinsert', function (event) {
+ editor.execCommand('widgetImagecaption');
+ event.cancel();
+ });
+
+ // Register the widget with a unique name "imagecaption".
+ editor.widgets.add('imagecaption', {
+ allowedContent: 'img[!src,alt,width,height,!data-caption,!data-align]',
+ template: '',
+ parts: {
+ image: 'img'
+ },
+
+ // Initialization method called for every widget instance being
+ // upcasted.
+ init: function () {
+ var image = this.parts.image;
+
+ // Save the initial widget data.
+ this.setData({
+ src: image.getAttribute('src'),
+ width: image.getAttribute('width') || '',
+ height: image.getAttribute('height') || '',
+ alt: image.getAttribute('alt') || '',
+ data_caption: image.getAttribute('data-caption'),
+ data_align: image.getAttribute('data-align'),
+ hasCaption: image.hasAttribute('data-caption')
+ });
+
+ // Remove the original element attributes.
+ image.removeAttributes(['data-caption', 'data-align']);
+ image.removeStyle('float');
+ },
+
+ // Called after initialization and on "data" changes.
+ data: function () {
+ this.parts.image.setAttribute('src', this.data.src);
+ this.parts.image.setAttribute('alt', this.data.alt);
+ this.parts.image.setAttribute('width', this.data.width);
+ this.parts.image.setAttribute('height', this.data.height);
+
+ // Float the wrapper too.
+ if (this.data.data_align === null) {
+ this.wrapper.removeStyle('float');
+ this.wrapper.removeStyle('text-align');
+ }
+ else if (this.data.data_align === 'center') {
+ this.wrapper.setStyle('float', 'none');
+ this.wrapper.setStyle('text-align', 'center');
+ }
+ else {
+ this.wrapper.setStyle('float', this.data.data_align);
+ this.wrapper.removeStyle('text-align');
+ }
+ },
+
+ // Check the elements that need to be converted to widgets.
+ upcast: function (el) {
+ // Upcast all elements that are alone inside a block element.
+ if (el.name === 'img') {
+ if (CKEDITOR.dtd.$block[el.parent.name] && el.parent.children.length === 1) {
+ return true;
+ }
+ }
+ },
+
+ // Convert the element back to its desired output representation.
+ downcast: function (el) {
+ if (this.data.hasCaption) {
+ el.attributes['data-caption'] = this.data.data_caption;
+ }
+
+ if (this.data.data_align) {
+ el.attributes['data-align'] = this.data.data_align;
+ }
+ },
+
+ _insertSaveCallback: function (returnValues) {
+ // We can't create an image with an empty "src" attribute.
+ if (returnValues.attributes.src.length === 0) {
+ return;
+ }
+
+ // Build the HTML for the widget.
+ var html = '';
+ var el = new CKEDITOR.dom.element.createFromHtml(html, editor.document);
+ editor.insertElement(editor.widgets.wrapElement(el, 'imagecaption'));
+
+ // Save snapshot for undo support.
+ editor.fire('saveSnapshot');
+
+ // Initialize and focus the widget.
+ var widget = editor.widgets.initOn(el, 'imagecaption');
+ widget.focus();
+ },
+
+ insert: function () {
+ var override = {
+ imageDOMElement: null,
+ existingValues: { hasCaption: false, data_align: '' },
+ saveCallback: this._insertSaveCallback,
+ dialogTitle: editor.config.drupalImage_dialogTitleAdd
+ };
+ editor.execCommand('image', override);
+ },
+
+ edit: function () {
+ var that = this;
+ var saveCallback = function (returnValues) {
+ // Save snapshot for undo support.
+ editor.fire('saveSnapshot');
+ // Set the updated widget data.
+ that.setData({
+ src: returnValues.attributes.src,
+ width: returnValues.attributes.width,
+ height: returnValues.attributes.height,
+ alt: returnValues.attributes.alt,
+ hasCaption: !!returnValues.hasCaption,
+ data_caption: returnValues.hasCaption ? that.data.data_caption : '',
+ data_align: returnValues.attributes.data_align === 'none' ? null : returnValues.attributes.data_align
+ });
+ };
+ var override = {
+ imageDOMElement: this.parts.image.$,
+ existingValues: this.data,
+ saveCallback: saveCallback,
+ dialogTitle: this.data.src === '' ? editor.config.drupalImage_dialogTitleAdd : editor.config.drupalImage_dialogTitleEdit,
+ };
+ editor.execCommand('image', override);
+ }
+ });
+ },
+
+ afterInit: function (editor) {
+ function setupAlignCommand (value) {
+ var command = editor.getCommand('justify' + value);
+ if (command) {
+ if (value in { right:1,left:1,center:1 }) {
+ command.on('exec', function (event) {
+ var widget = getSelectedWidget(editor);
+ if (widget && widget.name === 'imagecaption') {
+ widget.setData({ data_align: value });
+ event.cancel();
+ }
+ });
+ }
+
+ command.on('refresh', function (event) {
+ var widget = getSelectedWidget(editor),
+ allowed = { left:1,center:1,right:1 },
+ align;
+
+ if (widget) {
+ align = widget.data.data_align;
+
+ this.setState(
+ (align === value) ? CKEDITOR.TRISTATE_ON : (value in allowed) ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED);
+
+ event.cancel();
+ }
+ });
+ }
+ }
+
+ function getSelectedWidget(editor) {
+ var widget = editor.widgets.focused;
+ if (widget && widget.name === 'imagecaption') {
+ return widget;
+ }
+ return null;
+ }
+
+ // Customize the behavior of the alignment commands.
+ setupAlignCommand('left');
+ setupAlignCommand('right');
+ setupAlignCommand('center');
+ }
+});
+
+})(CKEDITOR);
diff --git a/core/modules/ckeditor/js/plugins/drupalimagecaption/theme.js b/core/modules/ckeditor/js/plugins/drupalimagecaption/theme.js
new file mode 100644
index 0000000..583b346
--- /dev/null
+++ b/core/modules/ckeditor/js/plugins/drupalimagecaption/theme.js
@@ -0,0 +1,202 @@
+(function (CKEDITOR) {
+
+"use strict";
+
+CKEDITOR.on('instanceCreated', function (event) {
+ var editor = event.editor;
+
+ // Listen to widget definitions and customize them as needed. It's
+ // basically rewriting parts of the definition.
+ editor.on('widgetDefinition', function (event) {
+ var widgetDefinition = event.data;
+
+ // Customize the "imagecaption" widget definition.
+ if (widgetDefinition.name === 'imagecaption') {
+
+ widgetDefinition.template =
+ '';
+
+ // Define the editables created by the overridden upcasting.
+ widgetDefinition.editables = {
+ caption: 'figcaption'
+ };
+
+ // Define the additional parts created by the overridden upcasting.
+ widgetDefinition.parts.caption = 'figcaption';
+
+ // Override "data" so we can make the new widget structure
+ // behave according to changes on data.
+ widgetDefinition.data = CKEDITOR.tools.override(widgetDefinition.data, function (originalDataFn) {
+ return function () {
+ // Call the original "data" implementation.
+ originalDataFn.apply(this, arguments);
+
+ // The image is wrapped in