ckeditor.js 12 KB

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