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