5 * This alters the existing CKEditor image2 widget plugin to:
6 * - require a data-entity-type and a data-entity-uuid attribute (which Drupal
7 * uses to track where images are being used)
8 * - use a Drupal-native dialog (that is in fact just an alterable Drupal form
9 * like any other) instead of CKEditor's own dialogs.
11 * @see \Drupal\editor\Form\EditorImageDialog
16 (function ($, Drupal, CKEDITOR) {
20 CKEDITOR.plugins.add('drupalimage', {
25 beforeInit: function (editor) {
26 // Override the image2 widget definition to require and handle the
27 // additional data-entity-type and data-entity-uuid attributes.
28 editor.on('widgetDefinition', function (event) {
29 var widgetDefinition = event.data;
30 if (widgetDefinition.name !== 'image') {
34 // First, convert requiredContent & allowedContent from the string
35 // format that image2 uses for both to formats that are better suited
36 // for extending, so that both this basic drupalimage plugin and Drupal
37 // modules can easily extend it.
38 // @see http://docs.ckeditor.com/#!/api/CKEDITOR.filter.allowedContentRules
39 // Mapped from image2's allowedContent. Unlike image2, we don't allow
40 // <figure>, <figcaption>, <div> or <p> in our downcast, so we omit
41 // those. For the <img> tag, we list all attributes it lists, but omit
42 // the classes, because the listed classes are for alignment, and for
43 // alignment we use the data-align attribute.
44 widgetDefinition.allowedContent = {
55 // Mapped from image2's requiredContent: "img[src,alt]". This does not
56 // use the object format unlike above, but a CKEDITOR.style instance,
57 // because requiredContent does not support the object format.
58 // @see https://www.drupal.org/node/2585173#comment-10456981
59 widgetDefinition.requiredContent = new CKEDITOR.style({
67 // Extend requiredContent & allowedContent.
68 // CKEDITOR.style is an immutable object: we cannot modify its
69 // definition to extend requiredContent. Hence we get the definition,
70 // modify it, and pass it to a new CKEDITOR.style instance.
71 var requiredContent = widgetDefinition.requiredContent.getDefinition();
72 requiredContent.attributes['data-entity-type'] = '';
73 requiredContent.attributes['data-entity-uuid'] = '';
74 widgetDefinition.requiredContent = new CKEDITOR.style(requiredContent);
75 widgetDefinition.allowedContent.img.attributes['!data-entity-type'] = true;
76 widgetDefinition.allowedContent.img.attributes['!data-entity-uuid'] = true;
78 // Override downcast(): since we only accept <img> in our upcast method,
79 // the element is already correct. We only need to update the element's
80 // data-entity-uuid attribute.
81 widgetDefinition.downcast = function (element) {
82 element.attributes['data-entity-type'] = this.data['data-entity-type'];
83 element.attributes['data-entity-uuid'] = this.data['data-entity-uuid'];
86 // We want to upcast <img> elements to a DOM structure required by the
87 // image2 widget; we only accept an <img> tag, and that <img> tag MAY
88 // have a data-entity-type and a data-entity-uuid attribute.
89 widgetDefinition.upcast = function (element, data) {
90 if (element.name !== 'img') {
93 // Don't initialize on pasted fake objects.
94 else if (element.attributes['data-cke-realelement']) {
98 // Parse the data-entity-type attribute.
99 data['data-entity-type'] = element.attributes['data-entity-type'];
100 // Parse the data-entity-uuid attribute.
101 data['data-entity-uuid'] = element.attributes['data-entity-uuid'];
106 // Overrides default implementation. Used to populate the "classes"
107 // property of the widget's "data" property, which is used for the
108 // "widget styles" functionality
109 // (http://docs.ckeditor.com/#!/guide/dev_styles-section-widget-styles).
110 // Is applied to whatever the main element of the widget is (<figure> or
111 // <img>). The classes in image2_captionedClass are always added due to
112 // a bug in CKEditor. In the case of drupalimage, we don't ever want to
113 // add that class, because the widget template already contains it.
114 // @see http://dev.ckeditor.com/ticket/13888
115 // @see https://www.drupal.org/node/2268941
116 var originalGetClasses = widgetDefinition.getClasses;
117 widgetDefinition.getClasses = function () {
118 var classes = originalGetClasses.call(this);
119 var captionedClasses = (this.editor.config.image2_captionedClass || '').split(/\s+/);
121 if (captionedClasses.length && classes) {
122 for (var i = 0; i < captionedClasses.length; i++) {
123 if (captionedClasses[i] in classes) {
124 delete classes[captionedClasses[i]];
132 // Protected; keys of the widget data to be sent to the Drupal dialog.
133 // Keys in the hash are the keys for image2's data, values are the keys
134 // that the Drupal dialog uses.
135 widgetDefinition._mapDataToDialog = {
140 'data-entity-type': 'data-entity-type',
141 'data-entity-uuid': 'data-entity-uuid'
144 // Protected; transforms widget's data object to the format used by the
145 // \Drupal\editor\Form\EditorImageDialog dialog, keeping only the data
146 // listed in widgetDefinition._dataForDialog.
147 widgetDefinition._dataToDialogValues = function (data) {
148 var dialogValues = {};
149 var map = widgetDefinition._mapDataToDialog;
150 Object.keys(widgetDefinition._mapDataToDialog).forEach(function (key) {
151 dialogValues[map[key]] = data[key];
156 // Protected; the inverse of _dataToDialogValues.
157 widgetDefinition._dialogValuesToData = function (dialogReturnValues) {
159 var map = widgetDefinition._mapDataToDialog;
160 Object.keys(widgetDefinition._mapDataToDialog).forEach(function (key) {
161 if (dialogReturnValues.hasOwnProperty(map[key])) {
162 data[key] = dialogReturnValues[map[key]];
168 // Protected; creates Drupal dialog save callback.
169 widgetDefinition._createDialogSaveCallback = function (editor, widget) {
170 return function (dialogReturnValues) {
171 var firstEdit = !widget.ready;
173 // Dialog may have blurred the widget. Re-focus it first.
178 editor.fire('saveSnapshot');
180 // Pass `true` so DocumentFragment will also be returned.
181 var container = widget.wrapper.getParent(true);
182 var image = widget.parts.image;
184 // Set the updated widget data, after the necessary conversions from
185 // the dialog's return values.
186 // Note: on widget#setData this widget instance might be destroyed.
187 var data = widgetDefinition._dialogValuesToData(dialogReturnValues.attributes);
188 widget.setData(data);
190 // Retrieve the widget once again. It could've been destroyed
191 // when shifting state, so might deal with a new instance.
192 widget = editor.widgets.getByElement(image);
194 // It's first edit, just after widget instance creation, but before
195 // it was inserted into DOM. So we need to retrieve the widget
196 // wrapper from inside the DocumentFragment which we cached above
197 // and finalize other things (like ready event and flag).
199 editor.widgets.finalizeCreation(container);
202 setTimeout(function () {
203 // (Re-)focus the widget.
205 // Save snapshot for undo support.
206 editor.fire('saveSnapshot');
213 var originalInit = widgetDefinition.init;
214 widgetDefinition.init = function () {
215 originalInit.call(this);
217 // Update data.link object with attributes if the link has been
219 // @see plugins/image2/plugin.js/init() in CKEditor; this is similar.
220 if (this.parts.link) {
221 this.setData('link', CKEDITOR.plugins.image2.getLinkAttributesParser()(editor, this.parts.link));
226 // Add a widget#edit listener to every instance of image2 widget in order
227 // to handle its editing with a Drupal-native dialog.
228 // This includes also a case just after the image was created
229 // and dialog should be opened for it for the first time.
230 editor.widgets.on('instanceCreated', function (event) {
231 var widget = event.data;
233 if (widget.name !== 'image') {
237 widget.on('edit', function (event) {
238 // Cancel edit event to break image2's dialog binding
239 // (and also to prevent automatic insertion before opening dialog).
242 // Open drupalimage dialog.
243 editor.execCommand('editdrupalimage', {
244 existingValues: widget.definition._dataToDialogValues(widget.data),
245 saveCallback: widget.definition._createDialogSaveCallback(editor, widget),
246 // Drupal.t() will not work inside CKEditor plugins because CKEditor
247 // loads the JavaScript file instead of Drupal. Pull translated
248 // strings from the plugin settings that are translated server-side.
249 dialogTitle: widget.data.src ? editor.config.drupalImage_dialogTitleEdit : editor.config.drupalImage_dialogTitleAdd
254 // Register the "editdrupalimage" command, which essentially just replaces
255 // the "image" command's CKEditor dialog with a Drupal-native dialog.
256 editor.addCommand('editdrupalimage', {
257 allowedContent: 'img[alt,!src,width,height,!data-entity-type,!data-entity-uuid]',
258 requiredContent: 'img[alt,src,data-entity-type,data-entity-uuid]',
261 exec: function (editor, data) {
262 var dialogSettings = {
263 title: data.dialogTitle,
264 dialogClass: 'editor-image-dialog'
266 Drupal.ckeditor.openDialog(editor, Drupal.url('editor/dialog/image/' + editor.config.drupal.format), data.existingValues, data.saveCallback, dialogSettings);
270 // Register the toolbar button.
271 if (editor.ui.addButton) {
272 editor.ui.addButton('DrupalImage', {
273 label: Drupal.t('Image'),
274 // Note that we use the original image2 command!
280 afterInit: function (editor) {
281 linkCommandIntegrator(editor);
286 // Override image2's integration with the official CKEditor link plugin:
287 // integrate with the drupallink plugin instead.
288 CKEDITOR.plugins.image2.getLinkAttributesParser = function () {
289 return CKEDITOR.plugins.drupallink.parseLinkAttributes;
291 CKEDITOR.plugins.image2.getLinkAttributesGetter = function () {
292 return CKEDITOR.plugins.drupallink.getLinkAttributes;
296 * Integrates the drupalimage widget with the drupallink plugin.
298 * Makes images linkable.
300 * @param {CKEDITOR.editor} editor
301 * A CKEditor instance.
303 function linkCommandIntegrator(editor) {
304 // Nothing to integrate with if the drupallink plugin is not loaded.
305 if (!editor.plugins.drupallink) {
309 // Override default behaviour of 'drupalunlink' command.
310 editor.getCommand('drupalunlink').on('exec', function (evt) {
311 var widget = getFocusedWidget(editor);
313 // Override 'drupalunlink' only when link truly belongs to the widget. If
314 // wrapped inline widget in a link, let default unlink work.
315 // @see https://dev.ckeditor.com/ticket/11814
316 if (!widget || !widget.parts.link) {
320 widget.setData('link', null);
322 // Selection (which is fake) may not change if unlinked image in focused
323 // widget, i.e. if captioned image. Let's refresh command state manually
325 this.refresh(editor, editor.elementPath());
330 // Override default refresh of 'drupalunlink' command.
331 editor.getCommand('drupalunlink').on('refresh', function (evt) {
332 var widget = getFocusedWidget(editor);
338 // Note that widget may be wrapped in a link, which
339 // does not belong to that widget (#11814).
340 this.setState(widget.data.link || widget.wrapper.getAscendant('a') ?
341 CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED);
348 * Gets the focused widget, if of the type specific for this plugin.
350 * @param {CKEDITOR.editor} editor
351 * A CKEditor instance.
353 * @return {?CKEDITOR.plugins.widget}
354 * The focused image2 widget instance, or null.
356 function getFocusedWidget(editor) {
357 var widget = editor.widgets.focused;
359 if (widget && widget.name === 'image') {
366 // Expose an API for other plugins to interact with drupalimage widgets.
367 CKEDITOR.plugins.drupalimage = {
368 getFocusedWidget: getFocusedWidget
371 })(jQuery, Drupal, CKEDITOR);