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)
23 .find('.ckeditor-toolbar-configuration')
24 .once('ckeditor-configuration');
25 if ($configurationForm.length) {
26 const $textarea = $configurationForm
27 // Hide the textarea that contains the serialized representation of the
28 // CKEditor configuration.
29 .find('.js-form-item-editor-settings-toolbar-button-groups')
31 // Return the textarea child node from this expression.
34 // The HTML for the CKEditor configuration is assembled on the server
35 // and sent to the client as a serialized DOM fragment.
36 $configurationForm.append(drupalSettings.ckeditor.toolbarAdmin);
38 // Create a configuration model.
39 Drupal.ckeditor.models.Model = new Drupal.ckeditor.Model({
41 activeEditorConfig: JSON.parse($textarea.val()),
42 hiddenEditorConfig: drupalSettings.ckeditor.hiddenCKEditorConfig,
45 // Create the configuration Views.
46 const viewDefaults = {
47 model: Drupal.ckeditor.models.Model,
48 el: $('.ckeditor-toolbar-configuration'),
50 Drupal.ckeditor.views = {
51 controller: new Drupal.ckeditor.ControllerView(viewDefaults),
52 visualView: new Drupal.ckeditor.VisualView(viewDefaults),
53 keyboardView: new Drupal.ckeditor.KeyboardView(viewDefaults),
54 auralView: new Drupal.ckeditor.AuralView(viewDefaults),
58 detach(context, settings, trigger) {
59 // Early-return if the trigger for detachment is something else than
61 if (trigger !== 'unload') {
65 // We're detaching because CKEditor as text editor has been disabled; this
66 // really means that all CKEditor toolbar buttons have been removed.
67 // Hence,all editor features will be removed, so any reactions from
68 // filters will be undone.
69 const $configurationForm = $(context)
70 .find('.ckeditor-toolbar-configuration')
71 .findOnce('ckeditor-configuration');
73 $configurationForm.length &&
74 Drupal.ckeditor.models &&
75 Drupal.ckeditor.models.Model
77 const config = Drupal.ckeditor.models.Model.toJSON().activeEditorConfig;
78 const buttons = Drupal.ckeditor.views.controller.getButtonList(config);
79 const $activeToolbar = $('.ckeditor-toolbar-configuration').find(
80 '.ckeditor-toolbar-active',
82 for (let i = 0; i < buttons.length; i++) {
83 $activeToolbar.trigger('CKEditorToolbarChanged', [
93 * CKEditor configuration UI methods of Backbone objects.
99 * A hash of View instances.
106 * A hash of Model instances.
113 * Translates changes in CKEditor config DOM structure to the config model.
115 * If the button is moved within an existing group, the DOM structure is
116 * simply translated to a configuration model. If the button is moved into a
117 * new group placeholder, then a process is launched to name that group
118 * before the button move is translated into configuration.
120 * @param {Backbone.View} view
121 * The Backbone View that invoked this function.
122 * @param {jQuery} $button
123 * A jQuery set that contains an li element that wraps a button element.
124 * @param {function} callback
125 * A callback to invoke after the button group naming modal dialog has
129 registerButtonMove(view, $button, callback) {
130 const $group = $button.closest('.ckeditor-toolbar-group');
132 // If dropped in a placeholder button group, the user must name it.
133 if ($group.hasClass('placeholder')) {
134 if (view.isProcessing) {
137 view.isProcessing = true;
139 Drupal.ckeditor.openGroupNameDialog(view, $group, callback);
141 view.model.set('isDirty', true);
147 * Translates changes in CKEditor config DOM structure to the config model.
149 * Each row has a placeholder group at the end of the row. A user may not
150 * move an existing button group past the placeholder group at the end of a
153 * @param {Backbone.View} view
154 * The Backbone View that invoked this function.
155 * @param {jQuery} $group
156 * A jQuery set that contains an li element that wraps a group of buttons.
158 registerGroupMove(view, $group) {
159 // Remove placeholder classes if necessary.
160 let $row = $group.closest('.ckeditor-row');
161 if ($row.hasClass('placeholder')) {
162 $row.removeClass('placeholder');
164 // If there are any rows with just a placeholder group, mark the row as a
172 $row.find('.ckeditor-toolbar-group').not('.placeholder').length ===
175 $row.addClass('placeholder');
178 view.model.set('isDirty', true);
182 * Opens a dialog with a form for changing the title of a button group.
184 * @param {Backbone.View} view
185 * The Backbone View that invoked this function.
186 * @param {jQuery} $group
187 * A jQuery set that contains an li element that wraps a group of buttons.
188 * @param {function} callback
189 * A callback to invoke after the button group naming modal dialog has
192 openGroupNameDialog(view, $group, callback) {
193 callback = callback || function() {};
196 * Validates the string provided as a button group title.
198 * @param {HTMLElement} form
199 * The form DOM element that contains the input with the new button
200 * group title string.
203 * Returns true when an error exists, otherwise returns false.
205 function validateForm(form) {
206 if (form.elements[0].value.length === 0) {
207 const $form = $(form);
208 if (!$form.hasClass('errors')) {
213 .attr('aria-invalid', 'true');
215 `<div class="description" >${Drupal.t(
216 'Please provide a name for the button group.',
218 ).insertAfter(form.elements[0]);
226 * Attempts to close the dialog; Validates user input.
228 * @param {string} action
229 * The dialog action chosen by the user: 'apply' or 'cancel'.
230 * @param {HTMLElement} form
231 * The form DOM element that contains the input with the new button
232 * group title string.
234 function closeDialog(action, form) {
236 * Closes the dialog when the user cancels or supplies valid data.
238 function shutdown() {
239 // eslint-disable-next-line no-use-before-define
240 dialog.close(action);
242 // The processing marker can be deleted since the dialog has been
244 delete view.isProcessing;
248 * Applies a string as the name of a CKEditor button group.
250 * @param {jQuery} $group
251 * A jQuery set that contains an li element that wraps a group of
253 * @param {string} name
254 * The new name of the CKEditor button group.
256 function namePlaceholderGroup($group, name) {
257 // If it's currently still a placeholder, then that means we're
258 // creating a new group, and we must do some extra work.
259 if ($group.hasClass('placeholder')) {
260 // Remove all whitespace from the name, lowercase it and ensure
261 // HTML-safe encoding, then use this as the group ID for CKEditor
262 // configuration UI accessibility purposes only.
263 const groupID = `ckeditor-toolbar-group-aria-label-for-${Drupal.checkPlain(
264 name.toLowerCase().replace(/\s/g, '-'),
267 // Update the group container.
268 .removeAttr('aria-label')
269 .attr('data-drupal-ckeditor-type', 'group')
271 // Update the group heading.
272 .children('.ckeditor-toolbar-group-name')
275 // Update the group items.
276 .children('.ckeditor-toolbar-group-buttons')
277 .attr('aria-labelledby', groupID);
281 .attr('data-drupal-ckeditor-toolbar-group-name', name)
282 .children('.ckeditor-toolbar-group-name')
286 // Invoke a user-provided callback and indicate failure.
287 if (action === 'cancel') {
289 callback(false, $group);
293 // Validate that a group name was provided.
294 if (form && validateForm(form)) {
298 // React to application of a valid group name.
299 if (action === 'apply') {
301 // Apply the provided name to the button group label.
302 namePlaceholderGroup(
304 Drupal.checkPlain(form.elements[0].value),
306 // Remove placeholder classes so that new placeholders will be
309 .closest('.ckeditor-row.placeholder')
311 .removeClass('placeholder');
313 // Invoke a user-provided callback and indicate success.
314 callback(true, $group);
316 // Signal that the active toolbar DOM structure has changed.
317 view.model.set('isDirty', true);
321 // Create a Drupal dialog that will get a button group name from the user.
322 const $ckeditorButtonGroupNameForm = $(
323 Drupal.theme('ckeditorButtonGroupNameForm'),
325 const dialog = Drupal.dialog($ckeditorButtonGroupNameForm.get(0), {
326 title: Drupal.t('Button group name'),
327 dialogClass: 'ckeditor-name-toolbar-group',
331 text: Drupal.t('Apply'),
333 closeDialog('apply', this);
338 text: Drupal.t('Cancel'),
340 closeDialog('cancel');
346 const $form = $(this);
347 const $widget = $form.parent();
348 $widget.find('.ui-dialog-titlebar-close').remove();
349 // Set a click handler on the input and button in the form.
350 $widget.on('keypress.ckeditor', 'input, button', event => {
351 // React to enter key press.
352 if (event.keyCode === 13) {
353 const $target = $(event.currentTarget);
354 const data = $target.data('ui-button');
355 let action = 'apply';
356 // Assume 'apply', but take into account that the user might have
357 // pressed the enter key on the dialog buttons.
358 if (data && data.options && data.options.label) {
359 action = data.options.label.toLowerCase();
361 closeDialog(action, form);
362 event.stopPropagation();
363 event.stopImmediatePropagation();
364 event.preventDefault();
367 // Announce to the user that a modal dialog is open.
369 'Editing the name of the new button group in a dialog.',
372 typeof $group.attr('data-drupal-ckeditor-toolbar-group-name') !==
376 'Editing the name of the "@groupName" button group in a dialog.',
378 '@groupName': $group.attr(
379 'data-drupal-ckeditor-toolbar-group-name',
384 Drupal.announce(text);
387 // Automatically destroy the DOM element that was used for the dialog.
388 $(event.target).remove();
392 // A modal dialog is used because the user must provide a button group
393 // name or cancel the button placement before taking any other action.
398 .querySelector('.ckeditor-name-toolbar-group')
399 .querySelector('input'),
401 // When editing, set the "group name" input in the form to the current
403 .attr('value', $group.attr('data-drupal-ckeditor-toolbar-group-name'))
404 // Focus on the "group name" input in the form.
410 * Automatically shows/hides settings of buttons-only CKEditor plugins.
412 * @type {Drupal~behavior}
414 * @prop {Drupal~behaviorAttach} attach
415 * Attaches show/hide behaviour to Plugin Settings buttons.
417 Drupal.behaviors.ckeditorAdminButtonPluginSettings = {
419 const $context = $(context);
420 const $ckeditorPluginSettings = $context
421 .find('#ckeditor-plugin-settings')
422 .once('ckeditor-plugin-settings');
423 if ($ckeditorPluginSettings.length) {
424 // Hide all button-dependent plugin settings initially.
425 $ckeditorPluginSettings
426 .find('[data-ckeditor-buttons]')
428 const $this = $(this);
429 if ($this.data('verticalTab')) {
430 $this.data('verticalTab').tabHide();
432 // On very narrow viewports, Vertical Tabs are disabled.
435 $this.data('ckeditorButtonPluginSettingsActiveButtons', []);
438 // Whenever a button is added or removed, check if we should show or
439 // hide the corresponding plugin settings. (Note that upon
440 // initialization, each button that already is part of the toolbar still
441 // is considered "added", hence it also works correctly for buttons that
442 // were added previously.)
444 .find('.ckeditor-toolbar-active')
445 .off('CKEditorToolbarChanged.ckeditorAdminPluginSettings')
447 'CKEditorToolbarChanged.ckeditorAdminPluginSettings',
448 (event, action, button) => {
449 const $pluginSettings = $ckeditorPluginSettings.find(
450 `[data-ckeditor-buttons~=${button}]`,
453 // No settings for this button.
454 if ($pluginSettings.length === 0) {
458 const verticalTab = $pluginSettings.data('verticalTab');
459 const activeButtons = $pluginSettings.data(
460 'ckeditorButtonPluginSettingsActiveButtons',
462 if (action === 'added') {
463 activeButtons.push(button);
464 // Show this plugin's settings if >=1 of its buttons are active.
466 verticalTab.tabShow();
468 // On very narrow viewports, Vertical Tabs remain fieldsets.
469 $pluginSettings.show();
472 // Remove this button from the list of active buttons.
473 activeButtons.splice(activeButtons.indexOf(button), 1);
474 // Show this plugin's settings 0 of its buttons are active.
475 if (activeButtons.length === 0) {
477 verticalTab.tabHide();
479 // On very narrow viewports, Vertical Tabs are disabled.
480 $pluginSettings.hide();
484 $pluginSettings.data(
485 'ckeditorButtonPluginSettingsActiveButtons',
495 * Themes a blank CKEditor row.
498 * A HTML string for a CKEditor row.
500 Drupal.theme.ckeditorRow = function() {
501 return '<li class="ckeditor-row placeholder" role="group"><ul class="ckeditor-toolbar-groups clearfix"></ul></li>';
505 * Themes a blank CKEditor button group.
508 * A HTML string for a CKEditor button group.
510 Drupal.theme.ckeditorToolbarGroup = function() {
512 group += `<li class="ckeditor-toolbar-group placeholder" role="presentation" aria-label="${Drupal.t(
513 'Place a button to create a new button group.',
515 group += `<h3 class="ckeditor-toolbar-group-name">${Drupal.t(
519 '<ul class="ckeditor-buttons ckeditor-toolbar-group-buttons" role="toolbar" data-drupal-ckeditor-button-sorting="target"></ul>';
525 * Themes a form for changing the title of a CKEditor button group.
528 * A HTML string for the form for the title of a CKEditor button group.
530 Drupal.theme.ckeditorButtonGroupNameForm = function() {
531 return '<form><input name="group-name" required="required"></form>';
535 * Themes a button that will toggle the button group names in active config.
538 * A HTML string for the button to toggle group names.
540 Drupal.theme.ckeditorButtonGroupNamesToggle = function() {
541 return '<button class="link ckeditor-groupnames-toggle" aria-pressed="false"></button>';
545 * Themes a button that will prompt the user to name a new button group.
548 * A HTML string for the button to create a name for a new button group.
550 Drupal.theme.ckeditorNewButtonGroup = function() {
551 return `<li class="ckeditor-add-new-group"><button aria-label="${Drupal.t(
552 'Add a CKEditor button group to the end of this row.',
553 )}">${Drupal.t('Add group')}</button></li>`;
555 })(jQuery, Drupal, drupalSettings, _);