Source: modules/ckeditor/js/views/KeyboardView.es6.js

/**
 * @file
 * Backbone View providing the aural view of CKEditor keyboard UX configuration.
 */

(function ($, Drupal, Backbone, _) {
  Drupal.ckeditor.KeyboardView = Backbone.View.extend(
    /** @lends Drupal.ckeditor.KeyboardView# */ {
      /**
       * Backbone View for CKEditor toolbar configuration; keyboard UX.
       *
       * @constructs
       *
       * @augments Backbone.View
       */
      initialize() {
        // Add keyboard arrow support.
        this.$el.on(
          'keydown.ckeditor',
          '.ckeditor-buttons a, .ckeditor-multiple-buttons a',
          this.onPressButton.bind(this),
        );
        this.$el.on(
          'keydown.ckeditor',
          '[data-drupal-ckeditor-type="group"]',
          this.onPressGroup.bind(this),
        );
      },

      /**
       * {@inheritdoc}
       */
      render() {},

      /**
       * Handles keypresses on a CKEditor configuration button.
       *
       * @param {jQuery.Event} event
       *   The keypress event triggered.
       */
      onPressButton(event) {
        const upDownKeys = [
          38, // Up arrow.
          63232, // Safari up arrow.
          40, // Down arrow.
          63233, // Safari down arrow.
        ];
        const leftRightKeys = [
          37, // Left arrow.
          63234, // Safari left arrow.
          39, // Right arrow.
          63235, // Safari right arrow.
        ];

        // Respond to an enter key press. Prevent the bubbling of the enter key
        // press to the button group parent element.
        if (event.keyCode === 13) {
          event.stopPropagation();
        }

        // Only take action when a direction key is pressed.
        if (_.indexOf(_.union(upDownKeys, leftRightKeys), event.keyCode) > -1) {
          let view = this;
          let $target = $(event.currentTarget);
          let $button = $target.parent();
          const $container = $button.parent();
          let $group = $button.closest('.ckeditor-toolbar-group');
          let $row;
          const containerType = $container.data(
            'drupal-ckeditor-button-sorting',
          );
          const $availableButtons = this.$el.find(
            '[data-drupal-ckeditor-button-sorting="source"]',
          );
          const $activeButtons = this.$el.find('.ckeditor-toolbar-active');
          // The current location of the button, just in case it needs to be put
          // back.
          const $originalGroup = $group;
          let dir;

          // Move available buttons between their container and the active
          // toolbar.
          if (containerType === 'source') {
            // Move the button to the active toolbar configuration when the down
            // or up keys are pressed.
            if (_.indexOf([40, 63233], event.keyCode) > -1) {
              // Move the button to the first row, first button group index
              // position.
              $activeButtons
                .find('.ckeditor-toolbar-group-buttons')
                .eq(0)
                .prepend($button);
            }
          } else if (containerType === 'target') {
            // Move buttons between sibling buttons in a group and between groups.
            if (_.indexOf(leftRightKeys, event.keyCode) > -1) {
              // Move left.
              const $siblings = $container.children();
              const index = $siblings.index($button);
              if (_.indexOf([37, 63234], event.keyCode) > -1) {
                // Move between sibling buttons.
                if (index > 0) {
                  $button.insertBefore($container.children().eq(index - 1));
                }
                // Move between button groups and rows.
                else {
                  // Move between button groups.
                  $group = $container.parent().prev();
                  if ($group.length > 0) {
                    $group
                      .find('.ckeditor-toolbar-group-buttons')
                      .append($button);
                  }
                  // Wrap between rows.
                  else {
                    $container
                      .closest('.ckeditor-row')
                      .prev()
                      .find('.ckeditor-toolbar-group')
                      .not('.placeholder')
                      .find('.ckeditor-toolbar-group-buttons')
                      .eq(-1)
                      .append($button);
                  }
                }
              }
              // Move right.
              else if (_.indexOf([39, 63235], event.keyCode) > -1) {
                // Move between sibling buttons.
                if (index < $siblings.length - 1) {
                  $button.insertAfter($container.children().eq(index + 1));
                }
                // Move between button groups. Moving right at the end of a row
                // will create a new group.
                else {
                  $container
                    .parent()
                    .next()
                    .find('.ckeditor-toolbar-group-buttons')
                    .prepend($button);
                }
              }
            }
            // Move buttons between rows and the available button set.
            else if (_.indexOf(upDownKeys, event.keyCode) > -1) {
              dir =
                _.indexOf([38, 63232], event.keyCode) > -1 ? 'prev' : 'next';
              $row = $container.closest('.ckeditor-row')[dir]();
              // Move the button back into the available button set.
              if (dir === 'prev' && $row.length === 0) {
                // If this is a divider, just destroy it.
                if ($button.data('drupal-ckeditor-type') === 'separator') {
                  $button.off().remove();
                  // Focus on the first button in the active toolbar.
                  $activeButtons
                    .find('.ckeditor-toolbar-group-buttons')
                    .eq(0)
                    .children()
                    .eq(0)
                    .children()
                    .trigger('focus');
                }
                // Otherwise, move it.
                else {
                  $availableButtons.prepend($button);
                }
              } else {
                $row
                  .find('.ckeditor-toolbar-group-buttons')
                  .eq(0)
                  .prepend($button);
              }
            }
          }
          // Move dividers between their container and the active toolbar.
          else if (containerType === 'dividers') {
            // Move the button to the active toolbar configuration when the down
            // or up keys are pressed.
            if (_.indexOf([40, 63233], event.keyCode) > -1) {
              // Move the button to the first row, first button group index
              // position.
              $button = $button.clone(true);
              $activeButtons
                .find('.ckeditor-toolbar-group-buttons')
                .eq(0)
                .prepend($button);
              $target = $button.children();
            }
          }

          view = this;
          // Attempt to move the button to the new toolbar position.
          Drupal.ckeditor.registerButtonMove(this, $button, (result) => {
            // Put the button back if the registration failed.
            // If the button was in a row, then it was in the active toolbar
            // configuration. The button was probably placed in a new group, but
            // that action was canceled.
            if (!result && $originalGroup) {
              $originalGroup.find('.ckeditor-buttons').append($button);
            }
            // Refocus the target button so that the user can continue from a
            // known place.
            $target.trigger('focus');
          });

          event.preventDefault();
          event.stopPropagation();
        }
      },

      /**
       * Handles keypresses on a CKEditor configuration group.
       *
       * @param {jQuery.Event} event
       *   The keypress event triggered.
       */
      onPressGroup(event) {
        const upDownKeys = [
          38, // Up arrow.
          63232, // Safari up arrow.
          40, // Down arrow.
          63233, // Safari down arrow.
        ];
        const leftRightKeys = [
          37, // Left arrow.
          63234, // Safari left arrow.
          39, // Right arrow.
          63235, // Safari right arrow.
        ];

        // Respond to an enter key press.
        if (event.keyCode === 13) {
          const view = this;
          // Open the group renaming dialog in the next evaluation cycle so that
          // this event can be cancelled and the bubbling wiped out. Otherwise,
          // Firefox has issues because the page focus is shifted to the dialog
          // along with the keydown event.
          window.setTimeout(() => {
            Drupal.ckeditor.openGroupNameDialog(view, $(event.currentTarget));
          }, 0);
          event.preventDefault();
          event.stopPropagation();
        }

        // Respond to direction key presses.
        if (_.indexOf(_.union(upDownKeys, leftRightKeys), event.keyCode) > -1) {
          const $group = $(event.currentTarget);
          const $container = $group.parent();
          const $siblings = $container.children();
          let index;
          let dir;
          // Move groups between sibling groups.
          if (_.indexOf(leftRightKeys, event.keyCode) > -1) {
            index = $siblings.index($group);
            // Move left between sibling groups.
            if (_.indexOf([37, 63234], event.keyCode) > -1) {
              if (index > 0) {
                $group.insertBefore($siblings.eq(index - 1));
              }
              // Wrap between rows. Insert the group before the placeholder group
              // at the end of the previous row.
              else {
                const $rowChildElement = $container
                  .closest('.ckeditor-row')
                  .prev()
                  .find('.ckeditor-toolbar-groups')
                  .children()
                  .eq(-1);
                $group.insertBefore($rowChildElement);
              }
            }
            // Move right between sibling groups.
            else if (_.indexOf([39, 63235], event.keyCode) > -1) {
              // Move to the right if the next group is not a placeholder.
              if (!$siblings.eq(index + 1).hasClass('placeholder')) {
                $group.insertAfter($container.children().eq(index + 1));
              }
              // Wrap group between rows.
              else {
                $container
                  .closest('.ckeditor-row')
                  .next()
                  .find('.ckeditor-toolbar-groups')
                  .prepend($group);
              }
            }
          }
          // Move groups between rows.
          else if (_.indexOf(upDownKeys, event.keyCode) > -1) {
            dir = _.indexOf([38, 63232], event.keyCode) > -1 ? 'prev' : 'next';
            $group
              .closest('.ckeditor-row')
              [dir]()
              .find('.ckeditor-toolbar-groups')
              .eq(0)
              .prepend($group);
          }

          Drupal.ckeditor.registerGroupMove(this, $group);
          $group.trigger('focus');
          event.preventDefault();
          event.stopPropagation();
        }
      },
    },
  );
})(jQuery, Drupal, Backbone, _);