/**
* @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 = $(
`<div class="ckeditor-dialog-loading"><span style="top: -40px;" class="ckeditor-dialog-loading-link">${Drupal.t(
'Loading...',
)}</span></div>`,
);
$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 = window.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');
window.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;
// Default max height. Will be updated as the viewport changes.
CKEDITOR.config.autoGrow_maxHeight = 0.7 * window.innerHeight;
// 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,
);