diffux/diffux

View on GitHub
app/assets/javascripts/_keyboard-shortcuts.js

Summary

Maintainability
A
0 mins
Test Coverage
$(function() {
  var defaultScrollSpeed    = 200,
      prefixKeysPressed     = {},
      prefixShortcutTimeout = 1100;

  $(document).on('keypress', function(event) {
    if ($(event.target).is(':input')) {
      return;
    }

    if (!($.isEmptyObject(prefixKeysPressed))) {
      event.preventDefault();
      handlePrefixedShortcuts(event.which);
      return;
    }

    resetPrefixKeys();
    switch (event.which) {
      case 106: // j
        focusNextFocusable();
        scrollToFocused();
        event.preventDefault();
        break;

      case 107: // k
        focusPreviousFocusable();
        scrollToFocused();
        event.preventDefault();
        break;

      case 120: // x
        switchSnapshotDiffTab();
        event.preventDefault();
        break;

      case 13:  // Enter
      case 111: // o
        openFocused();
        event.preventDefault();
        break;

      case 63:  // ?
        openHelpModal();
        event.preventDefault();
        break;

      case 71:  // G
        scrollAndFocusBottom();
        event.preventDefault();
        break;

      case 103: // g prefix
        setPrefixKey(event.which);
        event.preventDefault();
        break;

      default: // check for shortcut keys
        if (handleShortcutKey(event.which)) {
          event.preventDefault();
        }
    }

    // Handlers for shortcuts:

    function focusNextFocusable() {
      moveFocus({ forward: true });
    }

    function focusPreviousFocusable() {
      moveFocus({ backward: true });
    }

    function handlePrefixedShortcuts(keyCode) {
      resetPrefixKeys();
      // handle different prefixes with diff. shortcuts
      if(keyCode == 103) {
        switch (keyCode) {
          case 103: // g
            scrollAndFocusTop();
            break;

          default: // ignore if it wasn't a prefixed shortcut
            resetPrefixKeys();
        }
      }
    }

    function handleShortcutKey(keyCode) {
      var key = String.fromCharCode(keyCode);
      if (key) {
        var $shortcut = $('[data-keyboard-shortcut~="' + key + '"]');
        if ($shortcut.length) {
          $shortcut.attr('aria-selected', true).focus();
          $shortcut[0].click();
          scrollToFocused();
          return true;
        }
      }
    }

    // @param $focusable [$Object] jQuery element within which to search for
    //   focused element(s)
    // @return [$Object] jQuery element that is focused
    // side effect: if no focused el. is found, it sets the first el. to
    //   focused.
    function getFocusedElement($focusable) {
      var $focused = $focusable.filter('[aria-selected]');
      if (!($focused.length)) {
        setFocus('first');
        $focused = $focusable.filter('[aria-selected]');
      }
      return $focused;
    }

    // @param movement [Object] options hash allows either forward, backward,
    //   first or last to set movement type
    // @return [Boolean] true if movement was successful, false otherwise
    function moveFocus(movement) {
      var $focusable  = $('[data-keyboard-focusable]:visible'),
          focusExists = !!($('[aria-selected]').length),
          $focused    = getFocusedElement($focusable);
      if (movement.forward && !(focusExists)) {
        // if there was nothing in focus, we stop after moving focus to top
        return;
      }
      if ($focused.length) {
        if (movement.first || movement.last) {
          var $nextFocus = (movement.first) ? $focusable.first() : $focusable.last();
          $focused.removeAttr('aria-selected');
          $nextFocus.attr('aria-selected', true).focus();
          return true;
        } else {
          var dir     = (movement.forward) ? 1 : -1,
              moveTo  = $focusable.index($focused) + dir;
          if (moveTo >= 0 && moveTo < $focusable.length) {
            $focused.removeAttr('aria-selected');
            $focusable.eq(moveTo).attr('aria-selected', true).focus();
            return true;
          }
        }
      }
      return false;
    }

    function openFocused() {
      var $focused = $('[data-keyboard-focusable]:visible[aria-selected]');
      if ($focused.is('a')) {
        $focused[0].click();
      } else {
        var $link = $focused.find('a:visible:first');
        if ($link.length) {
          $link[0].click();
        }
      }
    }

    function openHelpModal() {
      $('.keyboard-shortcut-help').modal('toggle');
    }

    function resetPrefixKeys() {
      prefixKeysPressed = {};
    }

    function scrollAndFocusBottom() {
       $('html, body').animate({ scrollTop: $(document).height() },
          defaultScrollSpeed);
       moveFocus({ last: true });
    }

    function scrollAndFocusTop() {
      $('html, body').animate({ scrollTop: 0 }, defaultScrollSpeed);
      moveFocus({ first: true });
    }

    function scrollToFocused() {
      var $focused = $('[aria-selected]');
      if ($focused.length && !$focused.visible()) {
        $('html, body').stop(true, true).animate({
          scrollTop: $focused.offset().top - $(window).height() / 4
        }, defaultScrollSpeed);
      }
    }

    function setPrefixKey(key) {
      if (key) {
        prefixKeysPressed[key] = 1;
        setTimeout(resetPrefixKeys, prefixShortcutTimeout);
      }
    }

    // @param whereToFocus [String] either 'first' or 'last'; used to select
    //   focusable element
    function setFocus(whereToFocus) {
      $('[data-keyboard-focusable]:visible:' + whereToFocus)
        .attr('aria-selected', true)
        .focus();
    }

    function switchSnapshotDiffTab() {
      var tabSelector = '.snapshot-diff-image .nav li',
          $active     = $(tabSelector + '.active'),
          $next       = $active.next();
      if (!$next.length) {
        $next = $(tabSelector + ':first');
      }
      $next.find('a').click();
    }
  });
});