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