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