0025664e09f7378d1108e9022ed7dc60e531f94e
[yaffs-website] / web / core / modules / ckeditor / js / plugins / drupalimage / plugin.js
1 /**
2  * @file
3  * Drupal Image plugin.
4  *
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.
10  *
11  * @see \Drupal\editor\Form\EditorImageDialog
12  *
13  * @ignore
14  */
15
16 (function ($, Drupal, CKEDITOR) {
17
18   'use strict';
19
20   CKEDITOR.plugins.add('drupalimage', {
21     requires: 'image2',
22     icons: 'drupalimage',
23     hidpi: true,
24
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') {
31           return;
32         }
33
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 = {
45           img: {
46             attributes: {
47               '!src': true,
48               '!alt': true,
49               'width': true,
50               'height': true
51             },
52             classes: {}
53           }
54         };
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({
60           element: 'img',
61           attributes: {
62             src: '',
63             alt: ''
64           }
65         });
66
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;
77
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'];
84         };
85
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') {
91             return;
92           }
93           // Don't initialize on pasted fake objects.
94           else if (element.attributes['data-cke-realelement']) {
95             return;
96           }
97
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'];
102
103           return element;
104         };
105
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+/);
120
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]];
125               }
126             }
127           }
128
129           return classes;
130         };
131
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 = {
136           'src': 'src',
137           'alt': 'alt',
138           'width': 'width',
139           'height': 'height',
140           'data-entity-type': 'data-entity-type',
141           'data-entity-uuid': 'data-entity-uuid'
142         };
143
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];
152           });
153           return dialogValues;
154         };
155
156         // Protected; the inverse of _dataToDialogValues.
157         widgetDefinition._dialogValuesToData = function (dialogReturnValues) {
158           var data = {};
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]];
163             }
164           });
165           return data;
166         };
167
168         // Protected; creates Drupal dialog save callback.
169         widgetDefinition._createDialogSaveCallback = function (editor, widget) {
170           return function (dialogReturnValues) {
171             var firstEdit = !widget.ready;
172
173             // Dialog may have blurred the widget. Re-focus it first.
174             if (!firstEdit) {
175               widget.focus();
176             }
177
178             editor.fire('saveSnapshot');
179
180             // Pass `true` so DocumentFragment will also be returned.
181             var container = widget.wrapper.getParent(true);
182             var image = widget.parts.image;
183
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);
189
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);
193
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).
198             if (firstEdit) {
199               editor.widgets.finalizeCreation(container);
200             }
201
202             setTimeout(function () {
203               // (Re-)focus the widget.
204               widget.focus();
205               // Save snapshot for undo support.
206               editor.fire('saveSnapshot');
207             });
208
209             return widget;
210           };
211         };
212
213         var originalInit = widgetDefinition.init;
214         widgetDefinition.init = function () {
215           originalInit.call(this);
216
217           // Update data.link object with attributes if the link has been
218           // discovered.
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));
222           }
223         };
224       });
225
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;
232
233         if (widget.name !== 'image') {
234           return;
235         }
236
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).
240           event.cancel();
241
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
250           });
251         });
252       });
253
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]',
259         modes: {wysiwyg: 1},
260         canUndo: true,
261         exec: function (editor, data) {
262           var dialogSettings = {
263             title: data.dialogTitle,
264             dialogClass: 'editor-image-dialog'
265           };
266           Drupal.ckeditor.openDialog(editor, Drupal.url('editor/dialog/image/' + editor.config.drupal.format), data.existingValues, data.saveCallback, dialogSettings);
267         }
268       });
269
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!
275           command: 'image'
276         });
277       }
278     },
279
280     afterInit: function (editor) {
281       linkCommandIntegrator(editor);
282     }
283
284   });
285
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;
290   };
291   CKEDITOR.plugins.image2.getLinkAttributesGetter = function () {
292     return CKEDITOR.plugins.drupallink.getLinkAttributes;
293   };
294
295   /**
296    * Integrates the drupalimage widget with the drupallink plugin.
297    *
298    * Makes images linkable.
299    *
300    * @param {CKEDITOR.editor} editor
301    *   A CKEditor instance.
302    */
303   function linkCommandIntegrator(editor) {
304     // Nothing to integrate with if the drupallink plugin is not loaded.
305     if (!editor.plugins.drupallink) {
306       return;
307     }
308
309     // Override default behaviour of 'drupalunlink' command.
310     editor.getCommand('drupalunlink').on('exec', function (evt) {
311       var widget = getFocusedWidget(editor);
312
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) {
317         return;
318       }
319
320       widget.setData('link', null);
321
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
324       // here.
325       this.refresh(editor, editor.elementPath());
326
327       evt.cancel();
328     });
329
330     // Override default refresh of 'drupalunlink' command.
331     editor.getCommand('drupalunlink').on('refresh', function (evt) {
332       var widget = getFocusedWidget(editor);
333
334       if (!widget) {
335         return;
336       }
337
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);
342
343       evt.cancel();
344     });
345   }
346
347   /**
348    * Gets the focused widget, if of the type specific for this plugin.
349    *
350    * @param {CKEDITOR.editor} editor
351    *   A CKEditor instance.
352    *
353    * @return {?CKEDITOR.plugins.widget}
354    *   The focused image2 widget instance, or null.
355    */
356   function getFocusedWidget(editor) {
357     var widget = editor.widgets.focused;
358
359     if (widget && widget.name === 'image') {
360       return widget;
361     }
362
363     return null;
364   }
365
366   // Expose an API for other plugins to interact with drupalimage widgets.
367   CKEDITOR.plugins.drupalimage = {
368     getFocusedWidget: getFocusedWidget
369   };
370
371 })(jQuery, Drupal, CKEDITOR);