Chris@0
|
1 /**
|
Chris@0
|
2 * @file
|
Chris@0
|
3 * CKEditor implementation of {@link Drupal.editors} API.
|
Chris@0
|
4 */
|
Chris@0
|
5
|
Chris@17
|
6 (function(Drupal, debounce, CKEDITOR, $, displace, AjaxCommands) {
|
Chris@0
|
7 /**
|
Chris@0
|
8 * @namespace
|
Chris@0
|
9 */
|
Chris@0
|
10 Drupal.editors.ckeditor = {
|
Chris@0
|
11 /**
|
Chris@0
|
12 * Editor attach callback.
|
Chris@0
|
13 *
|
Chris@0
|
14 * @param {HTMLElement} element
|
Chris@0
|
15 * The element to attach the editor to.
|
Chris@0
|
16 * @param {string} format
|
Chris@0
|
17 * The text format for the editor.
|
Chris@0
|
18 *
|
Chris@0
|
19 * @return {bool}
|
Chris@0
|
20 * Whether the call to `CKEDITOR.replace()` created an editor or not.
|
Chris@0
|
21 */
|
Chris@0
|
22 attach(element, format) {
|
Chris@0
|
23 this._loadExternalPlugins(format);
|
Chris@0
|
24 // Also pass settings that are Drupal-specific.
|
Chris@0
|
25 format.editorSettings.drupal = {
|
Chris@0
|
26 format: format.format,
|
Chris@0
|
27 };
|
Chris@0
|
28
|
Chris@0
|
29 // Set a title on the CKEditor instance that includes the text field's
|
Chris@0
|
30 // label so that screen readers say something that is understandable
|
Chris@0
|
31 // for end users.
|
Chris@0
|
32 const label = $(`label[for=${element.getAttribute('id')}]`).html();
|
Chris@17
|
33 format.editorSettings.title = Drupal.t('Rich Text Editor, !label field', {
|
Chris@17
|
34 '!label': label,
|
Chris@17
|
35 });
|
Chris@0
|
36
|
Chris@0
|
37 return !!CKEDITOR.replace(element, format.editorSettings);
|
Chris@0
|
38 },
|
Chris@0
|
39
|
Chris@0
|
40 /**
|
Chris@0
|
41 * Editor detach callback.
|
Chris@0
|
42 *
|
Chris@0
|
43 * @param {HTMLElement} element
|
Chris@0
|
44 * The element to detach the editor from.
|
Chris@0
|
45 * @param {string} format
|
Chris@0
|
46 * The text format used for the editor.
|
Chris@0
|
47 * @param {string} trigger
|
Chris@0
|
48 * The event trigger for the detach.
|
Chris@0
|
49 *
|
Chris@0
|
50 * @return {bool}
|
Chris@0
|
51 * Whether the call to `CKEDITOR.dom.element.get(element).getEditor()`
|
Chris@0
|
52 * found an editor or not.
|
Chris@0
|
53 */
|
Chris@0
|
54 detach(element, format, trigger) {
|
Chris@0
|
55 const editor = CKEDITOR.dom.element.get(element).getEditor();
|
Chris@0
|
56 if (editor) {
|
Chris@0
|
57 if (trigger === 'serialize') {
|
Chris@0
|
58 editor.updateElement();
|
Chris@17
|
59 } else {
|
Chris@0
|
60 editor.destroy();
|
Chris@0
|
61 element.removeAttribute('contentEditable');
|
Chris@0
|
62 }
|
Chris@0
|
63 }
|
Chris@0
|
64 return !!editor;
|
Chris@0
|
65 },
|
Chris@0
|
66
|
Chris@0
|
67 /**
|
Chris@0
|
68 * Reacts on a change in the editor element.
|
Chris@0
|
69 *
|
Chris@0
|
70 * @param {HTMLElement} element
|
Chris@17
|
71 * The element where the change occurred.
|
Chris@0
|
72 * @param {function} callback
|
Chris@0
|
73 * Callback called with the value of the editor.
|
Chris@0
|
74 *
|
Chris@0
|
75 * @return {bool}
|
Chris@0
|
76 * Whether the call to `CKEDITOR.dom.element.get(element).getEditor()`
|
Chris@0
|
77 * found an editor or not.
|
Chris@0
|
78 */
|
Chris@0
|
79 onChange(element, callback) {
|
Chris@0
|
80 const editor = CKEDITOR.dom.element.get(element).getEditor();
|
Chris@0
|
81 if (editor) {
|
Chris@17
|
82 editor.on(
|
Chris@17
|
83 'change',
|
Chris@17
|
84 debounce(() => {
|
Chris@17
|
85 callback(editor.getData());
|
Chris@17
|
86 }, 400),
|
Chris@17
|
87 );
|
Chris@0
|
88
|
Chris@0
|
89 // A temporary workaround to control scrollbar appearance when using
|
Chris@0
|
90 // autoGrow event to control editor's height.
|
Chris@0
|
91 // @todo Remove when http://dev.ckeditor.com/ticket/12120 is fixed.
|
Chris@0
|
92 editor.on('mode', () => {
|
Chris@0
|
93 const editable = editor.editable();
|
Chris@0
|
94 if (!editable.isInline()) {
|
Chris@17
|
95 editor.on(
|
Chris@17
|
96 'autoGrow',
|
Chris@17
|
97 evt => {
|
Chris@17
|
98 const doc = evt.editor.document;
|
Chris@17
|
99 const scrollable = CKEDITOR.env.quirks
|
Chris@17
|
100 ? doc.getBody()
|
Chris@17
|
101 : doc.getDocumentElement();
|
Chris@0
|
102
|
Chris@17
|
103 if (scrollable.$.scrollHeight < scrollable.$.clientHeight) {
|
Chris@17
|
104 scrollable.setStyle('overflow-y', 'hidden');
|
Chris@17
|
105 } else {
|
Chris@17
|
106 scrollable.removeStyle('overflow-y');
|
Chris@17
|
107 }
|
Chris@17
|
108 },
|
Chris@17
|
109 null,
|
Chris@17
|
110 null,
|
Chris@17
|
111 10000,
|
Chris@17
|
112 );
|
Chris@0
|
113 }
|
Chris@0
|
114 });
|
Chris@0
|
115 }
|
Chris@0
|
116 return !!editor;
|
Chris@0
|
117 },
|
Chris@0
|
118
|
Chris@0
|
119 /**
|
Chris@0
|
120 * Attaches an inline editor to a DOM element.
|
Chris@0
|
121 *
|
Chris@0
|
122 * @param {HTMLElement} element
|
Chris@0
|
123 * The element to attach the editor to.
|
Chris@0
|
124 * @param {object} format
|
Chris@0
|
125 * The text format used in the editor.
|
Chris@0
|
126 * @param {string} [mainToolbarId]
|
Chris@0
|
127 * The id attribute for the main editor toolbar, if any.
|
Chris@0
|
128 * @param {string} [floatedToolbarId]
|
Chris@0
|
129 * The id attribute for the floated editor toolbar, if any.
|
Chris@0
|
130 *
|
Chris@0
|
131 * @return {bool}
|
Chris@0
|
132 * Whether the call to `CKEDITOR.replace()` created an editor or not.
|
Chris@0
|
133 */
|
Chris@0
|
134 attachInlineEditor(element, format, mainToolbarId, floatedToolbarId) {
|
Chris@0
|
135 this._loadExternalPlugins(format);
|
Chris@0
|
136 // Also pass settings that are Drupal-specific.
|
Chris@0
|
137 format.editorSettings.drupal = {
|
Chris@0
|
138 format: format.format,
|
Chris@0
|
139 };
|
Chris@0
|
140
|
Chris@0
|
141 const settings = $.extend(true, {}, format.editorSettings);
|
Chris@0
|
142
|
Chris@0
|
143 // If a toolbar is already provided for "true WYSIWYG" (in-place editing),
|
Chris@0
|
144 // then use that toolbar instead: override the default settings to render
|
Chris@0
|
145 // CKEditor UI's top toolbar into mainToolbar, and don't render the bottom
|
Chris@0
|
146 // toolbar at all. (CKEditor doesn't need a floated toolbar.)
|
Chris@0
|
147 if (mainToolbarId) {
|
Chris@0
|
148 const settingsOverride = {
|
Chris@0
|
149 extraPlugins: 'sharedspace',
|
Chris@0
|
150 removePlugins: 'floatingspace,elementspath',
|
Chris@0
|
151 sharedSpaces: {
|
Chris@0
|
152 top: mainToolbarId,
|
Chris@0
|
153 },
|
Chris@0
|
154 };
|
Chris@0
|
155
|
Chris@0
|
156 // Find the "Source" button, if any, and replace it with "Sourcedialog".
|
Chris@0
|
157 // (The 'sourcearea' plugin only works in CKEditor's iframe mode.)
|
Chris@0
|
158 let sourceButtonFound = false;
|
Chris@17
|
159 for (
|
Chris@17
|
160 let i = 0;
|
Chris@17
|
161 !sourceButtonFound && i < settings.toolbar.length;
|
Chris@17
|
162 i++
|
Chris@17
|
163 ) {
|
Chris@0
|
164 if (settings.toolbar[i] !== '/') {
|
Chris@17
|
165 for (
|
Chris@17
|
166 let j = 0;
|
Chris@17
|
167 !sourceButtonFound && j < settings.toolbar[i].items.length;
|
Chris@17
|
168 j++
|
Chris@17
|
169 ) {
|
Chris@0
|
170 if (settings.toolbar[i].items[j] === 'Source') {
|
Chris@0
|
171 sourceButtonFound = true;
|
Chris@0
|
172 // Swap sourcearea's "Source" button for sourcedialog's.
|
Chris@0
|
173 settings.toolbar[i].items[j] = 'Sourcedialog';
|
Chris@0
|
174 settingsOverride.extraPlugins += ',sourcedialog';
|
Chris@0
|
175 settingsOverride.removePlugins += ',sourcearea';
|
Chris@0
|
176 }
|
Chris@0
|
177 }
|
Chris@0
|
178 }
|
Chris@0
|
179 }
|
Chris@0
|
180
|
Chris@0
|
181 settings.extraPlugins += `,${settingsOverride.extraPlugins}`;
|
Chris@0
|
182 settings.removePlugins += `,${settingsOverride.removePlugins}`;
|
Chris@0
|
183 settings.sharedSpaces = settingsOverride.sharedSpaces;
|
Chris@0
|
184 }
|
Chris@0
|
185
|
Chris@0
|
186 // CKEditor requires an element to already have the contentEditable
|
Chris@0
|
187 // attribute set to "true", otherwise it won't attach an inline editor.
|
Chris@0
|
188 element.setAttribute('contentEditable', 'true');
|
Chris@0
|
189
|
Chris@0
|
190 return !!CKEDITOR.inline(element, settings);
|
Chris@0
|
191 },
|
Chris@0
|
192
|
Chris@0
|
193 /**
|
Chris@0
|
194 * Loads the required external plugins for the editor.
|
Chris@0
|
195 *
|
Chris@0
|
196 * @param {object} format
|
Chris@0
|
197 * The text format used in the editor.
|
Chris@0
|
198 */
|
Chris@0
|
199 _loadExternalPlugins(format) {
|
Chris@0
|
200 const externalPlugins = format.editorSettings.drupalExternalPlugins;
|
Chris@0
|
201 // Register and load additional CKEditor plugins as necessary.
|
Chris@0
|
202 if (externalPlugins) {
|
Chris@17
|
203 Object.keys(externalPlugins || {}).forEach(pluginName => {
|
Chris@17
|
204 CKEDITOR.plugins.addExternal(
|
Chris@17
|
205 pluginName,
|
Chris@17
|
206 externalPlugins[pluginName],
|
Chris@17
|
207 '',
|
Chris@17
|
208 );
|
Chris@14
|
209 });
|
Chris@0
|
210 delete format.editorSettings.drupalExternalPlugins;
|
Chris@0
|
211 }
|
Chris@0
|
212 },
|
Chris@0
|
213 };
|
Chris@0
|
214
|
Chris@0
|
215 Drupal.ckeditor = {
|
Chris@0
|
216 /**
|
Chris@0
|
217 * Variable storing the current dialog's save callback.
|
Chris@0
|
218 *
|
Chris@0
|
219 * @type {?function}
|
Chris@0
|
220 */
|
Chris@0
|
221 saveCallback: null,
|
Chris@0
|
222
|
Chris@0
|
223 /**
|
Chris@0
|
224 * Open a dialog for a Drupal-based plugin.
|
Chris@0
|
225 *
|
Chris@0
|
226 * This dynamically loads jQuery UI (if necessary) using the Drupal AJAX
|
Chris@0
|
227 * framework, then opens a dialog at the specified Drupal path.
|
Chris@0
|
228 *
|
Chris@0
|
229 * @param {CKEditor} editor
|
Chris@0
|
230 * The CKEditor instance that is opening the dialog.
|
Chris@0
|
231 * @param {string} url
|
Chris@0
|
232 * The URL that contains the contents of the dialog.
|
Chris@0
|
233 * @param {object} existingValues
|
Chris@0
|
234 * Existing values that will be sent via POST to the url for the dialog
|
Chris@0
|
235 * contents.
|
Chris@0
|
236 * @param {function} saveCallback
|
Chris@0
|
237 * A function to be called upon saving the dialog.
|
Chris@0
|
238 * @param {object} dialogSettings
|
Chris@0
|
239 * An object containing settings to be passed to the jQuery UI.
|
Chris@0
|
240 */
|
Chris@0
|
241 openDialog(editor, url, existingValues, saveCallback, dialogSettings) {
|
Chris@0
|
242 // Locate a suitable place to display our loading indicator.
|
Chris@0
|
243 let $target = $(editor.container.$);
|
Chris@0
|
244 if (editor.elementMode === CKEDITOR.ELEMENT_MODE_REPLACE) {
|
Chris@0
|
245 $target = $target.find('.cke_contents');
|
Chris@0
|
246 }
|
Chris@0
|
247
|
Chris@0
|
248 // Remove any previous loading indicator.
|
Chris@17
|
249 $target
|
Chris@17
|
250 .css('position', 'relative')
|
Chris@17
|
251 .find('.ckeditor-dialog-loading')
|
Chris@17
|
252 .remove();
|
Chris@0
|
253
|
Chris@0
|
254 // Add a consistent dialog class.
|
Chris@17
|
255 const classes = dialogSettings.dialogClass
|
Chris@17
|
256 ? dialogSettings.dialogClass.split(' ')
|
Chris@17
|
257 : [];
|
Chris@0
|
258 classes.push('ui-dialog--narrow');
|
Chris@0
|
259 dialogSettings.dialogClass = classes.join(' ');
|
Chris@17
|
260 dialogSettings.autoResize = window.matchMedia(
|
Chris@17
|
261 '(min-width: 600px)',
|
Chris@17
|
262 ).matches;
|
Chris@0
|
263 dialogSettings.width = 'auto';
|
Chris@0
|
264
|
Chris@0
|
265 // Add a "Loading…" message, hide it underneath the CKEditor toolbar,
|
Chris@0
|
266 // create a Drupal.Ajax instance to load the dialog and trigger it.
|
Chris@17
|
267 const $content = $(
|
Chris@17
|
268 `<div class="ckeditor-dialog-loading"><span style="top: -40px;" class="ckeditor-dialog-loading-link">${Drupal.t(
|
Chris@17
|
269 'Loading...',
|
Chris@17
|
270 )}</span></div>`,
|
Chris@17
|
271 );
|
Chris@0
|
272 $content.appendTo($target);
|
Chris@0
|
273
|
Chris@0
|
274 const ckeditorAjaxDialog = Drupal.ajax({
|
Chris@0
|
275 dialog: dialogSettings,
|
Chris@0
|
276 dialogType: 'modal',
|
Chris@0
|
277 selector: '.ckeditor-dialog-loading-link',
|
Chris@0
|
278 url,
|
Chris@0
|
279 progress: { type: 'throbber' },
|
Chris@0
|
280 submit: {
|
Chris@0
|
281 editor_object: existingValues,
|
Chris@0
|
282 },
|
Chris@0
|
283 });
|
Chris@0
|
284 ckeditorAjaxDialog.execute();
|
Chris@0
|
285
|
Chris@0
|
286 // After a short delay, show "Loading…" message.
|
Chris@0
|
287 window.setTimeout(() => {
|
Chris@0
|
288 $content.find('span').animate({ top: '0px' });
|
Chris@0
|
289 }, 1000);
|
Chris@0
|
290
|
Chris@0
|
291 // Store the save callback to be executed when this dialog is closed.
|
Chris@0
|
292 Drupal.ckeditor.saveCallback = saveCallback;
|
Chris@0
|
293 },
|
Chris@0
|
294 };
|
Chris@0
|
295
|
Chris@0
|
296 // Moves the dialog to the top of the CKEDITOR stack.
|
Chris@0
|
297 $(window).on('dialogcreate', (e, dialog, $element, settings) => {
|
Chris@0
|
298 $('.ui-dialog--narrow').css('zIndex', CKEDITOR.config.baseFloatZIndex + 1);
|
Chris@0
|
299 });
|
Chris@0
|
300
|
Chris@0
|
301 // Respond to new dialogs that are opened by CKEditor, closing the AJAX loader.
|
Chris@0
|
302 $(window).on('dialog:beforecreate', (e, dialog, $element, settings) => {
|
Chris@17
|
303 $('.ckeditor-dialog-loading').animate({ top: '-40px' }, function() {
|
Chris@0
|
304 $(this).remove();
|
Chris@0
|
305 });
|
Chris@0
|
306 });
|
Chris@0
|
307
|
Chris@0
|
308 // Respond to dialogs that are saved, sending data back to CKEditor.
|
Chris@0
|
309 $(window).on('editor:dialogsave', (e, values) => {
|
Chris@0
|
310 if (Drupal.ckeditor.saveCallback) {
|
Chris@0
|
311 Drupal.ckeditor.saveCallback(values);
|
Chris@0
|
312 }
|
Chris@0
|
313 });
|
Chris@0
|
314
|
Chris@0
|
315 // Respond to dialogs that are closed, removing the current save handler.
|
Chris@0
|
316 $(window).on('dialog:afterclose', (e, dialog, $element) => {
|
Chris@0
|
317 if (Drupal.ckeditor.saveCallback) {
|
Chris@0
|
318 Drupal.ckeditor.saveCallback = null;
|
Chris@0
|
319 }
|
Chris@0
|
320 });
|
Chris@0
|
321
|
Chris@0
|
322 // Formulate a default formula for the maximum autoGrow height.
|
Chris@0
|
323 $(document).on('drupalViewportOffsetChange', () => {
|
Chris@17
|
324 CKEDITOR.config.autoGrow_maxHeight =
|
Chris@17
|
325 0.7 *
|
Chris@17
|
326 (window.innerHeight - displace.offsets.top - displace.offsets.bottom);
|
Chris@0
|
327 });
|
Chris@0
|
328
|
Chris@0
|
329 // Redirect on hash change when the original hash has an associated CKEditor.
|
Chris@0
|
330 function redirectTextareaFragmentToCKEditorInstance() {
|
Chris@17
|
331 const hash = window.location.hash.substr(1);
|
Chris@0
|
332 const element = document.getElementById(hash);
|
Chris@0
|
333 if (element) {
|
Chris@0
|
334 const editor = CKEDITOR.dom.element.get(element).getEditor();
|
Chris@0
|
335 if (editor) {
|
Chris@0
|
336 const id = editor.container.getAttribute('id');
|
Chris@17
|
337 window.location.replace(`#${id}`);
|
Chris@0
|
338 }
|
Chris@0
|
339 }
|
Chris@0
|
340 }
|
Chris@17
|
341 $(window).on(
|
Chris@17
|
342 'hashchange.ckeditor',
|
Chris@17
|
343 redirectTextareaFragmentToCKEditorInstance,
|
Chris@17
|
344 );
|
Chris@0
|
345
|
Chris@0
|
346 // Set autoGrow to make the editor grow the moment it is created.
|
Chris@0
|
347 CKEDITOR.config.autoGrow_onStartup = true;
|
Chris@0
|
348
|
Chris@0
|
349 // Set the CKEditor cache-busting string to the same value as Drupal.
|
Chris@0
|
350 CKEDITOR.timestamp = drupalSettings.ckeditor.timestamp;
|
Chris@0
|
351
|
Chris@0
|
352 if (AjaxCommands) {
|
Chris@0
|
353 /**
|
Chris@0
|
354 * Command to add style sheets to a CKEditor instance.
|
Chris@0
|
355 *
|
Chris@0
|
356 * Works for both iframe and inline CKEditor instances.
|
Chris@0
|
357 *
|
Chris@0
|
358 * @param {Drupal.Ajax} [ajax]
|
Chris@0
|
359 * {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
|
Chris@0
|
360 * @param {object} response
|
Chris@0
|
361 * The response from the Ajax request.
|
Chris@0
|
362 * @param {string} response.editor_id
|
Chris@0
|
363 * The CKEditor instance ID.
|
Chris@0
|
364 * @param {number} [status]
|
Chris@0
|
365 * The XMLHttpRequest status.
|
Chris@0
|
366 *
|
Chris@0
|
367 * @see http://docs.ckeditor.com/#!/api/CKEDITOR.dom.document
|
Chris@0
|
368 */
|
Chris@17
|
369 AjaxCommands.prototype.ckeditor_add_stylesheet = function(
|
Chris@17
|
370 ajax,
|
Chris@17
|
371 response,
|
Chris@17
|
372 status,
|
Chris@17
|
373 ) {
|
Chris@0
|
374 const editor = CKEDITOR.instances[response.editor_id];
|
Chris@0
|
375
|
Chris@0
|
376 if (editor) {
|
Chris@17
|
377 response.stylesheets.forEach(url => {
|
Chris@0
|
378 editor.document.appendStyleSheet(url);
|
Chris@0
|
379 });
|
Chris@0
|
380 }
|
Chris@0
|
381 };
|
Chris@0
|
382 }
|
Chris@17
|
383 })(
|
Chris@17
|
384 Drupal,
|
Chris@17
|
385 Drupal.debounce,
|
Chris@17
|
386 CKEDITOR,
|
Chris@17
|
387 jQuery,
|
Chris@17
|
388 Drupal.displace,
|
Chris@17
|
389 Drupal.AjaxCommands,
|
Chris@17
|
390 );
|