Newer
Older
Alex Pott
committed
/**
* @file
* CKEditor implementation of {@link Drupal.editors} API.
*/
Lauri Eskola
committed
(function(Drupal, debounce, CKEDITOR, $, displace, AjaxCommands) {
Alex Pott
committed
/**
* @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) {
Alex Pott
committed
this._loadExternalPlugins(format);
// Also pass settings that are Drupal-specific.
format.editorSettings.drupal = {
format: format.format,
Alex Pott
committed
};
// 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();
Lauri Eskola
committed
format.editorSettings.title = Drupal.t('Rich Text Editor, !label field', {
'!label': label,
});
Alex Pott
committed
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();
Alex Pott
committed
if (editor) {
if (trigger === 'serialize') {
editor.updateElement();
Lauri Eskola
committed
} else {
Alex Pott
committed
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();
Alex Pott
committed
if (editor) {
Lauri Eskola
committed
editor.on(
'change',
debounce(() => {
callback(editor.getData());
}, 400),
);
Alex Pott
committed
// 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();
Alex Pott
committed
if (!editable.isInline()) {
Lauri Eskola
committed
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,
);
Alex Pott
committed
}
});
}
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) {
Alex Pott
committed
this._loadExternalPlugins(format);
// Also pass settings that are Drupal-specific.
format.editorSettings.drupal = {
format: format.format,
Alex Pott
committed
};
const settings = $.extend(true, {}, format.editorSettings);
Alex Pott
committed
// 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 = {
Alex Pott
committed
extraPlugins: 'sharedspace',
removePlugins: 'floatingspace,elementspath',
sharedSpaces: {
top: mainToolbarId,
},
Alex Pott
committed
};
// Find the "Source" button, if any, and replace it with "Sourcedialog".
// (The 'sourcearea' plugin only works in CKEditor's iframe mode.)
let sourceButtonFound = false;
Lauri Eskola
committed
for (
let i = 0;
!sourceButtonFound && i < settings.toolbar.length;
i++
) {
Alex Pott
committed
if (settings.toolbar[i] !== '/') {
Lauri Eskola
committed
for (
let j = 0;
!sourceButtonFound && j < settings.toolbar[i].items.length;
j++
) {
Alex Pott
committed
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}`;
Alex Pott
committed
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;
Alex Pott
committed
// Register and load additional CKEditor plugins as necessary.
if (externalPlugins) {
Lauri Eskola
committed
Object.keys(externalPlugins || {}).forEach(pluginName => {
CKEDITOR.plugins.addExternal(
pluginName,
externalPlugins[pluginName],
'',
);
Angie Byron
committed
});
Alex Pott
committed
delete format.editorSettings.drupalExternalPlugins;
}
},
Alex Pott
committed
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
};
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) {
Alex Pott
committed
// Locate a suitable place to display our loading indicator.
let $target = $(editor.container.$);
Alex Pott
committed
if (editor.elementMode === CKEDITOR.ELEMENT_MODE_REPLACE) {
$target = $target.find('.cke_contents');
}
// Remove any previous loading indicator.
Lauri Eskola
committed
$target
.css('position', 'relative')
.find('.ckeditor-dialog-loading')
.remove();
Alex Pott
committed
// Add a consistent dialog class.
Lauri Eskola
committed
const classes = dialogSettings.dialogClass
? dialogSettings.dialogClass.split(' ')
: [];
Alex Pott
committed
classes.push('ui-dialog--narrow');
dialogSettings.dialogClass = classes.join(' ');
Lauri Eskola
committed
dialogSettings.autoResize = window.matchMedia(
'(min-width: 600px)',
).matches;
Alex Pott
committed
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.
Lauri Eskola
committed
const $content = $(
`<div class="ckeditor-dialog-loading"><span style="top: -40px;" class="ckeditor-dialog-loading-link">${Drupal.t(
'Loading...',
)}</span></div>`,
);
Alex Pott
committed
$content.appendTo($target);
const ckeditorAjaxDialog = Drupal.ajax({
Alex Pott
committed
dialog: dialogSettings,
dialogType: 'modal',
selector: '.ckeditor-dialog-loading-link',
url,
progress: { type: 'throbber' },
Alex Pott
committed
submit: {
editor_object: existingValues,
},
Alex Pott
committed
});
ckeditorAjaxDialog.execute();
// After a short delay, show "Loading…" message.
window.setTimeout(() => {
$content.find('span').animate({ top: '0px' });
Alex Pott
committed
}, 1000);
// Store the save callback to be executed when this dialog is closed.
Drupal.ckeditor.saveCallback = saveCallback;
},
Alex Pott
committed
};
// Moves the dialog to the top of the CKEDITOR stack.
$(window).on('dialogcreate', (e, dialog, $element, settings) => {
Alex Pott
committed
$('.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) => {
Lauri Eskola
committed
$('.ckeditor-dialog-loading').animate({ top: '-40px' }, function() {
Alex Pott
committed
$(this).remove();
});
});
// Respond to dialogs that are saved, sending data back to CKEditor.
$(window).on('editor:dialogsave', (e, values) => {
Alex Pott
committed
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) => {
Alex Pott
committed
if (Drupal.ckeditor.saveCallback) {
Drupal.ckeditor.saveCallback = null;
}
});
// Formulate a default formula for the maximum autoGrow height.
$(document).on('drupalViewportOffsetChange', () => {
Lauri Eskola
committed
CKEDITOR.config.autoGrow_maxHeight =
0.7 *
(window.innerHeight - displace.offsets.top - displace.offsets.bottom);
Alex Pott
committed
});
// Redirect on hash change when the original hash has an associated CKEditor.
function redirectTextareaFragmentToCKEditorInstance() {
Lauri Eskola
committed
const hash = window.location.hash.substr(1);
const element = document.getElementById(hash);
Alex Pott
committed
if (element) {
const editor = CKEDITOR.dom.element.get(element).getEditor();
Alex Pott
committed
if (editor) {
const id = editor.container.getAttribute('id');
Lauri Eskola
committed
window.location.replace(`#${id}`);
Alex Pott
committed
}
}
}
Lauri Eskola
committed
$(window).on(
'hashchange.ckeditor',
redirectTextareaFragmentToCKEditorInstance,
);
Alex Pott
committed
// 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
*/
Lauri Eskola
committed
AjaxCommands.prototype.ckeditor_add_stylesheet = function(
ajax,
response,
status,
) {
const editor = CKEDITOR.instances[response.editor_id];
Alex Pott
committed
if (editor) {
Lauri Eskola
committed
response.stylesheets.forEach(url => {
Alex Pott
committed
editor.document.appendStyleSheet(url);
});
}
};
}
Lauri Eskola
committed
})(
Drupal,
Drupal.debounce,
CKEDITOR,
jQuery,
Drupal.displace,
Drupal.AjaxCommands,
);