annotate core/modules/ckeditor/js/ckeditor.admin.es6.js @ 2:5311817fb629

Theme updates
author Chris Cannam
date Tue, 10 Jul 2018 13:19:18 +0000
parents c75dbcec494b
children a9cd425dd02b
rev   line source
Chris@0 1 /**
Chris@0 2 * @file
Chris@0 3 * CKEditor button and group configuration user interface.
Chris@0 4 */
Chris@0 5
Chris@0 6 (function ($, Drupal, drupalSettings, _) {
Chris@0 7 Drupal.ckeditor = Drupal.ckeditor || {};
Chris@0 8
Chris@0 9 /**
Chris@0 10 * Sets config behaviour and creates config views for the CKEditor toolbar.
Chris@0 11 *
Chris@0 12 * @type {Drupal~behavior}
Chris@0 13 *
Chris@0 14 * @prop {Drupal~behaviorAttach} attach
Chris@0 15 * Attaches admin behaviour to the CKEditor buttons.
Chris@0 16 * @prop {Drupal~behaviorDetach} detach
Chris@0 17 * Detaches admin behaviour from the CKEditor buttons on 'unload'.
Chris@0 18 */
Chris@0 19 Drupal.behaviors.ckeditorAdmin = {
Chris@0 20 attach(context) {
Chris@0 21 // Process the CKEditor configuration fragment once.
Chris@0 22 const $configurationForm = $(context).find('.ckeditor-toolbar-configuration').once('ckeditor-configuration');
Chris@0 23 if ($configurationForm.length) {
Chris@0 24 const $textarea = $configurationForm
Chris@0 25 // Hide the textarea that contains the serialized representation of the
Chris@0 26 // CKEditor configuration.
Chris@0 27 .find('.js-form-item-editor-settings-toolbar-button-groups')
Chris@0 28 .hide()
Chris@0 29 // Return the textarea child node from this expression.
Chris@0 30 .find('textarea');
Chris@0 31
Chris@0 32 // The HTML for the CKEditor configuration is assembled on the server
Chris@0 33 // and sent to the client as a serialized DOM fragment.
Chris@0 34 $configurationForm.append(drupalSettings.ckeditor.toolbarAdmin);
Chris@0 35
Chris@0 36 // Create a configuration model.
Chris@0 37 Drupal.ckeditor.models.Model = new Drupal.ckeditor.Model({
Chris@0 38 $textarea,
Chris@0 39 activeEditorConfig: JSON.parse($textarea.val()),
Chris@0 40 hiddenEditorConfig: drupalSettings.ckeditor.hiddenCKEditorConfig,
Chris@0 41 });
Chris@0 42
Chris@0 43 // Create the configuration Views.
Chris@0 44 const viewDefaults = {
Chris@0 45 model: Drupal.ckeditor.models.Model,
Chris@0 46 el: $('.ckeditor-toolbar-configuration'),
Chris@0 47 };
Chris@0 48 Drupal.ckeditor.views = {
Chris@0 49 controller: new Drupal.ckeditor.ControllerView(viewDefaults),
Chris@0 50 visualView: new Drupal.ckeditor.VisualView(viewDefaults),
Chris@0 51 keyboardView: new Drupal.ckeditor.KeyboardView(viewDefaults),
Chris@0 52 auralView: new Drupal.ckeditor.AuralView(viewDefaults),
Chris@0 53 };
Chris@0 54 }
Chris@0 55 },
Chris@0 56 detach(context, settings, trigger) {
Chris@0 57 // Early-return if the trigger for detachment is something else than
Chris@0 58 // unload.
Chris@0 59 if (trigger !== 'unload') {
Chris@0 60 return;
Chris@0 61 }
Chris@0 62
Chris@0 63 // We're detaching because CKEditor as text editor has been disabled; this
Chris@0 64 // really means that all CKEditor toolbar buttons have been removed.
Chris@0 65 // Hence,all editor features will be removed, so any reactions from
Chris@0 66 // filters will be undone.
Chris@0 67 const $configurationForm = $(context).find('.ckeditor-toolbar-configuration').findOnce('ckeditor-configuration');
Chris@0 68 if ($configurationForm.length && Drupal.ckeditor.models && Drupal.ckeditor.models.Model) {
Chris@0 69 const config = Drupal.ckeditor.models.Model.toJSON().activeEditorConfig;
Chris@0 70 const buttons = Drupal.ckeditor.views.controller.getButtonList(config);
Chris@0 71 const $activeToolbar = $('.ckeditor-toolbar-configuration').find('.ckeditor-toolbar-active');
Chris@0 72 for (let i = 0; i < buttons.length; i++) {
Chris@0 73 $activeToolbar.trigger('CKEditorToolbarChanged', ['removed', buttons[i]]);
Chris@0 74 }
Chris@0 75 }
Chris@0 76 },
Chris@0 77 };
Chris@0 78
Chris@0 79 /**
Chris@0 80 * CKEditor configuration UI methods of Backbone objects.
Chris@0 81 *
Chris@0 82 * @namespace
Chris@0 83 */
Chris@0 84 Drupal.ckeditor = {
Chris@0 85
Chris@0 86 /**
Chris@0 87 * A hash of View instances.
Chris@0 88 *
Chris@0 89 * @type {object}
Chris@0 90 */
Chris@0 91 views: {},
Chris@0 92
Chris@0 93 /**
Chris@0 94 * A hash of Model instances.
Chris@0 95 *
Chris@0 96 * @type {object}
Chris@0 97 */
Chris@0 98 models: {},
Chris@0 99
Chris@0 100 /**
Chris@0 101 * Translates changes in CKEditor config DOM structure to the config model.
Chris@0 102 *
Chris@0 103 * If the button is moved within an existing group, the DOM structure is
Chris@0 104 * simply translated to a configuration model. If the button is moved into a
Chris@0 105 * new group placeholder, then a process is launched to name that group
Chris@0 106 * before the button move is translated into configuration.
Chris@0 107 *
Chris@0 108 * @param {Backbone.View} view
Chris@0 109 * The Backbone View that invoked this function.
Chris@0 110 * @param {jQuery} $button
Chris@0 111 * A jQuery set that contains an li element that wraps a button element.
Chris@0 112 * @param {function} callback
Chris@0 113 * A callback to invoke after the button group naming modal dialog has
Chris@0 114 * been closed.
Chris@0 115 *
Chris@0 116 */
Chris@0 117 registerButtonMove(view, $button, callback) {
Chris@0 118 const $group = $button.closest('.ckeditor-toolbar-group');
Chris@0 119
Chris@0 120 // If dropped in a placeholder button group, the user must name it.
Chris@0 121 if ($group.hasClass('placeholder')) {
Chris@0 122 if (view.isProcessing) {
Chris@0 123 return;
Chris@0 124 }
Chris@0 125 view.isProcessing = true;
Chris@0 126
Chris@0 127 Drupal.ckeditor.openGroupNameDialog(view, $group, callback);
Chris@0 128 }
Chris@0 129 else {
Chris@0 130 view.model.set('isDirty', true);
Chris@0 131 callback(true);
Chris@0 132 }
Chris@0 133 },
Chris@0 134
Chris@0 135 /**
Chris@0 136 * Translates changes in CKEditor config DOM structure to the config model.
Chris@0 137 *
Chris@0 138 * Each row has a placeholder group at the end of the row. A user may not
Chris@0 139 * move an existing button group past the placeholder group at the end of a
Chris@0 140 * row.
Chris@0 141 *
Chris@0 142 * @param {Backbone.View} view
Chris@0 143 * The Backbone View that invoked this function.
Chris@0 144 * @param {jQuery} $group
Chris@0 145 * A jQuery set that contains an li element that wraps a group of buttons.
Chris@0 146 */
Chris@0 147 registerGroupMove(view, $group) {
Chris@0 148 // Remove placeholder classes if necessary.
Chris@0 149 let $row = $group.closest('.ckeditor-row');
Chris@0 150 if ($row.hasClass('placeholder')) {
Chris@0 151 $row.removeClass('placeholder');
Chris@0 152 }
Chris@0 153 // If there are any rows with just a placeholder group, mark the row as a
Chris@0 154 // placeholder.
Chris@0 155 $row.parent().children().each(function () {
Chris@0 156 $row = $(this);
Chris@0 157 if ($row.find('.ckeditor-toolbar-group').not('.placeholder').length === 0) {
Chris@0 158 $row.addClass('placeholder');
Chris@0 159 }
Chris@0 160 });
Chris@0 161 view.model.set('isDirty', true);
Chris@0 162 },
Chris@0 163
Chris@0 164 /**
Chris@0 165 * Opens a dialog with a form for changing the title of a button group.
Chris@0 166 *
Chris@0 167 * @param {Backbone.View} view
Chris@0 168 * The Backbone View that invoked this function.
Chris@0 169 * @param {jQuery} $group
Chris@0 170 * A jQuery set that contains an li element that wraps a group of buttons.
Chris@0 171 * @param {function} callback
Chris@0 172 * A callback to invoke after the button group naming modal dialog has
Chris@0 173 * been closed.
Chris@0 174 */
Chris@0 175 openGroupNameDialog(view, $group, callback) {
Chris@0 176 callback = callback || function () {};
Chris@0 177
Chris@0 178 /**
Chris@0 179 * Validates the string provided as a button group title.
Chris@0 180 *
Chris@0 181 * @param {HTMLElement} form
Chris@0 182 * The form DOM element that contains the input with the new button
Chris@0 183 * group title string.
Chris@0 184 *
Chris@0 185 * @return {bool}
Chris@0 186 * Returns true when an error exists, otherwise returns false.
Chris@0 187 */
Chris@0 188 function validateForm(form) {
Chris@0 189 if (form.elements[0].value.length === 0) {
Chris@0 190 const $form = $(form);
Chris@0 191 if (!$form.hasClass('errors')) {
Chris@0 192 $form
Chris@0 193 .addClass('errors')
Chris@0 194 .find('input')
Chris@0 195 .addClass('error')
Chris@0 196 .attr('aria-invalid', 'true');
Chris@0 197 $(`<div class="description" >${Drupal.t('Please provide a name for the button group.')}</div>`).insertAfter(form.elements[0]);
Chris@0 198 }
Chris@0 199 return true;
Chris@0 200 }
Chris@0 201 return false;
Chris@0 202 }
Chris@0 203
Chris@0 204 /**
Chris@0 205 * Attempts to close the dialog; Validates user input.
Chris@0 206 *
Chris@0 207 * @param {string} action
Chris@0 208 * The dialog action chosen by the user: 'apply' or 'cancel'.
Chris@0 209 * @param {HTMLElement} form
Chris@0 210 * The form DOM element that contains the input with the new button
Chris@0 211 * group title string.
Chris@0 212 */
Chris@0 213 function closeDialog(action, form) {
Chris@0 214 /**
Chris@0 215 * Closes the dialog when the user cancels or supplies valid data.
Chris@0 216 */
Chris@0 217 function shutdown() {
Chris@0 218 dialog.close(action);
Chris@0 219
Chris@0 220 // The processing marker can be deleted since the dialog has been
Chris@0 221 // closed.
Chris@0 222 delete view.isProcessing;
Chris@0 223 }
Chris@0 224
Chris@0 225 /**
Chris@0 226 * Applies a string as the name of a CKEditor button group.
Chris@0 227 *
Chris@0 228 * @param {jQuery} $group
Chris@0 229 * A jQuery set that contains an li element that wraps a group of
Chris@0 230 * buttons.
Chris@0 231 * @param {string} name
Chris@0 232 * The new name of the CKEditor button group.
Chris@0 233 */
Chris@0 234 function namePlaceholderGroup($group, name) {
Chris@0 235 // If it's currently still a placeholder, then that means we're
Chris@0 236 // creating a new group, and we must do some extra work.
Chris@0 237 if ($group.hasClass('placeholder')) {
Chris@0 238 // Remove all whitespace from the name, lowercase it and ensure
Chris@0 239 // HTML-safe encoding, then use this as the group ID for CKEditor
Chris@0 240 // configuration UI accessibility purposes only.
Chris@0 241 const groupID = `ckeditor-toolbar-group-aria-label-for-${Drupal.checkPlain(name.toLowerCase().replace(/\s/g, '-'))}`;
Chris@0 242 $group
Chris@0 243 // Update the group container.
Chris@0 244 .removeAttr('aria-label')
Chris@0 245 .attr('data-drupal-ckeditor-type', 'group')
Chris@0 246 .attr('tabindex', 0)
Chris@0 247 // Update the group heading.
Chris@0 248 .children('.ckeditor-toolbar-group-name')
Chris@0 249 .attr('id', groupID)
Chris@0 250 .end()
Chris@0 251 // Update the group items.
Chris@0 252 .children('.ckeditor-toolbar-group-buttons')
Chris@0 253 .attr('aria-labelledby', groupID);
Chris@0 254 }
Chris@0 255
Chris@0 256 $group
Chris@0 257 .attr('data-drupal-ckeditor-toolbar-group-name', name)
Chris@0 258 .children('.ckeditor-toolbar-group-name')
Chris@0 259 .text(name);
Chris@0 260 }
Chris@0 261
Chris@0 262 // Invoke a user-provided callback and indicate failure.
Chris@0 263 if (action === 'cancel') {
Chris@0 264 shutdown();
Chris@0 265 callback(false, $group);
Chris@0 266 return;
Chris@0 267 }
Chris@0 268
Chris@0 269 // Validate that a group name was provided.
Chris@0 270 if (form && validateForm(form)) {
Chris@0 271 return;
Chris@0 272 }
Chris@0 273
Chris@0 274 // React to application of a valid group name.
Chris@0 275 if (action === 'apply') {
Chris@0 276 shutdown();
Chris@0 277 // Apply the provided name to the button group label.
Chris@0 278 namePlaceholderGroup($group, Drupal.checkPlain(form.elements[0].value));
Chris@0 279 // Remove placeholder classes so that new placeholders will be
Chris@0 280 // inserted.
Chris@0 281 $group.closest('.ckeditor-row.placeholder').addBack().removeClass('placeholder');
Chris@0 282
Chris@0 283 // Invoke a user-provided callback and indicate success.
Chris@0 284 callback(true, $group);
Chris@0 285
Chris@0 286 // Signal that the active toolbar DOM structure has changed.
Chris@0 287 view.model.set('isDirty', true);
Chris@0 288 }
Chris@0 289 }
Chris@0 290
Chris@0 291 // Create a Drupal dialog that will get a button group name from the user.
Chris@0 292 const $ckeditorButtonGroupNameForm = $(Drupal.theme('ckeditorButtonGroupNameForm'));
Chris@0 293 const dialog = Drupal.dialog($ckeditorButtonGroupNameForm.get(0), {
Chris@0 294 title: Drupal.t('Button group name'),
Chris@0 295 dialogClass: 'ckeditor-name-toolbar-group',
Chris@0 296 resizable: false,
Chris@0 297 buttons: [
Chris@0 298 {
Chris@0 299 text: Drupal.t('Apply'),
Chris@0 300 click() {
Chris@0 301 closeDialog('apply', this);
Chris@0 302 },
Chris@0 303 primary: true,
Chris@0 304 },
Chris@0 305 {
Chris@0 306 text: Drupal.t('Cancel'),
Chris@0 307 click() {
Chris@0 308 closeDialog('cancel');
Chris@0 309 },
Chris@0 310 },
Chris@0 311 ],
Chris@0 312 open() {
Chris@0 313 const form = this;
Chris@0 314 const $form = $(this);
Chris@0 315 const $widget = $form.parent();
Chris@0 316 $widget.find('.ui-dialog-titlebar-close').remove();
Chris@0 317 // Set a click handler on the input and button in the form.
Chris@0 318 $widget.on('keypress.ckeditor', 'input, button', (event) => {
Chris@0 319 // React to enter key press.
Chris@0 320 if (event.keyCode === 13) {
Chris@0 321 const $target = $(event.currentTarget);
Chris@0 322 const data = $target.data('ui-button');
Chris@0 323 let action = 'apply';
Chris@0 324 // Assume 'apply', but take into account that the user might have
Chris@0 325 // pressed the enter key on the dialog buttons.
Chris@0 326 if (data && data.options && data.options.label) {
Chris@0 327 action = data.options.label.toLowerCase();
Chris@0 328 }
Chris@0 329 closeDialog(action, form);
Chris@0 330 event.stopPropagation();
Chris@0 331 event.stopImmediatePropagation();
Chris@0 332 event.preventDefault();
Chris@0 333 }
Chris@0 334 });
Chris@0 335 // Announce to the user that a modal dialog is open.
Chris@0 336 let text = Drupal.t('Editing the name of the new button group in a dialog.');
Chris@0 337 if (typeof $group.attr('data-drupal-ckeditor-toolbar-group-name') !== 'undefined') {
Chris@0 338 text = Drupal.t('Editing the name of the "@groupName" button group in a dialog.', {
Chris@0 339 '@groupName': $group.attr('data-drupal-ckeditor-toolbar-group-name'),
Chris@0 340 });
Chris@0 341 }
Chris@0 342 Drupal.announce(text);
Chris@0 343 },
Chris@0 344 close(event) {
Chris@0 345 // Automatically destroy the DOM element that was used for the dialog.
Chris@0 346 $(event.target).remove();
Chris@0 347 },
Chris@0 348 });
Chris@0 349 // A modal dialog is used because the user must provide a button group
Chris@0 350 // name or cancel the button placement before taking any other action.
Chris@0 351 dialog.showModal();
Chris@0 352
Chris@0 353 $(document.querySelector('.ckeditor-name-toolbar-group').querySelector('input'))
Chris@0 354 // When editing, set the "group name" input in the form to the current
Chris@0 355 // value.
Chris@0 356 .attr('value', $group.attr('data-drupal-ckeditor-toolbar-group-name'))
Chris@0 357 // Focus on the "group name" input in the form.
Chris@0 358 .trigger('focus');
Chris@0 359 },
Chris@0 360
Chris@0 361 };
Chris@0 362
Chris@0 363 /**
Chris@0 364 * Automatically shows/hides settings of buttons-only CKEditor plugins.
Chris@0 365 *
Chris@0 366 * @type {Drupal~behavior}
Chris@0 367 *
Chris@0 368 * @prop {Drupal~behaviorAttach} attach
Chris@0 369 * Attaches show/hide behaviour to Plugin Settings buttons.
Chris@0 370 */
Chris@0 371 Drupal.behaviors.ckeditorAdminButtonPluginSettings = {
Chris@0 372 attach(context) {
Chris@0 373 const $context = $(context);
Chris@0 374 const $ckeditorPluginSettings = $context.find('#ckeditor-plugin-settings').once('ckeditor-plugin-settings');
Chris@0 375 if ($ckeditorPluginSettings.length) {
Chris@0 376 // Hide all button-dependent plugin settings initially.
Chris@0 377 $ckeditorPluginSettings.find('[data-ckeditor-buttons]').each(function () {
Chris@0 378 const $this = $(this);
Chris@0 379 if ($this.data('verticalTab')) {
Chris@0 380 $this.data('verticalTab').tabHide();
Chris@0 381 }
Chris@0 382 else {
Chris@0 383 // On very narrow viewports, Vertical Tabs are disabled.
Chris@0 384 $this.hide();
Chris@0 385 }
Chris@0 386 $this.data('ckeditorButtonPluginSettingsActiveButtons', []);
Chris@0 387 });
Chris@0 388
Chris@0 389 // Whenever a button is added or removed, check if we should show or
Chris@0 390 // hide the corresponding plugin settings. (Note that upon
Chris@0 391 // initialization, each button that already is part of the toolbar still
Chris@0 392 // is considered "added", hence it also works correctly for buttons that
Chris@0 393 // were added previously.)
Chris@0 394 $context
Chris@0 395 .find('.ckeditor-toolbar-active')
Chris@0 396 .off('CKEditorToolbarChanged.ckeditorAdminPluginSettings')
Chris@0 397 .on('CKEditorToolbarChanged.ckeditorAdminPluginSettings', (event, action, button) => {
Chris@0 398 const $pluginSettings = $ckeditorPluginSettings
Chris@0 399 .find(`[data-ckeditor-buttons~=${button}]`);
Chris@0 400
Chris@0 401 // No settings for this button.
Chris@0 402 if ($pluginSettings.length === 0) {
Chris@0 403 return;
Chris@0 404 }
Chris@0 405
Chris@0 406 const verticalTab = $pluginSettings.data('verticalTab');
Chris@0 407 const activeButtons = $pluginSettings.data('ckeditorButtonPluginSettingsActiveButtons');
Chris@0 408 if (action === 'added') {
Chris@0 409 activeButtons.push(button);
Chris@0 410 // Show this plugin's settings if >=1 of its buttons are active.
Chris@0 411 if (verticalTab) {
Chris@0 412 verticalTab.tabShow();
Chris@0 413 }
Chris@0 414 else {
Chris@0 415 // On very narrow viewports, Vertical Tabs remain fieldsets.
Chris@0 416 $pluginSettings.show();
Chris@0 417 }
Chris@0 418 }
Chris@0 419 else {
Chris@0 420 // Remove this button from the list of active buttons.
Chris@0 421 activeButtons.splice(activeButtons.indexOf(button), 1);
Chris@0 422 // Show this plugin's settings 0 of its buttons are active.
Chris@0 423 if (activeButtons.length === 0) {
Chris@0 424 if (verticalTab) {
Chris@0 425 verticalTab.tabHide();
Chris@0 426 }
Chris@0 427 else {
Chris@0 428 // On very narrow viewports, Vertical Tabs are disabled.
Chris@0 429 $pluginSettings.hide();
Chris@0 430 }
Chris@0 431 }
Chris@0 432 }
Chris@0 433 $pluginSettings.data('ckeditorButtonPluginSettingsActiveButtons', activeButtons);
Chris@0 434 });
Chris@0 435 }
Chris@0 436 },
Chris@0 437 };
Chris@0 438
Chris@0 439 /**
Chris@0 440 * Themes a blank CKEditor row.
Chris@0 441 *
Chris@0 442 * @return {string}
Chris@0 443 * A HTML string for a CKEditor row.
Chris@0 444 */
Chris@0 445 Drupal.theme.ckeditorRow = function () {
Chris@0 446 return '<li class="ckeditor-row placeholder" role="group"><ul class="ckeditor-toolbar-groups clearfix"></ul></li>';
Chris@0 447 };
Chris@0 448
Chris@0 449 /**
Chris@0 450 * Themes a blank CKEditor button group.
Chris@0 451 *
Chris@0 452 * @return {string}
Chris@0 453 * A HTML string for a CKEditor button group.
Chris@0 454 */
Chris@0 455 Drupal.theme.ckeditorToolbarGroup = function () {
Chris@0 456 let group = '';
Chris@0 457 group += `<li class="ckeditor-toolbar-group placeholder" role="presentation" aria-label="${Drupal.t('Place a button to create a new button group.')}">`;
Chris@0 458 group += `<h3 class="ckeditor-toolbar-group-name">${Drupal.t('New group')}</h3>`;
Chris@0 459 group += '<ul class="ckeditor-buttons ckeditor-toolbar-group-buttons" role="toolbar" data-drupal-ckeditor-button-sorting="target"></ul>';
Chris@0 460 group += '</li>';
Chris@0 461 return group;
Chris@0 462 };
Chris@0 463
Chris@0 464 /**
Chris@0 465 * Themes a form for changing the title of a CKEditor button group.
Chris@0 466 *
Chris@0 467 * @return {string}
Chris@0 468 * A HTML string for the form for the title of a CKEditor button group.
Chris@0 469 */
Chris@0 470 Drupal.theme.ckeditorButtonGroupNameForm = function () {
Chris@0 471 return '<form><input name="group-name" required="required"></form>';
Chris@0 472 };
Chris@0 473
Chris@0 474 /**
Chris@0 475 * Themes a button that will toggle the button group names in active config.
Chris@0 476 *
Chris@0 477 * @return {string}
Chris@0 478 * A HTML string for the button to toggle group names.
Chris@0 479 */
Chris@0 480 Drupal.theme.ckeditorButtonGroupNamesToggle = function () {
Chris@0 481 return '<button class="link ckeditor-groupnames-toggle" aria-pressed="false"></button>';
Chris@0 482 };
Chris@0 483
Chris@0 484 /**
Chris@0 485 * Themes a button that will prompt the user to name a new button group.
Chris@0 486 *
Chris@0 487 * @return {string}
Chris@0 488 * A HTML string for the button to create a name for a new button group.
Chris@0 489 */
Chris@0 490 Drupal.theme.ckeditorNewButtonGroup = function () {
Chris@0 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>`;
Chris@0 492 };
Chris@0 493 }(jQuery, Drupal, drupalSettings, _));