/** * @file * CKEditor implementation of {@link Drupal.editors} API. */ (function(Drupal, debounce, CKEDITOR, $, displace, AjaxCommands) { /** * @namespace */ Drupal.editors.ckeditor = { /** * Editor attach callback. * * @param {HTMLElement} element * The element to attach the editor to. * @param {string} format * The text format for the editor. * * @return {bool} * Whether the call to `CKEDITOR.replace()` created an editor or not. */ attach(element, format) { this._loadExternalPlugins(format); // Also pass settings that are Drupal-specific. format.editorSettings.drupal = { format: format.format, }; // Set a title on the CKEditor instance that includes the text field's // label so that screen readers say something that is understandable // for end users. const label = $(`label[for=${element.getAttribute('id')}]`).html(); format.editorSettings.title = Drupal.t('Rich Text Editor, !label field', { '!label': label, }); return !!CKEDITOR.replace(element, format.editorSettings); }, /** * Editor detach callback. * * @param {HTMLElement} element * The element to detach the editor from. * @param {string} format * The text format used for the editor. * @param {string} trigger * The event trigger for the detach. * * @return {bool} * Whether the call to `CKEDITOR.dom.element.get(element).getEditor()` * found an editor or not. */ detach(element, format, trigger) { const editor = CKEDITOR.dom.element.get(element).getEditor(); if (editor) { if (trigger === 'serialize') { editor.updateElement(); } else { editor.destroy(); element.removeAttribute('contentEditable'); } } return !!editor; }, /** * Reacts on a change in the editor element. * * @param {HTMLElement} element * The element where the change occurred. * @param {function} callback * Callback called with the value of the editor. * * @return {bool} * Whether the call to `CKEDITOR.dom.element.get(element).getEditor()` * found an editor or not. */ onChange(element, callback) { const editor = CKEDITOR.dom.element.get(element).getEditor(); if (editor) { editor.on( 'change', debounce(() => { callback(editor.getData()); }, 400), ); // A temporary workaround to control scrollbar appearance when using // autoGrow event to control editor's height. // @todo Remove when http://dev.ckeditor.com/ticket/12120 is fixed. editor.on('mode', () => { const editable = editor.editable(); if (!editable.isInline()) { editor.on( 'autoGrow', evt => { const doc = evt.editor.document; const scrollable = CKEDITOR.env.quirks ? doc.getBody() : doc.getDocumentElement(); if (scrollable.$.scrollHeight < scrollable.$.clientHeight) { scrollable.setStyle('overflow-y', 'hidden'); } else { scrollable.removeStyle('overflow-y'); } }, null, null, 10000, ); } }); } return !!editor; }, /** * Attaches an inline editor to a DOM element. * * @param {HTMLElement} element * The element to attach the editor to. * @param {object} format * The text format used in the editor. * @param {string} [mainToolbarId] * The id attribute for the main editor toolbar, if any. * @param {string} [floatedToolbarId] * The id attribute for the floated editor toolbar, if any. * * @return {bool} * Whether the call to `CKEDITOR.replace()` created an editor or not. */ attachInlineEditor(element, format, mainToolbarId, floatedToolbarId) { this._loadExternalPlugins(format); // Also pass settings that are Drupal-specific. format.editorSettings.drupal = { format: format.format, }; const settings = $.extend(true, {}, format.editorSettings); // If a toolbar is already provided for "true WYSIWYG" (in-place editing), // then use that toolbar instead: override the default settings to render // CKEditor UI's top toolbar into mainToolbar, and don't render the bottom // toolbar at all. (CKEditor doesn't need a floated toolbar.) if (mainToolbarId) { const settingsOverride = { extraPlugins: 'sharedspace', removePlugins: 'floatingspace,elementspath', sharedSpaces: { top: mainToolbarId, }, }; // Find the "Source" button, if any, and replace it with "Sourcedialog". // (The 'sourcearea' plugin only works in CKEditor's iframe mode.) let sourceButtonFound = false; for ( let i = 0; !sourceButtonFound && i < settings.toolbar.length; i++ ) { if (settings.toolbar[i] !== '/') { for ( let j = 0; !sourceButtonFound && j < settings.toolbar[i].items.length; j++ ) { if (settings.toolbar[i].items[j] === 'Source') { sourceButtonFound = true; // Swap sourcearea's "Source" button for sourcedialog's. settings.toolbar[i].items[j] = 'Sourcedialog'; settingsOverride.extraPlugins += ',sourcedialog'; settingsOverride.removePlugins += ',sourcearea'; } } } } settings.extraPlugins += `,${settingsOverride.extraPlugins}`; settings.removePlugins += `,${settingsOverride.removePlugins}`; settings.sharedSpaces = settingsOverride.sharedSpaces; } // CKEditor requires an element to already have the contentEditable // attribute set to "true", otherwise it won't attach an inline editor. element.setAttribute('contentEditable', 'true'); return !!CKEDITOR.inline(element, settings); }, /** * Loads the required external plugins for the editor. * * @param {object} format * The text format used in the editor. */ _loadExternalPlugins(format) { const externalPlugins = format.editorSettings.drupalExternalPlugins; // Register and load additional CKEditor plugins as necessary. if (externalPlugins) { Object.keys(externalPlugins || {}).forEach(pluginName => { CKEDITOR.plugins.addExternal( pluginName, externalPlugins[pluginName], '', ); }); delete format.editorSettings.drupalExternalPlugins; } }, }; Drupal.ckeditor = { /** * Variable storing the current dialog's save callback. * * @type {?function} */ saveCallback: null, /** * Open a dialog for a Drupal-based plugin. * * This dynamically loads jQuery UI (if necessary) using the Drupal AJAX * framework, then opens a dialog at the specified Drupal path. * * @param {CKEditor} editor * The CKEditor instance that is opening the dialog. * @param {string} url * The URL that contains the contents of the dialog. * @param {object} existingValues * Existing values that will be sent via POST to the url for the dialog * contents. * @param {function} saveCallback * A function to be called upon saving the dialog. * @param {object} dialogSettings * An object containing settings to be passed to the jQuery UI. */ openDialog(editor, url, existingValues, saveCallback, dialogSettings) { // Locate a suitable place to display our loading indicator. let $target = $(editor.container.$); if (editor.elementMode === CKEDITOR.ELEMENT_MODE_REPLACE) { $target = $target.find('.cke_contents'); } // Remove any previous loading indicator. $target .css('position', 'relative') .find('.ckeditor-dialog-loading') .remove(); // Add a consistent dialog class. const classes = dialogSettings.dialogClass ? dialogSettings.dialogClass.split(' ') : []; classes.push('ui-dialog--narrow'); dialogSettings.dialogClass = classes.join(' '); dialogSettings.autoResize = window.matchMedia( '(min-width: 600px)', ).matches; dialogSettings.width = 'auto'; // Add a "Loading…" message, hide it underneath the CKEditor toolbar, // create a Drupal.Ajax instance to load the dialog and trigger it. const $content = $( `