Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / ckeditor / js / plugins / drupalimage / plugin.es6.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   CKEDITOR.plugins.add('drupalimage', {
18     requires: 'image2',
19     icons: 'drupalimage',
20     hidpi: true,
21
22     beforeInit(editor) {
23       // Override the image2 widget definition to require and handle the
24       // additional data-entity-type and data-entity-uuid attributes.
25       editor.on('widgetDefinition', (event) => {
26         const widgetDefinition = event.data;
27         if (widgetDefinition.name !== 'image') {
28           return;
29         }
30
31         // First, convert requiredContent & allowedContent from the string
32         // format that image2 uses for both to formats that are better suited
33         // for extending, so that both this basic drupalimage plugin and Drupal
34         // modules can easily extend it.
35         // @see http://docs.ckeditor.com/#!/api/CKEDITOR.filter.allowedContentRules
36         // Mapped from image2's allowedContent. Unlike image2, we don't allow
37         // <figure>, <figcaption>, <div> or <p>  in our downcast, so we omit
38         // those. For the <img> tag, we list all attributes it lists, but omit
39         // the classes, because the listed classes are for alignment, and for
40         // alignment we use the data-align attribute.
41         widgetDefinition.allowedContent = {
42           img: {
43             attributes: {
44               '!src': true,
45               '!alt': true,
46               width: true,
47               height: true,
48             },
49             classes: {},
50           },
51         };
52         // Mapped from image2's requiredContent: "img[src,alt]". This does not
53         // use the object format unlike above, but a CKEDITOR.style instance,
54         // because requiredContent does not support the object format.
55         // @see https://www.drupal.org/node/2585173#comment-10456981
56         widgetDefinition.requiredContent = new CKEDITOR.style({
57           element: 'img',
58           attributes: {
59             src: '',
60             alt: '',
61           },
62         });
63
64         // Extend requiredContent & allowedContent.
65         // CKEDITOR.style is an immutable object: we cannot modify its
66         // definition to extend requiredContent. Hence we get the definition,
67         // modify it, and pass it to a new CKEDITOR.style instance.
68         const requiredContent = widgetDefinition.requiredContent.getDefinition();
69         requiredContent.attributes['data-entity-type'] = '';
70         requiredContent.attributes['data-entity-uuid'] = '';
71         widgetDefinition.requiredContent = new CKEDITOR.style(requiredContent);
72         widgetDefinition.allowedContent.img.attributes['!data-entity-type'] = true;
73         widgetDefinition.allowedContent.img.attributes['!data-entity-uuid'] = true;
74
75         // Override downcast(): since we only accept <img> in our upcast method,
76         // the element is already correct. We only need to update the element's
77         // data-entity-uuid attribute.
78         widgetDefinition.downcast = function (element) {
79           element.attributes['data-entity-type'] = this.data['data-entity-type'];
80           element.attributes['data-entity-uuid'] = this.data['data-entity-uuid'];
81         };
82
83         // We want to upcast <img> elements to a DOM structure required by the
84         // image2 widget; we only accept an <img> tag, and that <img> tag MAY
85         // have a data-entity-type and a data-entity-uuid attribute.
86         widgetDefinition.upcast = function (element, data) {
87           if (element.name !== 'img') {
88             return;
89           }
90           // Don't initialize on pasted fake objects.
91           else if (element.attributes['data-cke-realelement']) {
92             return;
93           }
94
95           // Parse the data-entity-type attribute.
96           data['data-entity-type'] = element.attributes['data-entity-type'];
97           // Parse the data-entity-uuid attribute.
98           data['data-entity-uuid'] = element.attributes['data-entity-uuid'];
99
100           return element;
101         };
102
103         // Overrides default implementation. Used to populate the "classes"
104         // property of the widget's "data" property, which is used for the
105         // "widget styles" functionality
106         // (http://docs.ckeditor.com/#!/guide/dev_styles-section-widget-styles).
107         // Is applied to whatever the main element of the widget is (<figure> or
108         // <img>). The classes in image2_captionedClass are always added due to
109         // a bug in CKEditor. In the case of drupalimage, we don't ever want to
110         // add that class, because the widget template already contains it.
111         // @see http://dev.ckeditor.com/ticket/13888
112         // @see https://www.drupal.org/node/2268941
113         const originalGetClasses = widgetDefinition.getClasses;
114         widgetDefinition.getClasses = function () {
115           const classes = originalGetClasses.call(this);
116           const captionedClasses = (this.editor.config.image2_captionedClass || '').split(/\s+/);
117
118           if (captionedClasses.length && classes) {
119             for (let i = 0; i < captionedClasses.length; i++) {
120               if (captionedClasses[i] in classes) {
121                 delete classes[captionedClasses[i]];
122               }
123             }
124           }
125
126           return classes;
127         };
128
129         // Protected; keys of the widget data to be sent to the Drupal dialog.
130         // Keys in the hash are the keys for image2's data, values are the keys
131         // that the Drupal dialog uses.
132         widgetDefinition._mapDataToDialog = {
133           src: 'src',
134           alt: 'alt',
135           width: 'width',
136           height: 'height',
137           'data-entity-type': 'data-entity-type',
138           'data-entity-uuid': 'data-entity-uuid',
139         };
140
141         // Protected; transforms widget's data object to the format used by the
142         // \Drupal\editor\Form\EditorImageDialog dialog, keeping only the data
143         // listed in widgetDefinition._dataForDialog.
144         widgetDefinition._dataToDialogValues = function (data) {
145           const dialogValues = {};
146           const map = widgetDefinition._mapDataToDialog;
147           Object.keys(widgetDefinition._mapDataToDialog).forEach((key) => {
148             dialogValues[map[key]] = data[key];
149           });
150           return dialogValues;
151         };
152
153         // Protected; the inverse of _dataToDialogValues.
154         widgetDefinition._dialogValuesToData = function (dialogReturnValues) {
155           const data = {};
156           const map = widgetDefinition._mapDataToDialog;
157           Object.keys(widgetDefinition._mapDataToDialog).forEach((key) => {
158             if (dialogReturnValues.hasOwnProperty(map[key])) {
159               data[key] = dialogReturnValues[map[key]];
160             }
161           });
162           return data;
163         };
164
165         // Protected; creates Drupal dialog save callback.
166         widgetDefinition._createDialogSaveCallback = function (editor, widget) {
167           return function (dialogReturnValues) {
168             const firstEdit = !widget.ready;
169
170             // Dialog may have blurred the widget. Re-focus it first.
171             if (!firstEdit) {
172               widget.focus();
173             }
174
175             editor.fire('saveSnapshot');
176
177             // Pass `true` so DocumentFragment will also be returned.
178             const container = widget.wrapper.getParent(true);
179             const image = widget.parts.image;
180
181             // Set the updated widget data, after the necessary conversions from
182             // the dialog's return values.
183             // Note: on widget#setData this widget instance might be destroyed.
184             const data = widgetDefinition._dialogValuesToData(dialogReturnValues.attributes);
185             widget.setData(data);
186
187             // Retrieve the widget once again. It could've been destroyed
188             // when shifting state, so might deal with a new instance.
189             widget = editor.widgets.getByElement(image);
190
191             // It's first edit, just after widget instance creation, but before
192             // it was inserted into DOM. So we need to retrieve the widget
193             // wrapper from inside the DocumentFragment which we cached above
194             // and finalize other things (like ready event and flag).
195             if (firstEdit) {
196               editor.widgets.finalizeCreation(container);
197             }
198
199             setTimeout(() => {
200               // (Re-)focus the widget.
201               widget.focus();
202               // Save snapshot for undo support.
203               editor.fire('saveSnapshot');
204             });
205
206             return widget;
207           };
208         };
209
210         const originalInit = widgetDefinition.init;
211         widgetDefinition.init = function () {
212           originalInit.call(this);
213
214           // Update data.link object with attributes if the link has been
215           // discovered.
216           // @see plugins/image2/plugin.js/init() in CKEditor; this is similar.
217           if (this.parts.link) {
218             this.setData('link', CKEDITOR.plugins.image2.getLinkAttributesParser()(editor, this.parts.link));
219           }
220         };
221       });
222
223       // Add a widget#edit listener to every instance of image2 widget in order
224       // to handle its editing with a Drupal-native dialog.
225       // This includes also a case just after the image was created
226       // and dialog should be opened for it for the first time.
227       editor.widgets.on('instanceCreated', (event) => {
228         const widget = event.data;
229
230         if (widget.name !== 'image') {
231           return;
232         }
233
234         widget.on('edit', (event) => {
235           // Cancel edit event to break image2's dialog binding
236           // (and also to prevent automatic insertion before opening dialog).
237           event.cancel();
238
239           // Open drupalimage dialog.
240           editor.execCommand('editdrupalimage', {
241             existingValues: widget.definition._dataToDialogValues(widget.data),
242             saveCallback: widget.definition._createDialogSaveCallback(editor, widget),
243             // Drupal.t() will not work inside CKEditor plugins because CKEditor
244             // loads the JavaScript file instead of Drupal. Pull translated
245             // strings from the plugin settings that are translated server-side.
246             dialogTitle: widget.data.src ? editor.config.drupalImage_dialogTitleEdit : editor.config.drupalImage_dialogTitleAdd,
247           });
248         });
249       });
250
251       // Register the "editdrupalimage" command, which essentially just replaces
252       // the "image" command's CKEditor dialog with a Drupal-native dialog.
253       editor.addCommand('editdrupalimage', {
254         allowedContent: 'img[alt,!src,width,height,!data-entity-type,!data-entity-uuid]',
255         requiredContent: 'img[alt,src,data-entity-type,data-entity-uuid]',
256         modes: { wysiwyg: 1 },
257         canUndo: true,
258         exec(editor, data) {
259           const dialogSettings = {
260             title: data.dialogTitle,
261             dialogClass: 'editor-image-dialog',
262           };
263           Drupal.ckeditor.openDialog(editor, Drupal.url(`editor/dialog/image/${editor.config.drupal.format}`), data.existingValues, data.saveCallback, dialogSettings);
264         },
265       });
266
267       // Register the toolbar button.
268       if (editor.ui.addButton) {
269         editor.ui.addButton('DrupalImage', {
270           label: Drupal.t('Image'),
271           // Note that we use the original image2 command!
272           command: 'image',
273         });
274       }
275     },
276
277     afterInit(editor) {
278       linkCommandIntegrator(editor);
279     },
280
281   });
282
283   // Override image2's integration with the official CKEditor link plugin:
284   // integrate with the drupallink plugin instead.
285   CKEDITOR.plugins.image2.getLinkAttributesParser = function () {
286     return CKEDITOR.plugins.drupallink.parseLinkAttributes;
287   };
288   CKEDITOR.plugins.image2.getLinkAttributesGetter = function () {
289     return CKEDITOR.plugins.drupallink.getLinkAttributes;
290   };
291
292   /**
293    * Integrates the drupalimage widget with the drupallink plugin.
294    *
295    * Makes images linkable.
296    *
297    * @param {CKEDITOR.editor} editor
298    *   A CKEditor instance.
299    */
300   function linkCommandIntegrator(editor) {
301     // Nothing to integrate with if the drupallink plugin is not loaded.
302     if (!editor.plugins.drupallink) {
303       return;
304     }
305
306     // Override default behaviour of 'drupalunlink' command.
307     editor.getCommand('drupalunlink').on('exec', function (evt) {
308       const widget = getFocusedWidget(editor);
309
310       // Override 'drupalunlink' only when link truly belongs to the widget. If
311       // wrapped inline widget in a link, let default unlink work.
312       // @see https://dev.ckeditor.com/ticket/11814
313       if (!widget || !widget.parts.link) {
314         return;
315       }
316
317       widget.setData('link', null);
318
319       // Selection (which is fake) may not change if unlinked image in focused
320       // widget, i.e. if captioned image. Let's refresh command state manually
321       // here.
322       this.refresh(editor, editor.elementPath());
323
324       evt.cancel();
325     });
326
327     // Override default refresh of 'drupalunlink' command.
328     editor.getCommand('drupalunlink').on('refresh', function (evt) {
329       const widget = getFocusedWidget(editor);
330
331       if (!widget) {
332         return;
333       }
334
335       // Note that widget may be wrapped in a link, which
336       // does not belong to that widget (#11814).
337       this.setState(widget.data.link || widget.wrapper.getAscendant('a') ?
338         CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED);
339
340       evt.cancel();
341     });
342   }
343
344   /**
345    * Gets the focused widget, if of the type specific for this plugin.
346    *
347    * @param {CKEDITOR.editor} editor
348    *   A CKEditor instance.
349    *
350    * @return {?CKEDITOR.plugins.widget}
351    *   The focused image2 widget instance, or null.
352    */
353   function getFocusedWidget(editor) {
354     const widget = editor.widgets.focused;
355
356     if (widget && widget.name === 'image') {
357       return widget;
358     }
359
360     return null;
361   }
362
363   // Expose an API for other plugins to interact with drupalimage widgets.
364   CKEDITOR.plugins.drupalimage = {
365     getFocusedWidget,
366   };
367 }(jQuery, Drupal, CKEDITOR));