Chris@0: /** Chris@0: * @file Chris@0: * A Backbone View that provides the aural view of CKEditor toolbar Chris@0: * configuration. Chris@0: */ Chris@0: Chris@17: (function(Drupal, Backbone, $) { Chris@17: Drupal.ckeditor.AuralView = Backbone.View.extend( Chris@17: /** @lends Drupal.ckeditor.AuralView# */ { Chris@17: /** Chris@17: * @type {object} Chris@17: */ Chris@17: events: { Chris@17: 'click .ckeditor-buttons a': 'announceButtonHelp', Chris@17: 'click .ckeditor-multiple-buttons a': 'announceSeparatorHelp', Chris@17: 'focus .ckeditor-button a': 'onFocus', Chris@17: 'focus .ckeditor-button-separator a': 'onFocus', Chris@17: 'focus .ckeditor-toolbar-group': 'onFocus', Chris@17: }, Chris@0: Chris@17: /** Chris@17: * Backbone View for CKEditor toolbar configuration; aural UX (output only). Chris@17: * Chris@17: * @constructs Chris@17: * Chris@17: * @augments Backbone.View Chris@17: */ Chris@17: initialize() { Chris@17: // Announce the button and group positions when the model is no longer Chris@17: // dirty. Chris@17: this.listenTo(this.model, 'change:isDirty', this.announceMove); Chris@17: }, Chris@0: Chris@17: /** Chris@17: * Calls announce on buttons and groups when their position is changed. Chris@17: * Chris@17: * @param {Drupal.ckeditor.ConfigurationModel} model Chris@17: * The ckeditor configuration model. Chris@17: * @param {bool} isDirty Chris@17: * A model attribute that indicates if the changed toolbar configuration Chris@17: * has been stored or not. Chris@17: */ Chris@17: announceMove(model, isDirty) { Chris@17: // Announce the position of a button or group after the model has been Chris@17: // updated. Chris@17: if (!isDirty) { Chris@17: const item = document.activeElement || null; Chris@17: if (item) { Chris@17: const $item = $(item); Chris@17: if ($item.hasClass('ckeditor-toolbar-group')) { Chris@17: this.announceButtonGroupPosition($item); Chris@17: } else if ($item.parent().hasClass('ckeditor-button')) { Chris@17: this.announceButtonPosition($item.parent()); Chris@17: } Chris@0: } Chris@0: } Chris@17: }, Chris@0: Chris@17: /** Chris@17: * Handles the focus event of elements in the active and available toolbars. Chris@17: * Chris@17: * @param {jQuery.Event} event Chris@17: * The focus event that was triggered. Chris@17: */ Chris@17: onFocus(event) { Chris@17: event.stopPropagation(); Chris@0: Chris@17: const $originalTarget = $(event.target); Chris@17: const $currentTarget = $(event.currentTarget); Chris@17: const $parent = $currentTarget.parent(); Chris@17: if ( Chris@17: $parent.hasClass('ckeditor-button') || Chris@17: $parent.hasClass('ckeditor-button-separator') Chris@17: ) { Chris@17: this.announceButtonPosition($currentTarget.parent()); Chris@17: } else if ( Chris@17: $originalTarget.attr('role') !== 'button' && Chris@17: $currentTarget.hasClass('ckeditor-toolbar-group') Chris@17: ) { Chris@17: this.announceButtonGroupPosition($currentTarget); Chris@17: } Chris@17: }, Chris@0: Chris@17: /** Chris@17: * Announces the current position of a button group. Chris@17: * Chris@17: * @param {jQuery} $group Chris@17: * A jQuery set that contains an li element that wraps a group of buttons. Chris@17: */ Chris@17: announceButtonGroupPosition($group) { Chris@17: const $groups = $group.parent().children(); Chris@17: const $row = $group.closest('.ckeditor-row'); Chris@17: const $rows = $row.parent().children(); Chris@17: const position = $groups.index($group) + 1; Chris@17: const positionCount = $groups.not('.placeholder').length; Chris@17: const row = $rows.index($row) + 1; Chris@17: const rowCount = $rows.not('.placeholder').length; Chris@17: let text = Drupal.t( Chris@17: '@groupName button group in position @position of @positionCount in row @row of @rowCount.', Chris@17: { Chris@17: '@groupName': $group.attr( Chris@17: 'data-drupal-ckeditor-toolbar-group-name', Chris@17: ), Chris@17: '@position': position, Chris@17: '@positionCount': positionCount, Chris@17: '@row': row, Chris@17: '@rowCount': rowCount, Chris@17: }, Chris@17: ); Chris@0: // If this position is the first in the last row then tell the user that Chris@0: // pressing the down arrow key will create a new row. Chris@17: if (position === 1 && row === rowCount) { Chris@0: text += '\n'; Chris@17: text += Drupal.t('Press the down arrow key to create a new row.'); Chris@0: } Chris@0: Drupal.announce(text, 'assertive'); Chris@17: }, Chris@17: Chris@17: /** Chris@17: * Announces current button position. Chris@17: * Chris@17: * @param {jQuery} $button Chris@17: * A jQuery set that contains an li element that wraps a button. Chris@17: */ Chris@17: announceButtonPosition($button) { Chris@17: const $row = $button.closest('.ckeditor-row'); Chris@17: const $rows = $row.parent().children(); Chris@17: const $buttons = $button.closest('.ckeditor-buttons').children(); Chris@17: const $group = $button.closest('.ckeditor-toolbar-group'); Chris@17: const $groups = $group.parent().children(); Chris@17: const groupPosition = $groups.index($group) + 1; Chris@17: const groupPositionCount = $groups.not('.placeholder').length; Chris@17: const position = $buttons.index($button) + 1; Chris@17: const positionCount = $buttons.length; Chris@17: const row = $rows.index($row) + 1; Chris@17: const rowCount = $rows.not('.placeholder').length; Chris@17: // The name of the button separator is 'button separator' and its type Chris@17: // is 'separator', so we do not want to print the type of this item, Chris@17: // otherwise the UA will speak 'button separator separator'. Chris@17: const type = Chris@17: $button.attr('data-drupal-ckeditor-type') === 'separator' Chris@17: ? '' Chris@17: : Drupal.t('button'); Chris@17: let text; Chris@17: // The button is located in the available button set. Chris@17: if ($button.closest('.ckeditor-toolbar-disabled').length > 0) { Chris@17: text = Drupal.t('@name @type.', { Chris@17: '@name': $button.children().attr('aria-label'), Chris@17: '@type': type, Chris@17: }); Chris@17: text += `\n${Drupal.t('Press the down arrow key to activate.')}`; Chris@17: Chris@17: Drupal.announce(text, 'assertive'); Chris@17: } Chris@17: // The button is in the active toolbar. Chris@17: else if ($group.not('.placeholder').length === 1) { Chris@17: text = Drupal.t( Chris@17: '@name @type in position @position of @positionCount in @groupName button group in row @row of @rowCount.', Chris@17: { Chris@17: '@name': $button.children().attr('aria-label'), Chris@17: '@type': type, Chris@17: '@position': position, Chris@17: '@positionCount': positionCount, Chris@17: '@groupName': $group.attr( Chris@17: 'data-drupal-ckeditor-toolbar-group-name', Chris@17: ), Chris@17: '@row': row, Chris@17: '@rowCount': rowCount, Chris@17: }, Chris@17: ); Chris@17: // If this position is the first in the last row then tell the user that Chris@17: // pressing the down arrow key will create a new row. Chris@17: if (groupPosition === 1 && position === 1 && row === rowCount) { Chris@17: text += '\n'; Chris@17: text += Drupal.t( Chris@17: 'Press the down arrow key to create a new button group in a new row.', Chris@17: ); Chris@17: } Chris@17: // If this position is the last one in this row then tell the user that Chris@17: // moving the button to the next group will create a new group. Chris@17: if ( Chris@17: groupPosition === groupPositionCount && Chris@17: position === positionCount Chris@17: ) { Chris@17: text += '\n'; Chris@17: text += Drupal.t( Chris@17: 'This is the last group. Move the button forward to create a new group.', Chris@17: ); Chris@17: } Chris@17: Drupal.announce(text, 'assertive'); Chris@17: } Chris@17: }, Chris@17: Chris@17: /** Chris@17: * Provides help information when a button is clicked. Chris@17: * Chris@17: * @param {jQuery.Event} event Chris@17: * The click event for the button click. Chris@17: */ Chris@17: announceButtonHelp(event) { Chris@17: const $link = $(event.currentTarget); Chris@17: const $button = $link.parent(); Chris@17: const enabled = $button.closest('.ckeditor-toolbar-active').length > 0; Chris@17: let message; Chris@17: Chris@17: if (enabled) { Chris@17: message = Drupal.t('The "@name" button is currently enabled.', { Chris@17: '@name': $link.attr('aria-label'), Chris@17: }); Chris@17: message += `\n${Drupal.t( Chris@17: 'Use the keyboard arrow keys to change the position of this button.', Chris@17: )}`; Chris@17: message += `\n${Drupal.t( Chris@17: 'Press the up arrow key on the top row to disable the button.', Chris@17: )}`; Chris@17: } else { Chris@17: message = Drupal.t('The "@name" button is currently disabled.', { Chris@17: '@name': $link.attr('aria-label'), Chris@17: }); Chris@17: message += `\n${Drupal.t( Chris@17: 'Use the down arrow key to move this button into the active toolbar.', Chris@17: )}`; Chris@17: } Chris@17: Drupal.announce(message); Chris@17: event.preventDefault(); Chris@17: }, Chris@17: Chris@17: /** Chris@17: * Provides help information when a separator is clicked. Chris@17: * Chris@17: * @param {jQuery.Event} event Chris@17: * The click event for the separator click. Chris@17: */ Chris@17: announceSeparatorHelp(event) { Chris@17: const $link = $(event.currentTarget); Chris@17: const $button = $link.parent(); Chris@17: const enabled = $button.closest('.ckeditor-toolbar-active').length > 0; Chris@17: let message; Chris@17: Chris@17: if (enabled) { Chris@17: message = Drupal.t('This @name is currently enabled.', { Chris@17: '@name': $link.attr('aria-label'), Chris@17: }); Chris@17: message += `\n${Drupal.t( Chris@17: 'Use the keyboard arrow keys to change the position of this separator.', Chris@17: )}`; Chris@17: } else { Chris@17: message = Drupal.t( Chris@17: 'Separators are used to visually split individual buttons.', Chris@17: ); Chris@17: message += `\n${Drupal.t('This @name is currently disabled.', { Chris@17: '@name': $link.attr('aria-label'), Chris@17: })}`; Chris@17: message += `\n${Drupal.t( Chris@17: 'Use the down arrow key to move this separator into the active toolbar.', Chris@17: )}`; Chris@17: message += `\n${Drupal.t( Chris@17: 'You may add multiple separators to each button group.', Chris@17: )}`; Chris@17: } Chris@17: Drupal.announce(message); Chris@17: event.preventDefault(); Chris@17: }, Chris@0: }, Chris@17: ); Chris@17: })(Drupal, Backbone, jQuery);