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