3 * CKEditor implementation of {@link Drupal.editors} API.
6 (function (Drupal, debounce, CKEDITOR, $, displace, AjaxCommands) {
13 Drupal.editors.ckeditor = {
16 * Editor attach callback.
18 * @param {HTMLElement} element
19 * The element to attach the editor to.
20 * @param {string} format
21 * The text format for the editor.
24 * Whether the call to `CKEDITOR.replace()` created an editor or not.
26 attach: function (element, format) {
27 this._loadExternalPlugins(format);
28 // Also pass settings that are Drupal-specific.
29 format.editorSettings.drupal = {
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
36 var label = $('label[for=' + element.getAttribute('id') + ']').html();
37 format.editorSettings.title = Drupal.t('Rich Text Editor, !label field', {'!label': label});
39 return !!CKEDITOR.replace(element, format.editorSettings);
43 * Editor detach callback.
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.
53 * Whether the call to `CKEDITOR.dom.element.get(element).getEditor()`
54 * found an editor or not.
56 detach: function (element, format, trigger) {
57 var editor = CKEDITOR.dom.element.get(element).getEditor();
59 if (trigger === 'serialize') {
60 editor.updateElement();
64 element.removeAttribute('contentEditable');
71 * Reacts on a change in the editor element.
73 * @param {HTMLElement} element
74 * The element where the change occured.
75 * @param {function} callback
76 * Callback called with the value of the editor.
79 * Whether the call to `CKEDITOR.dom.element.get(element).getEditor()`
80 * found an editor or not.
82 onChange: function (element, callback) {
83 var editor = CKEDITOR.dom.element.get(element).getEditor();
85 editor.on('change', debounce(function () {
86 callback(editor.getData());
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();
99 if (scrollable.$.scrollHeight < scrollable.$.clientHeight) {
100 scrollable.setStyle('overflow-y', 'hidden');
103 scrollable.removeStyle('overflow-y');
105 }, null, null, 10000);
113 * Attaches an inline editor to a DOM element.
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.
125 * Whether the call to `CKEDITOR.replace()` created an editor or not.
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
134 var settings = $.extend(true, {}, format.editorSettings);
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.)
141 var settingsOverride = {
142 extraPlugins: 'sharedspace',
143 removePlugins: 'floatingspace,elementspath',
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';
166 settings.extraPlugins += ',' + settingsOverride.extraPlugins;
167 settings.removePlugins += ',' + settingsOverride.removePlugins;
168 settings.sharedSpaces = settingsOverride.sharedSpaces;
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');
175 return !!CKEDITOR.inline(element, settings);
179 * Loads the required external plugins for the editor.
181 * @param {object} format
182 * The text format used in the editor.
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], '');
193 delete format.editorSettings.drupalExternalPlugins;
202 * Variable storing the current dialog's save callback.
209 * Open a dialog for a Drupal-based plugin.
211 * This dynamically loads jQuery UI (if necessary) using the Drupal AJAX
212 * framework, then opens a dialog at the specified Drupal path.
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
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.
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');
233 // Remove any previous loading indicator.
234 $target.css('position', 'relative').find('.ckeditor-dialog-loading').remove();
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';
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);
248 var ckeditorAjaxDialog = Drupal.ajax({
249 dialog: dialogSettings,
251 selector: '.ckeditor-dialog-loading-link',
253 progress: {type: 'throbber'},
255 editor_object: existingValues
258 ckeditorAjaxDialog.execute();
260 // After a short delay, show "Loading…" message.
261 window.setTimeout(function () {
262 $content.find('span').animate({top: '0px'});
265 // Store the save callback to be executed when this dialog is closed.
266 Drupal.ckeditor.saveCallback = saveCallback;
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);
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 () {
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);
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;
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);
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);
306 var editor = CKEDITOR.dom.element.get(element).getEditor();
308 var id = editor.container.getAttribute('id');
309 location.replace('#' + id);
313 $(window).on('hashchange.ckeditor', redirectTextareaFragmentToCKEditorInstance);
315 // Set autoGrow to make the editor grow the moment it is created.
316 CKEDITOR.config.autoGrow_onStartup = true;
318 // Set the CKEditor cache-busting string to the same value as Drupal.
319 CKEDITOR.timestamp = drupalSettings.ckeditor.timestamp;
324 * Command to add style sheets to a CKEditor instance.
326 * Works for both iframe and inline CKEditor instances.
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.
337 * @see http://docs.ckeditor.com/#!/api/CKEDITOR.dom.document
339 AjaxCommands.prototype.ckeditor_add_stylesheet = function (ajax, response, status) {
340 var editor = CKEDITOR.instances[response.editor_id];
343 response.stylesheets.forEach(function (url) {
344 editor.document.appendStyleSheet(url);
350 })(Drupal, Drupal.debounce, CKEDITOR, jQuery, Drupal.displace, Drupal.AjaxCommands);