3 * CKEditor button and group configuration user interface.
6 (function ($, Drupal, drupalSettings, _) {
10 Drupal.ckeditor = Drupal.ckeditor || {};
13 * Sets config behaviour and creates config views for the CKEditor toolbar.
15 * @type {Drupal~behavior}
17 * @prop {Drupal~behaviorAttach} attach
18 * Attaches admin behaviour to the CKEditor buttons.
19 * @prop {Drupal~behaviorDetach} detach
20 * Detaches admin behaviour from the CKEditor buttons on 'unload'.
22 Drupal.behaviors.ckeditorAdmin = {
23 attach: function (context) {
24 // Process the CKEditor configuration fragment once.
25 var $configurationForm = $(context).find('.ckeditor-toolbar-configuration').once('ckeditor-configuration');
26 if ($configurationForm.length) {
27 var $textarea = $configurationForm
28 // Hide the textarea that contains the serialized representation of the
29 // CKEditor configuration.
30 .find('.js-form-item-editor-settings-toolbar-button-groups')
32 // Return the textarea child node from this expression.
35 // The HTML for the CKEditor configuration is assembled on the server
36 // and sent to the client as a serialized DOM fragment.
37 $configurationForm.append(drupalSettings.ckeditor.toolbarAdmin);
39 // Create a configuration model.
40 var model = Drupal.ckeditor.models.Model = new Drupal.ckeditor.Model({
42 activeEditorConfig: JSON.parse($textarea.val()),
43 hiddenEditorConfig: drupalSettings.ckeditor.hiddenCKEditorConfig
46 // Create the configuration Views.
49 el: $('.ckeditor-toolbar-configuration')
51 Drupal.ckeditor.views = {
52 controller: new Drupal.ckeditor.ControllerView(viewDefaults),
53 visualView: new Drupal.ckeditor.VisualView(viewDefaults),
54 keyboardView: new Drupal.ckeditor.KeyboardView(viewDefaults),
55 auralView: new Drupal.ckeditor.AuralView(viewDefaults)
59 detach: function (context, settings, trigger) {
60 // Early-return if the trigger for detachment is something else than
62 if (trigger !== 'unload') {
66 // We're detaching because CKEditor as text editor has been disabled; this
67 // really means that all CKEditor toolbar buttons have been removed.
68 // Hence,all editor features will be removed, so any reactions from
69 // filters will be undone.
70 var $configurationForm = $(context).find('.ckeditor-toolbar-configuration').findOnce('ckeditor-configuration');
71 if ($configurationForm.length && Drupal.ckeditor.models && Drupal.ckeditor.models.Model) {
72 var config = Drupal.ckeditor.models.Model.toJSON().activeEditorConfig;
73 var buttons = Drupal.ckeditor.views.controller.getButtonList(config);
74 var $activeToolbar = $('.ckeditor-toolbar-configuration').find('.ckeditor-toolbar-active');
75 for (var i = 0; i < buttons.length; i++) {
76 $activeToolbar.trigger('CKEditorToolbarChanged', ['removed', buttons[i]]);
83 * CKEditor configuration UI methods of Backbone objects.
90 * A hash of View instances.
97 * A hash of Model instances.
104 * Translates changes in CKEditor config DOM structure to the config model.
106 * If the button is moved within an existing group, the DOM structure is
107 * simply translated to a configuration model. If the button is moved into a
108 * new group placeholder, then a process is launched to name that group
109 * before the button move is translated into configuration.
111 * @param {Backbone.View} view
112 * The Backbone View that invoked this function.
113 * @param {jQuery} $button
114 * A jQuery set that contains an li element that wraps a button element.
115 * @param {function} callback
116 * A callback to invoke after the button group naming modal dialog has
120 registerButtonMove: function (view, $button, callback) {
121 var $group = $button.closest('.ckeditor-toolbar-group');
123 // If dropped in a placeholder button group, the user must name it.
124 if ($group.hasClass('placeholder')) {
125 if (view.isProcessing) {
128 view.isProcessing = true;
130 Drupal.ckeditor.openGroupNameDialog(view, $group, callback);
133 view.model.set('isDirty', true);
139 * Translates changes in CKEditor config DOM structure to the config model.
141 * Each row has a placeholder group at the end of the row. A user may not
142 * move an existing button group past the placeholder group at the end of a
145 * @param {Backbone.View} view
146 * The Backbone View that invoked this function.
147 * @param {jQuery} $group
148 * A jQuery set that contains an li element that wraps a group of buttons.
150 registerGroupMove: function (view, $group) {
151 // Remove placeholder classes if necessary.
152 var $row = $group.closest('.ckeditor-row');
153 if ($row.hasClass('placeholder')) {
154 $row.removeClass('placeholder');
156 // If there are any rows with just a placeholder group, mark the row as a
158 $row.parent().children().each(function () {
160 if ($row.find('.ckeditor-toolbar-group').not('.placeholder').length === 0) {
161 $row.addClass('placeholder');
164 view.model.set('isDirty', true);
168 * Opens a dialog with a form for changing the title of a button group.
170 * @param {Backbone.View} view
171 * The Backbone View that invoked this function.
172 * @param {jQuery} $group
173 * A jQuery set that contains an li element that wraps a group of buttons.
174 * @param {function} callback
175 * A callback to invoke after the button group naming modal dialog has
178 openGroupNameDialog: function (view, $group, callback) {
179 callback = callback || function () {};
182 * Validates the string provided as a button group title.
184 * @param {HTMLElement} form
185 * The form DOM element that contains the input with the new button
186 * group title string.
189 * Returns true when an error exists, otherwise returns false.
191 function validateForm(form) {
192 if (form.elements[0].value.length === 0) {
194 if (!$form.hasClass('errors')) {
199 .attr('aria-invalid', 'true');
200 $('<div class=\"description\" >' + Drupal.t('Please provide a name for the button group.') + '</div>').insertAfter(form.elements[0]);
208 * Attempts to close the dialog; Validates user input.
210 * @param {string} action
211 * The dialog action chosen by the user: 'apply' or 'cancel'.
212 * @param {HTMLElement} form
213 * The form DOM element that contains the input with the new button
214 * group title string.
216 function closeDialog(action, form) {
219 * Closes the dialog when the user cancels or supplies valid data.
221 function shutdown() {
222 dialog.close(action);
224 // The processing marker can be deleted since the dialog has been
226 delete view.isProcessing;
230 * Applies a string as the name of a CKEditor button group.
232 * @param {jQuery} $group
233 * A jQuery set that contains an li element that wraps a group of
235 * @param {string} name
236 * The new name of the CKEditor button group.
238 function namePlaceholderGroup($group, name) {
239 // If it's currently still a placeholder, then that means we're
240 // creating a new group, and we must do some extra work.
241 if ($group.hasClass('placeholder')) {
242 // Remove all whitespace from the name, lowercase it and ensure
243 // HTML-safe encoding, then use this as the group ID for CKEditor
244 // configuration UI accessibility purposes only.
245 var groupID = 'ckeditor-toolbar-group-aria-label-for-' + Drupal.checkPlain(name.toLowerCase().replace(/\s/g, '-'));
247 // Update the group container.
248 .removeAttr('aria-label')
249 .attr('data-drupal-ckeditor-type', 'group')
251 // Update the group heading.
252 .children('.ckeditor-toolbar-group-name')
255 // Update the group items.
256 .children('.ckeditor-toolbar-group-buttons')
257 .attr('aria-labelledby', groupID);
261 .attr('data-drupal-ckeditor-toolbar-group-name', name)
262 .children('.ckeditor-toolbar-group-name')
266 // Invoke a user-provided callback and indicate failure.
267 if (action === 'cancel') {
269 callback(false, $group);
273 // Validate that a group name was provided.
274 if (form && validateForm(form)) {
278 // React to application of a valid group name.
279 if (action === 'apply') {
281 // Apply the provided name to the button group label.
282 namePlaceholderGroup($group, Drupal.checkPlain(form.elements[0].value));
283 // Remove placeholder classes so that new placeholders will be
285 $group.closest('.ckeditor-row.placeholder').addBack().removeClass('placeholder');
287 // Invoke a user-provided callback and indicate success.
288 callback(true, $group);
290 // Signal that the active toolbar DOM structure has changed.
291 view.model.set('isDirty', true);
295 // Create a Drupal dialog that will get a button group name from the user.
296 var $ckeditorButtonGroupNameForm = $(Drupal.theme('ckeditorButtonGroupNameForm'));
297 var dialog = Drupal.dialog($ckeditorButtonGroupNameForm.get(0), {
298 title: Drupal.t('Button group name'),
299 dialogClass: 'ckeditor-name-toolbar-group',
303 text: Drupal.t('Apply'),
305 closeDialog('apply', this);
310 text: Drupal.t('Cancel'),
312 closeDialog('cancel');
319 var $widget = $form.parent();
320 $widget.find('.ui-dialog-titlebar-close').remove();
321 // Set a click handler on the input and button in the form.
322 $widget.on('keypress.ckeditor', 'input, button', function (event) {
323 // React to enter key press.
324 if (event.keyCode === 13) {
325 var $target = $(event.currentTarget);
326 var data = $target.data('ui-button');
327 var action = 'apply';
328 // Assume 'apply', but take into account that the user might have
329 // pressed the enter key on the dialog buttons.
330 if (data && data.options && data.options.label) {
331 action = data.options.label.toLowerCase();
333 closeDialog(action, form);
334 event.stopPropagation();
335 event.stopImmediatePropagation();
336 event.preventDefault();
339 // Announce to the user that a modal dialog is open.
340 var text = Drupal.t('Editing the name of the new button group in a dialog.');
341 if (typeof $group.attr('data-drupal-ckeditor-toolbar-group-name') !== 'undefined') {
342 text = Drupal.t('Editing the name of the "@groupName" button group in a dialog.', {
343 '@groupName': $group.attr('data-drupal-ckeditor-toolbar-group-name')
346 Drupal.announce(text);
348 close: function (event) {
349 // Automatically destroy the DOM element that was used for the dialog.
350 $(event.target).remove();
353 // A modal dialog is used because the user must provide a button group
354 // name or cancel the button placement before taking any other action.
357 $(document.querySelector('.ckeditor-name-toolbar-group').querySelector('input'))
358 // When editing, set the "group name" input in the form to the current
360 .attr('value', $group.attr('data-drupal-ckeditor-toolbar-group-name'))
361 // Focus on the "group name" input in the form.
368 * Automatically shows/hides settings of buttons-only CKEditor plugins.
370 * @type {Drupal~behavior}
372 * @prop {Drupal~behaviorAttach} attach
373 * Attaches show/hide behaviour to Plugin Settings buttons.
375 Drupal.behaviors.ckeditorAdminButtonPluginSettings = {
376 attach: function (context) {
377 var $context = $(context);
378 var $ckeditorPluginSettings = $context.find('#ckeditor-plugin-settings').once('ckeditor-plugin-settings');
379 if ($ckeditorPluginSettings.length) {
380 // Hide all button-dependent plugin settings initially.
381 $ckeditorPluginSettings.find('[data-ckeditor-buttons]').each(function () {
383 if ($this.data('verticalTab')) {
384 $this.data('verticalTab').tabHide();
387 // On very narrow viewports, Vertical Tabs are disabled.
390 $this.data('ckeditorButtonPluginSettingsActiveButtons', []);
393 // Whenever a button is added or removed, check if we should show or
394 // hide the corresponding plugin settings. (Note that upon
395 // initialization, each button that already is part of the toolbar still
396 // is considered "added", hence it also works correctly for buttons that
397 // were added previously.)
399 .find('.ckeditor-toolbar-active')
400 .off('CKEditorToolbarChanged.ckeditorAdminPluginSettings')
401 .on('CKEditorToolbarChanged.ckeditorAdminPluginSettings', function (event, action, button) {
402 var $pluginSettings = $ckeditorPluginSettings
403 .find('[data-ckeditor-buttons~=' + button + ']');
405 // No settings for this button.
406 if ($pluginSettings.length === 0) {
410 var verticalTab = $pluginSettings.data('verticalTab');
411 var activeButtons = $pluginSettings.data('ckeditorButtonPluginSettingsActiveButtons');
412 if (action === 'added') {
413 activeButtons.push(button);
414 // Show this plugin's settings if >=1 of its buttons are active.
416 verticalTab.tabShow();
419 // On very narrow viewports, Vertical Tabs remain fieldsets.
420 $pluginSettings.show();
425 // Remove this button from the list of active buttons.
426 activeButtons.splice(activeButtons.indexOf(button), 1);
427 // Show this plugin's settings 0 of its buttons are active.
428 if (activeButtons.length === 0) {
430 verticalTab.tabHide();
433 // On very narrow viewports, Vertical Tabs are disabled.
434 $pluginSettings.hide();
438 $pluginSettings.data('ckeditorButtonPluginSettingsActiveButtons', activeButtons);
445 * Themes a blank CKEditor row.
448 * A HTML string for a CKEditor row.
450 Drupal.theme.ckeditorRow = function () {
451 return '<li class="ckeditor-row placeholder" role="group"><ul class="ckeditor-toolbar-groups clearfix"></ul></li>';
455 * Themes a blank CKEditor button group.
458 * A HTML string for a CKEditor button group.
460 Drupal.theme.ckeditorToolbarGroup = function () {
462 group += '<li class="ckeditor-toolbar-group placeholder" role="presentation" aria-label="' + Drupal.t('Place a button to create a new button group.') + '">';
463 group += '<h3 class="ckeditor-toolbar-group-name">' + Drupal.t('New group') + '</h3>';
464 group += '<ul class="ckeditor-buttons ckeditor-toolbar-group-buttons" role="toolbar" data-drupal-ckeditor-button-sorting="target"></ul>';
470 * Themes a form for changing the title of a CKEditor button group.
473 * A HTML string for the form for the title of a CKEditor button group.
475 Drupal.theme.ckeditorButtonGroupNameForm = function () {
476 return '<form><input name="group-name" required="required"></form>';
480 * Themes a button that will toggle the button group names in active config.
483 * A HTML string for the button to toggle group names.
485 Drupal.theme.ckeditorButtonGroupNamesToggle = function () {
486 return '<button class="link ckeditor-groupnames-toggle" aria-pressed="false"></button>';
490 * Themes a button that will prompt the user to name a new button group.
493 * A HTML string for the button to create a name for a new button group.
495 Drupal.theme.ckeditorNewButtonGroup = function () {
496 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>';
499 })(jQuery, Drupal, drupalSettings, _);