Added Entity and Entity Reference Revisions which got dropped somewhere along the...
[yaffs-website] / web / core / modules / ckeditor / js / ckeditor.es6.js
1 /**
2  * @file
3  * CKEditor implementation of {@link Drupal.editors} API.
4  */
5
6 (function(Drupal, debounce, CKEDITOR, $, displace, AjaxCommands) {
7   /**
8    * @namespace
9    */
10   Drupal.editors.ckeditor = {
11     /**
12      * Editor attach callback.
13      *
14      * @param {HTMLElement} element
15      *   The element to attach the editor to.
16      * @param {string} format
17      *   The text format for the editor.
18      *
19      * @return {bool}
20      *   Whether the call to `CKEDITOR.replace()` created an editor or not.
21      */
22     attach(element, format) {
23       this._loadExternalPlugins(format);
24       // Also pass settings that are Drupal-specific.
25       format.editorSettings.drupal = {
26         format: format.format,
27       };
28
29       // Set a title on the CKEditor instance that includes the text field's
30       // label so that screen readers say something that is understandable
31       // for end users.
32       const label = $(`label[for=${element.getAttribute('id')}]`).html();
33       format.editorSettings.title = Drupal.t('Rich Text Editor, !label field', {
34         '!label': label,
35       });
36
37       return !!CKEDITOR.replace(element, format.editorSettings);
38     },
39
40     /**
41      * Editor detach callback.
42      *
43      * @param {HTMLElement} element
44      *   The element to detach the editor from.
45      * @param {string} format
46      *   The text format used for the editor.
47      * @param {string} trigger
48      *   The event trigger for the detach.
49      *
50      * @return {bool}
51      *   Whether the call to `CKEDITOR.dom.element.get(element).getEditor()`
52      *   found an editor or not.
53      */
54     detach(element, format, trigger) {
55       const editor = CKEDITOR.dom.element.get(element).getEditor();
56       if (editor) {
57         if (trigger === 'serialize') {
58           editor.updateElement();
59         } else {
60           editor.destroy();
61           element.removeAttribute('contentEditable');
62         }
63       }
64       return !!editor;
65     },
66
67     /**
68      * Reacts on a change in the editor element.
69      *
70      * @param {HTMLElement} element
71      *   The element where the change occurred.
72      * @param {function} callback
73      *   Callback called with the value of the editor.
74      *
75      * @return {bool}
76      *   Whether the call to `CKEDITOR.dom.element.get(element).getEditor()`
77      *   found an editor or not.
78      */
79     onChange(element, callback) {
80       const editor = CKEDITOR.dom.element.get(element).getEditor();
81       if (editor) {
82         editor.on(
83           'change',
84           debounce(() => {
85             callback(editor.getData());
86           }, 400),
87         );
88
89         // A temporary workaround to control scrollbar appearance when using
90         // autoGrow event to control editor's height.
91         // @todo Remove when http://dev.ckeditor.com/ticket/12120 is fixed.
92         editor.on('mode', () => {
93           const editable = editor.editable();
94           if (!editable.isInline()) {
95             editor.on(
96               'autoGrow',
97               evt => {
98                 const doc = evt.editor.document;
99                 const scrollable = CKEDITOR.env.quirks
100                   ? doc.getBody()
101                   : doc.getDocumentElement();
102
103                 if (scrollable.$.scrollHeight < scrollable.$.clientHeight) {
104                   scrollable.setStyle('overflow-y', 'hidden');
105                 } else {
106                   scrollable.removeStyle('overflow-y');
107                 }
108               },
109               null,
110               null,
111               10000,
112             );
113           }
114         });
115       }
116       return !!editor;
117     },
118
119     /**
120      * Attaches an inline editor to a DOM element.
121      *
122      * @param {HTMLElement} element
123      *   The element to attach the editor to.
124      * @param {object} format
125      *   The text format used in the editor.
126      * @param {string} [mainToolbarId]
127      *   The id attribute for the main editor toolbar, if any.
128      * @param {string} [floatedToolbarId]
129      *   The id attribute for the floated editor toolbar, if any.
130      *
131      * @return {bool}
132      *   Whether the call to `CKEDITOR.replace()` created an editor or not.
133      */
134     attachInlineEditor(element, format, mainToolbarId, floatedToolbarId) {
135       this._loadExternalPlugins(format);
136       // Also pass settings that are Drupal-specific.
137       format.editorSettings.drupal = {
138         format: format.format,
139       };
140
141       const settings = $.extend(true, {}, format.editorSettings);
142
143       // If a toolbar is already provided for "true WYSIWYG" (in-place editing),
144       // then use that toolbar instead: override the default settings to render
145       // CKEditor UI's top toolbar into mainToolbar, and don't render the bottom
146       // toolbar at all. (CKEditor doesn't need a floated toolbar.)
147       if (mainToolbarId) {
148         const settingsOverride = {
149           extraPlugins: 'sharedspace',
150           removePlugins: 'floatingspace,elementspath',
151           sharedSpaces: {
152             top: mainToolbarId,
153           },
154         };
155
156         // Find the "Source" button, if any, and replace it with "Sourcedialog".
157         // (The 'sourcearea' plugin only works in CKEditor's iframe mode.)
158         let sourceButtonFound = false;
159         for (
160           let i = 0;
161           !sourceButtonFound && i < settings.toolbar.length;
162           i++
163         ) {
164           if (settings.toolbar[i] !== '/') {
165             for (
166               let j = 0;
167               !sourceButtonFound && j < settings.toolbar[i].items.length;
168               j++
169             ) {
170               if (settings.toolbar[i].items[j] === 'Source') {
171                 sourceButtonFound = true;
172                 // Swap sourcearea's "Source" button for sourcedialog's.
173                 settings.toolbar[i].items[j] = 'Sourcedialog';
174                 settingsOverride.extraPlugins += ',sourcedialog';
175                 settingsOverride.removePlugins += ',sourcearea';
176               }
177             }
178           }
179         }
180
181         settings.extraPlugins += `,${settingsOverride.extraPlugins}`;
182         settings.removePlugins += `,${settingsOverride.removePlugins}`;
183         settings.sharedSpaces = settingsOverride.sharedSpaces;
184       }
185
186       // CKEditor requires an element to already have the contentEditable
187       // attribute set to "true", otherwise it won't attach an inline editor.
188       element.setAttribute('contentEditable', 'true');
189
190       return !!CKEDITOR.inline(element, settings);
191     },
192
193     /**
194      * Loads the required external plugins for the editor.
195      *
196      * @param {object} format
197      *   The text format used in the editor.
198      */
199     _loadExternalPlugins(format) {
200       const externalPlugins = format.editorSettings.drupalExternalPlugins;
201       // Register and load additional CKEditor plugins as necessary.
202       if (externalPlugins) {
203         Object.keys(externalPlugins || {}).forEach(pluginName => {
204           CKEDITOR.plugins.addExternal(
205             pluginName,
206             externalPlugins[pluginName],
207             '',
208           );
209         });
210         delete format.editorSettings.drupalExternalPlugins;
211       }
212     },
213   };
214
215   Drupal.ckeditor = {
216     /**
217      * Variable storing the current dialog's save callback.
218      *
219      * @type {?function}
220      */
221     saveCallback: null,
222
223     /**
224      * Open a dialog for a Drupal-based plugin.
225      *
226      * This dynamically loads jQuery UI (if necessary) using the Drupal AJAX
227      * framework, then opens a dialog at the specified Drupal path.
228      *
229      * @param {CKEditor} editor
230      *   The CKEditor instance that is opening the dialog.
231      * @param {string} url
232      *   The URL that contains the contents of the dialog.
233      * @param {object} existingValues
234      *   Existing values that will be sent via POST to the url for the dialog
235      *   contents.
236      * @param {function} saveCallback
237      *   A function to be called upon saving the dialog.
238      * @param {object} dialogSettings
239      *   An object containing settings to be passed to the jQuery UI.
240      */
241     openDialog(editor, url, existingValues, saveCallback, dialogSettings) {
242       // Locate a suitable place to display our loading indicator.
243       let $target = $(editor.container.$);
244       if (editor.elementMode === CKEDITOR.ELEMENT_MODE_REPLACE) {
245         $target = $target.find('.cke_contents');
246       }
247
248       // Remove any previous loading indicator.
249       $target
250         .css('position', 'relative')
251         .find('.ckeditor-dialog-loading')
252         .remove();
253
254       // Add a consistent dialog class.
255       const classes = dialogSettings.dialogClass
256         ? dialogSettings.dialogClass.split(' ')
257         : [];
258       classes.push('ui-dialog--narrow');
259       dialogSettings.dialogClass = classes.join(' ');
260       dialogSettings.autoResize = window.matchMedia(
261         '(min-width: 600px)',
262       ).matches;
263       dialogSettings.width = 'auto';
264
265       // Add a "Loading…" message, hide it underneath the CKEditor toolbar,
266       // create a Drupal.Ajax instance to load the dialog and trigger it.
267       const $content = $(
268         `<div class="ckeditor-dialog-loading"><span style="top: -40px;" class="ckeditor-dialog-loading-link">${Drupal.t(
269           'Loading...',
270         )}</span></div>`,
271       );
272       $content.appendTo($target);
273
274       const ckeditorAjaxDialog = Drupal.ajax({
275         dialog: dialogSettings,
276         dialogType: 'modal',
277         selector: '.ckeditor-dialog-loading-link',
278         url,
279         progress: { type: 'throbber' },
280         submit: {
281           editor_object: existingValues,
282         },
283       });
284       ckeditorAjaxDialog.execute();
285
286       // After a short delay, show "Loading…" message.
287       window.setTimeout(() => {
288         $content.find('span').animate({ top: '0px' });
289       }, 1000);
290
291       // Store the save callback to be executed when this dialog is closed.
292       Drupal.ckeditor.saveCallback = saveCallback;
293     },
294   };
295
296   // Moves the dialog to the top of the CKEDITOR stack.
297   $(window).on('dialogcreate', (e, dialog, $element, settings) => {
298     $('.ui-dialog--narrow').css('zIndex', CKEDITOR.config.baseFloatZIndex + 1);
299   });
300
301   // Respond to new dialogs that are opened by CKEditor, closing the AJAX loader.
302   $(window).on('dialog:beforecreate', (e, dialog, $element, settings) => {
303     $('.ckeditor-dialog-loading').animate({ top: '-40px' }, function() {
304       $(this).remove();
305     });
306   });
307
308   // Respond to dialogs that are saved, sending data back to CKEditor.
309   $(window).on('editor:dialogsave', (e, values) => {
310     if (Drupal.ckeditor.saveCallback) {
311       Drupal.ckeditor.saveCallback(values);
312     }
313   });
314
315   // Respond to dialogs that are closed, removing the current save handler.
316   $(window).on('dialog:afterclose', (e, dialog, $element) => {
317     if (Drupal.ckeditor.saveCallback) {
318       Drupal.ckeditor.saveCallback = null;
319     }
320   });
321
322   // Formulate a default formula for the maximum autoGrow height.
323   $(document).on('drupalViewportOffsetChange', () => {
324     CKEDITOR.config.autoGrow_maxHeight =
325       0.7 *
326       (window.innerHeight - displace.offsets.top - displace.offsets.bottom);
327   });
328
329   // Redirect on hash change when the original hash has an associated CKEditor.
330   function redirectTextareaFragmentToCKEditorInstance() {
331     const hash = window.location.hash.substr(1);
332     const element = document.getElementById(hash);
333     if (element) {
334       const editor = CKEDITOR.dom.element.get(element).getEditor();
335       if (editor) {
336         const id = editor.container.getAttribute('id');
337         window.location.replace(`#${id}`);
338       }
339     }
340   }
341   $(window).on(
342     'hashchange.ckeditor',
343     redirectTextareaFragmentToCKEditorInstance,
344   );
345
346   // Set autoGrow to make the editor grow the moment it is created.
347   CKEDITOR.config.autoGrow_onStartup = true;
348
349   // Set the CKEditor cache-busting string to the same value as Drupal.
350   CKEDITOR.timestamp = drupalSettings.ckeditor.timestamp;
351
352   if (AjaxCommands) {
353     /**
354      * Command to add style sheets to a CKEditor instance.
355      *
356      * Works for both iframe and inline CKEditor instances.
357      *
358      * @param {Drupal.Ajax} [ajax]
359      *   {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
360      * @param {object} response
361      *   The response from the Ajax request.
362      * @param {string} response.editor_id
363      *   The CKEditor instance ID.
364      * @param {number} [status]
365      *   The XMLHttpRequest status.
366      *
367      * @see http://docs.ckeditor.com/#!/api/CKEDITOR.dom.document
368      */
369     AjaxCommands.prototype.ckeditor_add_stylesheet = function(
370       ajax,
371       response,
372       status,
373     ) {
374       const editor = CKEDITOR.instances[response.editor_id];
375
376       if (editor) {
377         response.stylesheets.forEach(url => {
378           editor.document.appendStyleSheet(url);
379         });
380       }
381     };
382   }
383 })(
384   Drupal,
385   Drupal.debounce,
386   CKEDITOR,
387   jQuery,
388   Drupal.displace,
389   Drupal.AjaxCommands,
390 );