X-Git-Url: http://www.aleph1.co.uk/gitweb/?a=blobdiff_plain;ds=sidebyside;f=web%2Fcore%2Fmodules%2Fckeditor%2Fjs%2Fckeditor.es6.js;fp=web%2Fcore%2Fmodules%2Fckeditor%2Fjs%2Fckeditor.es6.js;h=21f6e4bb451d928a61d2a4e3351b88a69a3ce9f8;hb=9917807b03b64faf00f6a1f29dcb6eafc454efa5;hp=0000000000000000000000000000000000000000;hpb=aea91e65e895364e460983b890e295aa5d5540a5;p=yaffs-website diff --git a/web/core/modules/ckeditor/js/ckeditor.es6.js b/web/core/modules/ckeditor/js/ckeditor.es6.js new file mode 100644 index 000000000..21f6e4bb4 --- /dev/null +++ b/web/core/modules/ckeditor/js/ckeditor.es6.js @@ -0,0 +1,345 @@ +/** + * @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 occured. + * @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) { + for (const pluginName in externalPlugins) { + if (externalPlugins.hasOwnProperty(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 = $(`
${Drupal.t('Loading...')}
`); + $content.appendTo($target); + + const ckeditorAjaxDialog = Drupal.ajax({ + dialog: dialogSettings, + dialogType: 'modal', + selector: '.ckeditor-dialog-loading-link', + url, + progress: { type: 'throbber' }, + submit: { + editor_object: existingValues, + }, + }); + ckeditorAjaxDialog.execute(); + + // After a short delay, show "Loading…" message. + window.setTimeout(() => { + $content.find('span').animate({ top: '0px' }); + }, 1000); + + // Store the save callback to be executed when this dialog is closed. + Drupal.ckeditor.saveCallback = saveCallback; + }, + }; + + // Moves the dialog to the top of the CKEDITOR stack. + $(window).on('dialogcreate', (e, dialog, $element, settings) => { + $('.ui-dialog--narrow').css('zIndex', CKEDITOR.config.baseFloatZIndex + 1); + }); + + // Respond to new dialogs that are opened by CKEditor, closing the AJAX loader. + $(window).on('dialog:beforecreate', (e, dialog, $element, settings) => { + $('.ckeditor-dialog-loading').animate({ top: '-40px' }, function () { + $(this).remove(); + }); + }); + + // Respond to dialogs that are saved, sending data back to CKEditor. + $(window).on('editor:dialogsave', (e, values) => { + if (Drupal.ckeditor.saveCallback) { + Drupal.ckeditor.saveCallback(values); + } + }); + + // Respond to dialogs that are closed, removing the current save handler. + $(window).on('dialog:afterclose', (e, dialog, $element) => { + if (Drupal.ckeditor.saveCallback) { + Drupal.ckeditor.saveCallback = null; + } + }); + + // Formulate a default formula for the maximum autoGrow height. + $(document).on('drupalViewportOffsetChange', () => { + CKEDITOR.config.autoGrow_maxHeight = 0.7 * (window.innerHeight - displace.offsets.top - displace.offsets.bottom); + }); + + // Redirect on hash change when the original hash has an associated CKEditor. + function redirectTextareaFragmentToCKEditorInstance() { + const hash = location.hash.substr(1); + const element = document.getElementById(hash); + if (element) { + const editor = CKEDITOR.dom.element.get(element).getEditor(); + if (editor) { + const id = editor.container.getAttribute('id'); + location.replace(`#${id}`); + } + } + } + $(window).on('hashchange.ckeditor', redirectTextareaFragmentToCKEditorInstance); + + // Set autoGrow to make the editor grow the moment it is created. + CKEDITOR.config.autoGrow_onStartup = true; + + // Set the CKEditor cache-busting string to the same value as Drupal. + CKEDITOR.timestamp = drupalSettings.ckeditor.timestamp; + + if (AjaxCommands) { + /** + * Command to add style sheets to a CKEditor instance. + * + * Works for both iframe and inline CKEditor instances. + * + * @param {Drupal.Ajax} [ajax] + * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. + * @param {object} response + * The response from the Ajax request. + * @param {string} response.editor_id + * The CKEditor instance ID. + * @param {number} [status] + * The XMLHttpRequest status. + * + * @see http://docs.ckeditor.com/#!/api/CKEDITOR.dom.document + */ + AjaxCommands.prototype.ckeditor_add_stylesheet = function (ajax, response, status) { + const editor = CKEDITOR.instances[response.editor_id]; + + if (editor) { + response.stylesheets.forEach((url) => { + editor.document.appendStyleSheet(url); + }); + } + }; + } +}(Drupal, Drupal.debounce, CKEDITOR, jQuery, Drupal.displace, Drupal.AjaxCommands));