3 * CKEditor button and group configuration user interface.
6 (function ($, Drupal, drupalSettings, _) {
7 Drupal.ckeditor = Drupal.ckeditor || {};
10 * Sets config behaviour and creates config views for the CKEditor toolbar.
12 * @type {Drupal~behavior}
14 * @prop {Drupal~behaviorAttach} attach
15 * Attaches admin behaviour to the CKEditor buttons.
16 * @prop {Drupal~behaviorDetach} detach
17 * Detaches admin behaviour from the CKEditor buttons on 'unload'.
19 Drupal.behaviors.ckeditorAdmin = {
21 // Process the CKEditor configuration fragment once.
22 const $configurationForm = $(context).find('.ckeditor-toolbar-configuration').once('ckeditor-configuration');
23 if ($configurationForm.length) {
24 const $textarea = $configurationForm
25 // Hide the textarea that contains the serialized representation of the
26 // CKEditor configuration.
27 .find('.js-form-item-editor-settings-toolbar-button-groups')
29 // Return the textarea child node from this expression.
32 // The HTML for the CKEditor configuration is assembled on the server
33 // and sent to the client as a serialized DOM fragment.
34 $configurationForm.append(drupalSettings.ckeditor.toolbarAdmin);
36 // Create a configuration model.
37 Drupal.ckeditor.models.Model = new Drupal.ckeditor.Model({
39 activeEditorConfig: JSON.parse($textarea.val()),
40 hiddenEditorConfig: drupalSettings.ckeditor.hiddenCKEditorConfig,
43 // Create the configuration Views.
44 const viewDefaults = {
45 model: Drupal.ckeditor.models.Model,
46 el: $('.ckeditor-toolbar-configuration'),
48 Drupal.ckeditor.views = {
49 controller: new Drupal.ckeditor.ControllerView(viewDefaults),
50 visualView: new Drupal.ckeditor.VisualView(viewDefaults),
51 keyboardView: new Drupal.ckeditor.KeyboardView(viewDefaults),
52 auralView: new Drupal.ckeditor.AuralView(viewDefaults),
56 detach(context, settings, trigger) {
57 // Early-return if the trigger for detachment is something else than
59 if (trigger !== 'unload') {
63 // We're detaching because CKEditor as text editor has been disabled; this
64 // really means that all CKEditor toolbar buttons have been removed.
65 // Hence,all editor features will be removed, so any reactions from
66 // filters will be undone.
67 const $configurationForm = $(context).find('.ckeditor-toolbar-configuration').findOnce('ckeditor-configuration');
68 if ($configurationForm.length && Drupal.ckeditor.models && Drupal.ckeditor.models.Model) {
69 const config = Drupal.ckeditor.models.Model.toJSON().activeEditorConfig;
70 const buttons = Drupal.ckeditor.views.controller.getButtonList(config);
71 const $activeToolbar = $('.ckeditor-toolbar-configuration').find('.ckeditor-toolbar-active');
72 for (let i = 0; i < buttons.length; i++) {
73 $activeToolbar.trigger('CKEditorToolbarChanged', ['removed', buttons[i]]);
80 * CKEditor configuration UI methods of Backbone objects.
87 * A hash of View instances.
94 * A hash of Model instances.
101 * Translates changes in CKEditor config DOM structure to the config model.
103 * If the button is moved within an existing group, the DOM structure is
104 * simply translated to a configuration model. If the button is moved into a
105 * new group placeholder, then a process is launched to name that group
106 * before the button move is translated into configuration.
108 * @param {Backbone.View} view
109 * The Backbone View that invoked this function.
110 * @param {jQuery} $button
111 * A jQuery set that contains an li element that wraps a button element.
112 * @param {function} callback
113 * A callback to invoke after the button group naming modal dialog has
117 registerButtonMove(view, $button, callback) {
118 const $group = $button.closest('.ckeditor-toolbar-group');
120 // If dropped in a placeholder button group, the user must name it.
121 if ($group.hasClass('placeholder')) {
122 if (view.isProcessing) {
125 view.isProcessing = true;
127 Drupal.ckeditor.openGroupNameDialog(view, $group, callback);
130 view.model.set('isDirty', true);
136 * Translates changes in CKEditor config DOM structure to the config model.
138 * Each row has a placeholder group at the end of the row. A user may not
139 * move an existing button group past the placeholder group at the end of a
142 * @param {Backbone.View} view
143 * The Backbone View that invoked this function.
144 * @param {jQuery} $group
145 * A jQuery set that contains an li element that wraps a group of buttons.
147 registerGroupMove(view, $group) {
148 // Remove placeholder classes if necessary.
149 let $row = $group.closest('.ckeditor-row');
150 if ($row.hasClass('placeholder')) {
151 $row.removeClass('placeholder');
153 // If there are any rows with just a placeholder group, mark the row as a
155 $row.parent().children().each(function () {
157 if ($row.find('.ckeditor-toolbar-group').not('.placeholder').length === 0) {
158 $row.addClass('placeholder');
161 view.model.set('isDirty', true);
165 * Opens a dialog with a form for changing the title of a button group.
167 * @param {Backbone.View} view
168 * The Backbone View that invoked this function.
169 * @param {jQuery} $group
170 * A jQuery set that contains an li element that wraps a group of buttons.
171 * @param {function} callback
172 * A callback to invoke after the button group naming modal dialog has
175 openGroupNameDialog(view, $group, callback) {
176 callback = callback || function () {};
179 * Validates the string provided as a button group title.
181 * @param {HTMLElement} form
182 * The form DOM element that contains the input with the new button
183 * group title string.
186 * Returns true when an error exists, otherwise returns false.
188 function validateForm(form) {
189 if (form.elements[0].value.length === 0) {
190 const $form = $(form);
191 if (!$form.hasClass('errors')) {
196 .attr('aria-invalid', 'true');
197 $(`<div class="description" >${Drupal.t('Please provide a name for the button group.')}</div>`).insertAfter(form.elements[0]);
205 * Attempts to close the dialog; Validates user input.
207 * @param {string} action
208 * The dialog action chosen by the user: 'apply' or 'cancel'.
209 * @param {HTMLElement} form
210 * The form DOM element that contains the input with the new button
211 * group title string.
213 function closeDialog(action, form) {
215 * Closes the dialog when the user cancels or supplies valid data.
217 function shutdown() {
218 dialog.close(action);
220 // The processing marker can be deleted since the dialog has been
222 delete view.isProcessing;
226 * Applies a string as the name of a CKEditor button group.
228 * @param {jQuery} $group
229 * A jQuery set that contains an li element that wraps a group of
231 * @param {string} name
232 * The new name of the CKEditor button group.
234 function namePlaceholderGroup($group, name) {
235 // If it's currently still a placeholder, then that means we're
236 // creating a new group, and we must do some extra work.
237 if ($group.hasClass('placeholder')) {
238 // Remove all whitespace from the name, lowercase it and ensure
239 // HTML-safe encoding, then use this as the group ID for CKEditor
240 // configuration UI accessibility purposes only.
241 const groupID = `ckeditor-toolbar-group-aria-label-for-${Drupal.checkPlain(name.toLowerCase().replace(/\s/g, '-'))}`;
243 // Update the group container.
244 .removeAttr('aria-label')
245 .attr('data-drupal-ckeditor-type', 'group')
247 // Update the group heading.
248 .children('.ckeditor-toolbar-group-name')
251 // Update the group items.
252 .children('.ckeditor-toolbar-group-buttons')
253 .attr('aria-labelledby', groupID);
257 .attr('data-drupal-ckeditor-toolbar-group-name', name)
258 .children('.ckeditor-toolbar-group-name')
262 // Invoke a user-provided callback and indicate failure.
263 if (action === 'cancel') {
265 callback(false, $group);
269 // Validate that a group name was provided.
270 if (form && validateForm(form)) {
274 // React to application of a valid group name.
275 if (action === 'apply') {
277 // Apply the provided name to the button group label.
278 namePlaceholderGroup($group, Drupal.checkPlain(form.elements[0].value));
279 // Remove placeholder classes so that new placeholders will be
281 $group.closest('.ckeditor-row.placeholder').addBack().removeClass('placeholder');
283 // Invoke a user-provided callback and indicate success.
284 callback(true, $group);
286 // Signal that the active toolbar DOM structure has changed.
287 view.model.set('isDirty', true);
291 // Create a Drupal dialog that will get a button group name from the user.
292 const $ckeditorButtonGroupNameForm = $(Drupal.theme('ckeditorButtonGroupNameForm'));
293 const dialog = Drupal.dialog($ckeditorButtonGroupNameForm.get(0), {
294 title: Drupal.t('Button group name'),
295 dialogClass: 'ckeditor-name-toolbar-group',
299 text: Drupal.t('Apply'),
301 closeDialog('apply', this);
306 text: Drupal.t('Cancel'),
308 closeDialog('cancel');
314 const $form = $(this);
315 const $widget = $form.parent();
316 $widget.find('.ui-dialog-titlebar-close').remove();
317 // Set a click handler on the input and button in the form.
318 $widget.on('keypress.ckeditor', 'input, button', (event) => {
319 // React to enter key press.
320 if (event.keyCode === 13) {
321 const $target = $(event.currentTarget);
322 const data = $target.data('ui-button');
323 let action = 'apply';
324 // Assume 'apply', but take into account that the user might have
325 // pressed the enter key on the dialog buttons.
326 if (data && data.options && data.options.label) {
327 action = data.options.label.toLowerCase();
329 closeDialog(action, form);
330 event.stopPropagation();
331 event.stopImmediatePropagation();
332 event.preventDefault();
335 // Announce to the user that a modal dialog is open.
336 let text = Drupal.t('Editing the name of the new button group in a dialog.');
337 if (typeof $group.attr('data-drupal-ckeditor-toolbar-group-name') !== 'undefined') {
338 text = Drupal.t('Editing the name of the "@groupName" button group in a dialog.', {
339 '@groupName': $group.attr('data-drupal-ckeditor-toolbar-group-name'),
342 Drupal.announce(text);
345 // Automatically destroy the DOM element that was used for the dialog.
346 $(event.target).remove();
349 // A modal dialog is used because the user must provide a button group
350 // name or cancel the button placement before taking any other action.
353 $(document.querySelector('.ckeditor-name-toolbar-group').querySelector('input'))
354 // When editing, set the "group name" input in the form to the current
356 .attr('value', $group.attr('data-drupal-ckeditor-toolbar-group-name'))
357 // Focus on the "group name" input in the form.
364 * Automatically shows/hides settings of buttons-only CKEditor plugins.
366 * @type {Drupal~behavior}
368 * @prop {Drupal~behaviorAttach} attach
369 * Attaches show/hide behaviour to Plugin Settings buttons.
371 Drupal.behaviors.ckeditorAdminButtonPluginSettings = {
373 const $context = $(context);
374 const $ckeditorPluginSettings = $context.find('#ckeditor-plugin-settings').once('ckeditor-plugin-settings');
375 if ($ckeditorPluginSettings.length) {
376 // Hide all button-dependent plugin settings initially.
377 $ckeditorPluginSettings.find('[data-ckeditor-buttons]').each(function () {
378 const $this = $(this);
379 if ($this.data('verticalTab')) {
380 $this.data('verticalTab').tabHide();
383 // On very narrow viewports, Vertical Tabs are disabled.
386 $this.data('ckeditorButtonPluginSettingsActiveButtons', []);
389 // Whenever a button is added or removed, check if we should show or
390 // hide the corresponding plugin settings. (Note that upon
391 // initialization, each button that already is part of the toolbar still
392 // is considered "added", hence it also works correctly for buttons that
393 // were added previously.)
395 .find('.ckeditor-toolbar-active')
396 .off('CKEditorToolbarChanged.ckeditorAdminPluginSettings')
397 .on('CKEditorToolbarChanged.ckeditorAdminPluginSettings', (event, action, button) => {
398 const $pluginSettings = $ckeditorPluginSettings
399 .find(`[data-ckeditor-buttons~=${button}]`);
401 // No settings for this button.
402 if ($pluginSettings.length === 0) {
406 const verticalTab = $pluginSettings.data('verticalTab');
407 const activeButtons = $pluginSettings.data('ckeditorButtonPluginSettingsActiveButtons');
408 if (action === 'added') {
409 activeButtons.push(button);
410 // Show this plugin's settings if >=1 of its buttons are active.
412 verticalTab.tabShow();
415 // On very narrow viewports, Vertical Tabs remain fieldsets.
416 $pluginSettings.show();
420 // Remove this button from the list of active buttons.
421 activeButtons.splice(activeButtons.indexOf(button), 1);
422 // Show this plugin's settings 0 of its buttons are active.
423 if (activeButtons.length === 0) {
425 verticalTab.tabHide();
428 // On very narrow viewports, Vertical Tabs are disabled.
429 $pluginSettings.hide();
433 $pluginSettings.data('ckeditorButtonPluginSettingsActiveButtons', activeButtons);
440 * Themes a blank CKEditor row.
443 * A HTML string for a CKEditor row.
445 Drupal.theme.ckeditorRow = function () {
446 return '<li class="ckeditor-row placeholder" role="group"><ul class="ckeditor-toolbar-groups clearfix"></ul></li>';
450 * Themes a blank CKEditor button group.
453 * A HTML string for a CKEditor button group.
455 Drupal.theme.ckeditorToolbarGroup = function () {
457 group += `<li class="ckeditor-toolbar-group placeholder" role="presentation" aria-label="${Drupal.t('Place a button to create a new button group.')}">`;
458 group += `<h3 class="ckeditor-toolbar-group-name">${Drupal.t('New group')}</h3>`;
459 group += '<ul class="ckeditor-buttons ckeditor-toolbar-group-buttons" role="toolbar" data-drupal-ckeditor-button-sorting="target"></ul>';
465 * Themes a form for changing the title of a CKEditor button group.
468 * A HTML string for the form for the title of a CKEditor button group.
470 Drupal.theme.ckeditorButtonGroupNameForm = function () {
471 return '<form><input name="group-name" required="required"></form>';
475 * Themes a button that will toggle the button group names in active config.
478 * A HTML string for the button to toggle group names.
480 Drupal.theme.ckeditorButtonGroupNamesToggle = function () {
481 return '<button class="link ckeditor-groupnames-toggle" aria-pressed="false"></button>';
485 * Themes a button that will prompt the user to name a new button group.
488 * A HTML string for the button to create a name for a new button group.
490 Drupal.theme.ckeditorNewButtonGroup = function () {
491 return `<li class="ckeditor-add-new-group"><button aria-label="${Drupal.t('Add a CKEditor button group to the end of this row.')}">${Drupal.t('Add group')}</button></li>`;
493 }(jQuery, Drupal, drupalSettings, _));