From 0cbc19f9449ffc6e624c0b7e22ea837468658377 Mon Sep 17 00:00:00 2001
From: Robert Schilling <rschilling@student.tugraz.at>
Date: Thu, 21 Aug 2014 10:14:31 +0200
Subject: [PATCH] Awesome shortcuts for GitLab

---
 CHANGELOG                                     |   1 +
 Gemfile                                       |   3 +
 Gemfile.lock                                  |   2 +
 app/assets/javascripts/application.js.coffee  |  27 +--
 app/assets/javascripts/branch-graph.js.coffee |  37 +--
 app/assets/javascripts/dispatcher.js.coffee   |  38 ++-
 app/assets/javascripts/network.js.coffee      |   6 +-
 app/assets/javascripts/shortcuts.js.coffee    |  25 +-
 .../shortcuts_dashboard_navigation.js.coffee  |  14 ++
 .../javascripts/shortcuts_issueable.coffee    |  19 ++
 .../javascripts/shortcuts_navigation.coffee   |  20 ++
 .../javascripts/shortcuts_network.js.coffee   |  12 +
 app/assets/stylesheets/main/layout.scss       |   1 +
 app/assets/stylesheets/sections/help.scss     |  53 ++++
 app/views/help/_shortcuts.html.haml           | 229 ++++++++++++++++--
 app/views/help/index.html.haml                |   4 +-
 app/views/layouts/_search.html.haml           |   7 +
 app/views/layouts/nav/_dashboard.html.haml    |   8 +-
 app/views/layouts/nav/_project.html.haml      |  23 +-
 .../projects/issues/_issue_context.html.haml  |   4 +-
 .../merge_requests/show/_context.html.haml    |   4 +-
 app/views/projects/network/show.html.haml     |   3 +-
 features/dashboard/shortcuts.feature          |  21 ++
 features/project/shortcuts.feature            |  46 ++++
 features/steps/dashboard/active_tab.rb        |  14 +-
 features/steps/dashboard/shortcuts.rb         |   6 +
 features/steps/project/active_tab.rb          |  39 +--
 features/steps/project/project_shortcuts.rb   |  36 +++
 features/steps/shared/active_tab.rb           |  20 ++
 features/steps/shared/project_tab.rb          |  44 ++++
 features/steps/shared/shortcuts.rb            |  18 ++
 31 files changed, 646 insertions(+), 138 deletions(-)
 create mode 100644 app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee
 create mode 100644 app/assets/javascripts/shortcuts_issueable.coffee
 create mode 100644 app/assets/javascripts/shortcuts_navigation.coffee
 create mode 100644 app/assets/javascripts/shortcuts_network.js.coffee
 create mode 100644 features/dashboard/shortcuts.feature
 create mode 100644 features/project/shortcuts.feature
 create mode 100644 features/steps/dashboard/shortcuts.rb
 create mode 100644 features/steps/project/project_shortcuts.rb
 create mode 100644 features/steps/shared/project_tab.rb
 create mode 100644 features/steps/shared/shortcuts.rb

diff --git a/CHANGELOG b/CHANGELOG
index f26570965e6f8..8ebd4addcbb14 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -10,6 +10,7 @@ v 7.3.0
   - Support Unix domain sockets for Redis
   - Store session Redis keys in 'session:gitlab:' namespace
   - Deprecate LDAP account takeover based on partial LDAP email / GitLab username match
+  - Keyboard shortcuts for productivity (Robert Schilling)
 
 v 7.2.0
   - Explore page
diff --git a/Gemfile b/Gemfile
index 61a9c6cdf667f..6e08d13bccb16 100644
--- a/Gemfile
+++ b/Gemfile
@@ -156,6 +156,9 @@ gem "rack-attack"
 # Ace editor
 gem 'ace-rails-ap'
 
+# Keyboard shortcuts 
+gem 'mousetrap-rails'
+
 # Semantic UI Sass for Sidebar
 gem 'semantic-ui-sass', '~> 0.16.1.0'
 
diff --git a/Gemfile.lock b/Gemfile.lock
index edd30fda37adc..cee22969215fc 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -287,6 +287,7 @@ GEM
     mime-types (1.25.1)
     mini_portile (0.6.0)
     minitest (5.3.5)
+    mousetrap-rails (1.4.6)
     multi_json (1.10.1)
     multi_xml (0.5.5)
     multipart-post (1.2.0)
@@ -636,6 +637,7 @@ DEPENDENCIES
   launchy
   letter_opener
   minitest (~> 5.3.0)
+  mousetrap-rails
   mysql2
   nprogress-rails
   omniauth (~> 1.1.3)
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 6bc24cf75902f..86ccd8c21ed0e 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -33,6 +33,12 @@
 #= require nprogress-turbolinks
 #= require dropzone
 #= require semantic-ui/sidebar
+#= require mousetrap
+#= require shortcuts
+#= require shortcuts_navigation
+#= require shortcuts_dashboard_navigation
+#= require shortcuts_issueable
+#= require shortcuts_network
 #= require_tree .
 
 window.slugify = (text) ->
@@ -119,6 +125,13 @@ $ ->
   # Initialize select2 selects
   $('select.select2').select2(width: 'resolve', dropdownAutoWidth: true)
 
+  # Close select2 on escape
+  $('.js-select2').bind 'select2-close', ->
+    setTimeout ( ->
+      $('.select2-container-active').removeClass('select2-container-active')
+      $(':focus').blur()
+    ), 1
+
   # Initialize tooltips
   $('.has_tooltip').tooltip()
 
@@ -151,20 +164,6 @@ $ ->
   # Show/Hide the profile menu when hovering the account box
   $('.account-box').hover -> $(@).toggleClass('hover')
 
-  # Focus search field by pressing 's' key
-  $(document).keypress (e) ->
-    # Don't do anything if typing in an input
-    return if $(e.target).is(":input")
-
-    switch e.which
-      when 115
-        $("#search").focus()
-        e.preventDefault()
-      when 63
-        new Shortcuts()
-        e.preventDefault()
-
-
   # Commit show suppressed diff
   $(".diff-content").on "click", ".supp_diff_link", ->
     $(@).next('table').show()
diff --git a/app/assets/javascripts/branch-graph.js.coffee b/app/assets/javascripts/branch-graph.js.coffee
index f6d57bd55bba7..b8af07579f2a8 100644
--- a/app/assets/javascripts/branch-graph.js.coffee
+++ b/app/assets/javascripts/branch-graph.js.coffee
@@ -1,4 +1,4 @@
-class BranchGraph
+class @BranchGraph
   constructor: (@element, @options) ->
     @preparedCommits = {}
     @mtime = 0
@@ -120,23 +120,32 @@ class BranchGraph
       @top.toFront()
 
   bindEvents: ->
-    drag = {}
     element = @element
 
     $(element).scroll (event) =>
       @renderPartialGraph()
 
-    $(window).on
-      keydown: (event) =>
-        # left
-        element.scrollLeft element.scrollLeft() - 50  if event.keyCode is 37
-        # top
-        element.scrollTop element.scrollTop() - 50  if event.keyCode is 38
-        # right
-        element.scrollLeft element.scrollLeft() + 50  if event.keyCode is 39
-        # bottom
-        element.scrollTop element.scrollTop() + 50  if event.keyCode is 40
-        @renderPartialGraph()
+  scrollDown: =>
+    @element.scrollTop @element.scrollTop() + 50
+    @renderPartialGraph()
+
+  scrollUp: =>
+    @element.scrollTop @element.scrollTop() - 50
+    @renderPartialGraph()
+
+  scrollLeft: =>
+    @element.scrollLeft @element.scrollLeft() - 50
+    @renderPartialGraph()
+
+  scrollRight: =>
+    @element.scrollLeft @element.scrollLeft() + 50
+    @renderPartialGraph()
+
+  scrollBottom: =>
+    @element.scrollTop @element.find('svg').height()
+
+  scrollTop: =>
+    @element.scrollTop 0
 
   appendLabel: (x, y, commit) ->
     return unless commit.refs
@@ -325,5 +334,3 @@ Raphael::textWrap = (t, width) ->
   b = t.getBBox()
   h = Math.abs(b.y2) - Math.abs(b.y) + 1
   t.attr y: b.y + h
-
-@BranchGraph = BranchGraph
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index e5e62c87e40da..ae4cf57717967 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -15,49 +15,83 @@ class Dispatcher
       return false
 
     path = page.split(':')
+    shortcut_handler = null
 
     switch page
       when 'projects:issues:index'
         Issues.init()
+        shortcut_handler = new ShortcutsNavigation()
       when 'projects:issues:show'
         new Issue()
+        shortcut_handler = new ShortcutsIssueable()
       when 'projects:milestones:show'
         new Milestone()
       when 'projects:issues:new'
         GitLab.GfmAutoComplete.setup()
+        shortcut_handler = new ShortcutsNavigation()
       when 'projects:merge_requests:new'
         GitLab.GfmAutoComplete.setup()
         new Diff()
+        shortcut_handler = new ShortcutsNavigation()
       when 'projects:merge_requests:show'
         new Diff()
+        shortcut_handler = new ShortcutsIssueable()
       when "projects:merge_requests:diffs"
         new Diff()
+      when 'projects:merge_requests:index'
+        shortcut_handler = new ShortcutsNavigation()
       when 'dashboard:show'
         new Dashboard()
         new Activities()
       when 'projects:commit:show'
         new Commit()
         new Diff()
+        shortcut_handler = new ShortcutsNavigation()
+      when 'projects:commits:show'
+        shortcut_handler = new ShortcutsNavigation()
       when 'groups:show', 'projects:show'
         new Activities()
-      when 'projects:new', 'projects:edit'
+        shortcut_handler = new ShortcutsNavigation()
+      when 'projects:new'
         new Project()
+      when 'projects:edit'
+        new Project()
+        shortcut_handler = new ShortcutsNavigation()
       when 'projects:teams:members:index'
         new TeamMembers()
       when 'groups:members'
         new GroupMembers()
       when 'projects:tree:show'
         new TreeView()
+        shortcut_handler = new ShortcutsNavigation()
       when 'projects:blob:show'
         new BlobView()
+        shortcut_handler = new ShortcutsNavigation()
       when 'projects:labels:new', 'projects:labels:edit'
         new Labels()
+      when 'projects:network:show'
+        # Ensure we don't create a particular shortcut handler here. This is
+        # already created, where the network graph is created.
+        shortcut_handler = true
 
     switch path.first()
       when 'admin' then new Admin()
+      when 'dashboard'
+        shortcut_handler = new ShortcutsDashboardNavigation()
       when 'projects'
-        new Wikis() if path[1] == 'wikis'
+        switch path[1]
+          when 'wikis'
+            new Wikis()
+            shortcut_handler = new ShortcutsNavigation()
+          when 'snippets', 'labels', 'graphs'
+            shortcut_handler = new ShortcutsNavigation()
+          when 'team_members', 'deploy_keys', 'hooks', 'services', 'protected_branches'
+            shortcut_handler = new ShortcutsNavigation()
+
 
+    # If we haven't installed a custom shortcut handler, install the default one
+    if not shortcut_handler
+      new Shortcuts()
 
   initSearch: ->
     opts = $('.search-autocomplete-opts')
diff --git a/app/assets/javascripts/network.js.coffee b/app/assets/javascripts/network.js.coffee
index cea5986f45a65..f4ef07a50a7e1 100644
--- a/app/assets/javascripts/network.js.coffee
+++ b/app/assets/javascripts/network.js.coffee
@@ -1,11 +1,9 @@
-class Network
+class @Network
   constructor: (opts) ->
     $("#filter_ref").click ->
       $(this).closest('form').submit()
 
-    branch_graph = new BranchGraph($(".network-graph"), opts)
+    @branch_graph = new BranchGraph($(".network-graph"), opts)
 
     vph = $(window).height() - 250
     $('.network-graph').css 'height': (vph + 'px')
-
-@Network = Network
diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee
index e7e40a066eca3..e9aeb1e9525cc 100644
--- a/app/assets/javascripts/shortcuts.js.coffee
+++ b/app/assets/javascripts/shortcuts.js.coffee
@@ -1,11 +1,30 @@
-class Shortcuts
+class @Shortcuts
   constructor: ->
+    @enabledHelp = []
+    Mousetrap.reset()
+    Mousetrap.bind('?', @selectiveHelp)
+    Mousetrap.bind('s', Shortcuts.focusSearch)
+
+  selectiveHelp: (e) =>
+    Shortcuts.showHelp(e, @enabledHelp)
+      
+  @showHelp: (e, location) ->
     if $('#modal-shortcuts').length > 0
       $('#modal-shortcuts').modal('show')
     else
       $.ajax(
         url: '/help/shortcuts',
-        dataType: "script"
+        dataType: 'script',
+        success: (e) ->
+          if location and location.length > 0
+            for l in location
+              $(l).show()
+          else
+            $('.hidden-shortcut').show()
+            $('.js-more-help-button').remove()
       )
+      e.preventDefault()
 
-@Shortcuts = Shortcuts
+  @focusSearch: (e) ->
+    $('#search').focus()
+    e.preventDefault()
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee b/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee
new file mode 100644
index 0000000000000..d522d9f3b9073
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee
@@ -0,0 +1,14 @@
+#= require shortcuts
+
+class @ShortcutsDashboardNavigation extends Shortcuts
+ constructor: ->
+   super()
+   Mousetrap.bind('g a', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-activity'))
+   Mousetrap.bind('g p', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-projects'))
+   Mousetrap.bind('g i', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-issues'))
+   Mousetrap.bind('g m', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-merge_requests'))
+
+ @findAndollowLink: (selector) ->
+   link = $(selector).attr('href')
+   if link
+     window.location = link
diff --git a/app/assets/javascripts/shortcuts_issueable.coffee b/app/assets/javascripts/shortcuts_issueable.coffee
new file mode 100644
index 0000000000000..b8dae71e03731
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_issueable.coffee
@@ -0,0 +1,19 @@
+#= require shortcuts_navigation
+
+class @ShortcutsIssueable extends ShortcutsNavigation
+  constructor: (isMergeRequest) ->
+    super()
+    Mousetrap.bind('a', ->
+      $('.js-assignee').select2('open')
+      return false
+    )
+    Mousetrap.bind('m', ->
+      $('.js-milestone').select2('open')
+      return false
+    )
+    
+    if isMergeRequest
+      @enabledHelp.push('.hidden-shortcut.merge_reuests')
+    else
+      @enabledHelp.push('.hidden-shortcut.issues')
+
diff --git a/app/assets/javascripts/shortcuts_navigation.coffee b/app/assets/javascripts/shortcuts_navigation.coffee
new file mode 100644
index 0000000000000..e24a74ea9b6a2
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_navigation.coffee
@@ -0,0 +1,20 @@
+#= require shortcuts
+
+class @ShortcutsNavigation extends Shortcuts
+  constructor: ->
+    super()
+    Mousetrap.bind('g a', -> ShortcutsNavigation.findAndollowLink('.shortcuts-activity'))
+    Mousetrap.bind('g f', -> ShortcutsNavigation.findAndollowLink('.shortcuts-tree'))
+    Mousetrap.bind('g c', -> ShortcutsNavigation.findAndollowLink('.shortcuts-commits'))
+    Mousetrap.bind('g n', -> ShortcutsNavigation.findAndollowLink('.shortcuts-network'))
+    Mousetrap.bind('g g', -> ShortcutsNavigation.findAndollowLink('.shortcuts-graphs'))
+    Mousetrap.bind('g i', -> ShortcutsNavigation.findAndollowLink('.shortcuts-issues'))
+    Mousetrap.bind('g m', -> ShortcutsNavigation.findAndollowLink('.shortcuts-merge_requests'))
+    Mousetrap.bind('g w', -> ShortcutsNavigation.findAndollowLink('.shortcuts-wiki'))
+    Mousetrap.bind('g s', -> ShortcutsNavigation.findAndollowLink('.shortcuts-snippets'))
+    @enabledHelp.push('.hidden-shortcut.project')
+   
+  @findAndollowLink: (selector) ->
+   link = $(selector).attr('href')
+   if link
+     window.location = link
diff --git a/app/assets/javascripts/shortcuts_network.js.coffee b/app/assets/javascripts/shortcuts_network.js.coffee
new file mode 100644
index 0000000000000..cc95ad7ebfed1
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_network.js.coffee
@@ -0,0 +1,12 @@
+#= require shortcuts_navigation
+
+class @ShortcutsNetwork extends ShortcutsNavigation
+  constructor: (@graph) ->
+    super()
+    Mousetrap.bind(['left', 'h'], @graph.scrollLeft)
+    Mousetrap.bind(['right', 'l'], @graph.scrollRight)
+    Mousetrap.bind(['up', 'k'], @graph.scrollUp)
+    Mousetrap.bind(['down', 'j'], @graph.scrollDown)
+    Mousetrap.bind(['shift+up', 'shift+k'], @graph.scrollTop)
+    Mousetrap.bind(['shift+down', 'shift+j'],  @graph.scrollBottom)
+    @enabledHelp.push('.hidden-shortcut.network')
diff --git a/app/assets/stylesheets/main/layout.scss b/app/assets/stylesheets/main/layout.scss
index e28da65c01fec..2800feb81f2c7 100644
--- a/app/assets/stylesheets/main/layout.scss
+++ b/app/assets/stylesheets/main/layout.scss
@@ -16,3 +16,4 @@ body {
 .container .content {
   margin: 0 0;
 }
+
diff --git a/app/assets/stylesheets/sections/help.scss b/app/assets/stylesheets/sections/help.scss
index 90ed98ba25fbd..07c62f98c36d4 100644
--- a/app/assets/stylesheets/sections/help.scss
+++ b/app/assets/stylesheets/sections/help.scss
@@ -17,3 +17,56 @@
     }
   }
 }
+
+
+.shortcut-mappings {
+  font-size: 12px;
+  color: #555;
+
+  tbody:first-child tr:first-child {
+    padding-top: 0
+  }
+
+  th {
+    padding-top: 15px;
+    font-size: 14px;
+    line-height: 1.5;
+    color: #333;
+    text-align: left
+  }
+
+  td {
+    padding-top: 3px;
+    padding-bottom: 3px;
+    vertical-align: top;
+    line-height: 20px
+  }
+
+  .shortcut {
+    padding-right: 10px;
+    color: #999;
+    text-align: right;
+    white-space: nowrap
+  }
+
+  .key {
+    @extend .label;
+    @extend .label-inverse;
+    font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
+    padding: 3px 5px;
+  }
+}
+
+.modal-body {
+  position: relative;
+  overflow-y: auto;
+  padding: 15px;
+}
+
+body.modal-open {
+  overflow: hidden;
+}
+
+.modal .modal-dialog {
+  width: 860px;
+}
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 500e5dc65e116..4301a6eafc1da 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -3,30 +3,207 @@
     .modal-content
       .modal-header
         %a.close{href: "#", "data-dismiss" => "modal"} ×
-        %h3 Keyboard Shortcuts
-      .modal-body
-        %h5 Global Shortcuts
-        %p
-          %span.label.label-inverse s
-          &ndash;
-          Focus Search
-        %p
-          %span.label.label-inverse ?
-          &ndash;
-          Show this dialog
+        %h4 
+          Keyboard Shortcuts
+          %small 
+            = link_to '(Show all)', '#', class: 'js-more-help-button'
+      .modal-body.shortcuts-cheatsheet
+        .col-lg-4
+          %table.shortcut-mappings
+            %tbody
+              %tr
+                %th
+                %th Global Shortcuts
+              %tr
+                %td.shortcut 
+                  .key s
+                %td Focus Search
+              %tr
+                %td.shortcut 
+                  .key ?
+                %td Show this dialog
+            %tbody
+              %tr
+                %th
+                %th Project Files browsing
+              %tr
+                %td.shortcut
+                  .key
+                    %i.icon-arrow-up
+                %td Move selection up
+              %tr
+                %td.shortcut
+                  .key
+                    %i.icon-arrow-down
+                %td Move selection down
+              %tr
+                %td.shortcut 
+                  .key enter
+                %td Open Selection
 
-        %h5 Project Files browsing
-        %p
-          %span.label.label-inverse
-            %i.icon-arrow-up
-          &ndash;
-          Move selection up
-        %p
-          %span.label.label-inverse
-            %i.icon-arrow-down
-          &ndash;
-          Move selection down
-        %p
-          %span.label.label-inverse Enter
-          &ndash;
-          Open selection
+        .col-lg-4
+          %table.shortcut-mappings
+            %tbody{ class: 'hidden-shortcut project', style: 'display:none' }
+              %tr
+                %th
+                %th Global Dashboard
+              %tr
+                %td.shortcut 
+                  .key g
+                  .key a
+                %td
+                  Go to the activity feed
+              %tr
+                %td.shortcut 
+                  .key g
+                  .key p
+                %td
+                  Go to projects
+              %tr
+                %td.shortcut 
+                  .key g
+                  .key i
+                %td
+                  Go to issues
+              %tr
+                %td.shortcut 
+                  .key g
+                  .key m
+                %td
+                  Go to merge requests
+            %tbody
+              %tr
+                %th
+                %th Project
+              %tr
+                %td.shortcut 
+                  .key g
+                  .key a
+                %td
+                  Go to the activity feed
+              %tr
+                %td.shortcut 
+                  .key g
+                  .key f
+                %td
+                  Go to files
+              %tr
+                %td.shortcut 
+                  .key g
+                  .key c
+                %td
+                  Go to commits
+              %tr
+                %td.shortcut 
+                  .key g
+                  .key n
+                %td
+                  Go to network graph
+              %tr
+                %td.shortcut 
+                  .key g
+                  .key g
+                %td
+                  Go to graphs
+              %tr
+                %td.shortcut 
+                  .key g
+                  .key i
+                %td
+                  Go to issues
+              %tr
+                %td.shortcut 
+                  .key g
+                  .key m
+                %td
+                  Go to merge requests
+              %tr
+                %td.shortcut
+                  .key g
+                  .key s
+                %td
+                  Go to snippets
+        .col-lg-4
+          %table.shortcut-mappings
+            %tbody{ class: 'hidden-shortcut network', style: 'display:none' }
+              %tr
+                %th
+                %th Network Graph
+              %tr
+                %td.shortcut
+                  .key
+                    %i.icon-arrow-left
+                  \/
+                  .key h
+                %td Scroll left
+              %tr
+                %td.shortcut
+                  .key
+                    %i.icon-arrow-right
+                  \/
+                  .key l
+                %td Scroll right
+              %tr
+                %td.shortcut
+                  .key
+                    %i.icon-arrow-up
+                  \/
+                  .key k
+                %td Scroll up
+              %tr
+                %td.shortcut
+                  .key
+                    %i.icon-arrow-down
+                  \/
+                  .key j
+                %td Scroll down
+              %tr
+                %td.shortcut
+                  .key
+                    shift
+                    %i.icon-arrow-up
+                  \/
+                  .key
+                    shift k
+                %td Scroll to top
+              %tr
+                %td.shortcut
+                  .key
+                    shift
+                    %i.icon-arrow-down
+                  \/
+                  .key
+                    shift j
+                %td Scroll to bottom
+            %tbody{ class: 'hidden-shortcut issues', style: 'display:none' }
+              %tr
+                %th
+                %th Issues
+              %tr
+                %td.shortcut 
+                  .key a
+                %td Change assignee
+              %tr
+                %td.shortcut 
+                  .key m
+                %td Change milestone
+            %tbody{ class: 'hidden-shortcut merge_reuests', style: 'display:none' }
+              %tr
+                %th
+                %th Merge Requests
+              %tr
+                %td.shortcut 
+                  .key a
+                %td Change assignee
+              %tr
+                %td.shortcut 
+                  .key m
+                %td Change milestone
+
+
+:javascript
+  $('.js-more-help-button').click(function(e){
+      $(this).remove()
+      $('.hidden-shortcut').show()
+      e.preventDefault()
+  });
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 219693af09f70..903e093e5fcaf 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -37,8 +37,8 @@
           = link_to "getting help", "https://www.gitlab.com/getting-help/"
         %li
           Use the
-          = link_to "search bar", '#', onclick: "$('#search').focus();"
+          = link_to 'search bar', '#', onclick: 'Shortcuts.focusSearch(event)'
           on the top of this page
         %li
           Use
-          = link_to "shortcuts", '#', onclick: "new Shortcuts()"
+          = link_to 'shortcuts', '#', onclick: 'Shortcuts.showHelp(event)'
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index caf0e39234ade..f485aee1e1a0c 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -8,3 +8,10 @@
     = hidden_field_tag :repository_ref, @ref
     = submit_tag 'Go' if ENV['RAILS_ENV'] == 'test'
     .search-autocomplete-opts.hide{:'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref }
+
+:javascript
+  $('.search-input').on('keyup', function(e) {
+    if (e.keyCode == 27) {
+      $('.search-input').blur()
+    }
+  })
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index a300bbc1904e5..a6e9772d93f50 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,16 +1,16 @@
 %ul
   = nav_link(path: 'dashboard#show', html_options: {class: 'home'}) do
-    = link_to root_path, title: "Home" do
+    = link_to root_path, title: 'Home', class: 'shortcuts-activity' do
       Activity
   = nav_link(path: 'dashboard#projects') do
-    = link_to projects_dashboard_path do
+    = link_to projects_dashboard_path, class: 'shortcuts-projects' do
       Projects
   = nav_link(path: 'dashboard#issues') do
-    = link_to issues_dashboard_path do
+    = link_to issues_dashboard_path, class: 'shortcuts-issues' do
       Issues
       %span.count= current_user.assigned_issues.opened.count
   = nav_link(path: 'dashboard#merge_requests') do
-    = link_to merge_requests_dashboard_path do
+    = link_to merge_requests_dashboard_path, class: 'shortcuts-merge_requests' do
       Merge Requests
       %span.count= current_user.assigned_merge_requests.opened.count
   = nav_link(controller: :help) do
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 92ef7923714f5..b26bc797e63c6 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -1,44 +1,43 @@
-%ul
+%ul.project-navigation
   = nav_link(path: 'projects#show', html_options: {class: "home"}) do
-    = link_to project_path(@project), title: "Project" do
-      Project
-
+    = link_to project_path(@project), title: 'Project', class: 'shortcuts-activity' do
+      Activity
   - if project_nav_tab? :files
     = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do
-      = link_to 'Files', project_tree_path(@project, @ref || @repository.root_ref)
+      = link_to 'Files', project_tree_path(@project, @ref || @repository.root_ref), class: 'shortcuts-tree'
 
   - if project_nav_tab? :commits
     = nav_link(controller: %w(commit commits compare repositories tags branches)) do
-      = link_to "Commits", project_commits_path(@project, @ref || @repository.root_ref)
+      = link_to "Commits", project_commits_path(@project, @ref || @repository.root_ref), class: 'shortcuts-commits'
 
   - if project_nav_tab? :network
     = nav_link(controller: %w(network)) do
-      = link_to "Network", project_network_path(@project, @ref || @repository.root_ref)
+      = link_to "Network", project_network_path(@project, @ref || @repository.root_ref), class: 'shortcuts-network'
 
   - if project_nav_tab? :graphs
     = nav_link(controller: %w(graphs)) do
-      = link_to "Graphs", project_graph_path(@project, @ref || @repository.root_ref)
+      = link_to "Graphs", project_graph_path(@project, @ref || @repository.root_ref), class: 'shortcuts-graphs'
 
   - if project_nav_tab? :issues
     = nav_link(controller: %w(issues milestones labels)) do
-      = link_to url_for_project_issues do
+      = link_to url_for_project_issues, class: 'shortcuts-issues' do
         Issues
         - if @project.used_default_issues_tracker?
           %span.count.issue_counter= @project.issues.opened.count
 
   - if project_nav_tab? :merge_requests
     = nav_link(controller: :merge_requests) do
-      = link_to project_merge_requests_path(@project) do
+      = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests'  do
         Merge Requests
         %span.count.merge_counter= @project.merge_requests.opened.count
 
   - if project_nav_tab? :wiki
     = nav_link(controller: :wikis) do
-      = link_to 'Wiki', project_wiki_path(@project, :home)
+      = link_to 'Wiki', project_wiki_path(@project, :home), class: 'shortcuts-wiki'
 
   - if project_nav_tab? :snippets
     = nav_link(controller: :snippets) do
-      = link_to 'Snippets', project_snippets_path(@project)
+      = link_to 'Snippets', project_snippets_path(@project), class: 'shortcuts-snippets'
 
   - if project_nav_tab? :settings
     = nav_link(html_options: {class: "#{project_tab_class}"}) do
diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml
index d7987f43fbb41..8c3f08233866f 100644
--- a/app/views/projects/issues/_issue_context.html.haml
+++ b/app/views/projects/issues/_issue_context.html.haml
@@ -5,7 +5,7 @@
         Assignee:
 
       - if can?(current_user, :modify_issue, @issue)
-        = project_users_select_tag('issue[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control', selected: @issue.assignee_id)
+        = project_users_select_tag('issue[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @issue.assignee_id)
       - elsif issue.assignee
         = link_to_member(@project, @issue.assignee)
       - else
@@ -15,7 +15,7 @@
       %strong.append-right-10
         Milestone:
       - if can?(current_user, :modify_issue, @issue)
-        = f.select(:milestone_id, milestone_options(@issue), { include_blank: "Select milestone" }, {class: 'select2 select2-compact'})
+        = f.select(:milestone_id, milestone_options(@issue), { include_blank: "Select milestone" }, {class: 'select2 select2-compact js-select2 js-milestone'})
         = hidden_field_tag :issue_context
         = f.submit class: 'btn'
       - elsif issue.milestone
diff --git a/app/views/projects/merge_requests/show/_context.html.haml b/app/views/projects/merge_requests/show/_context.html.haml
index ab00b34242a0e..089302e358828 100644
--- a/app/views/projects/merge_requests/show/_context.html.haml
+++ b/app/views/projects/merge_requests/show/_context.html.haml
@@ -5,7 +5,7 @@
         Assignee:
 
       - if can?(current_user, :modify_merge_request, @merge_request)
-        = project_users_select_tag('merge_request[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control', selected: @merge_request.assignee_id)
+        = project_users_select_tag('merge_request[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @merge_request.assignee_id)
       - elsif merge_request.assignee
         = link_to_member(@project, @merge_request.assignee)
       - else
@@ -15,7 +15,7 @@
       %strong.append-right-10
         Milestone:
       - if can?(current_user, :modify_merge_request, @merge_request)
-        = f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2 select2-compact'})
+        = f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2 select2-compact js-select2 js-milestone'})
         = hidden_field_tag :merge_request_context
         = f.submit class: 'btn'
       - elsif merge_request.milestone
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index 5310822823d95..8356bef28b06c 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -15,9 +15,10 @@
     = spinner nil, true
 
 :javascript
-  new Network({
+  network_graph = new Network({
     url: '#{project_network_path(@project, @ref, @options.merge(format: :json))}',
     commit_url: '#{project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")}',
     ref: '#{@ref}',
     commit_id: '#{@commit.id}'
   })
+  new ShortcutsNetwork(network_graph.branch_graph)
diff --git a/features/dashboard/shortcuts.feature b/features/dashboard/shortcuts.feature
new file mode 100644
index 0000000000000..7c25b3926c9d7
--- /dev/null
+++ b/features/dashboard/shortcuts.feature
@@ -0,0 +1,21 @@
+@dashboard
+Feature: Dashboard shortcuts
+  Background:
+    Given I sign in as a user
+    And I visit dashboard page
+
+  @javascript
+  Scenario: Navigate to projects tab
+    Given I press "g" and "p"
+    Then the active main tab should be Projects
+
+  @javascript
+  Scenario: Navigate to issue tab
+    Given I press "g" and "i"
+    Then the active main tab should be Issues
+
+  @javascript
+  Scenario: Navigate to merge requests tab
+    Given I press "g" and "m"
+    Then the active main tab should be Merge Requests
+
diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature
new file mode 100644
index 0000000000000..16882fded8e46
--- /dev/null
+++ b/features/project/shortcuts.feature
@@ -0,0 +1,46 @@
+@dashboard
+Feature: Project shortcuts
+  Background:
+    Given I sign in as a user
+    And I own a project
+    And I visit my project's home page
+
+  @javascript
+  Scenario: Navigate to files tab
+    Given I press "g" and "f"
+    Then the active main tab should be Files
+
+  @javascript
+  Scenario: Navigate to commits tab
+    Given I press "g" and "c"
+    Then the active main tab should be Commits
+
+  @javascript
+  Scenario: Navigate to network tab
+    Given I press "g" and "n"
+    Then the active main tab should be Network
+
+  @javascript
+  Scenario: Navigate to graphs tab
+    Given I press "g" and "g"
+    Then the active main tab should be Graphs
+
+  @javascript
+  Scenario: Navigate to issues tab
+    Given I press "g" and "i"
+    Then the active main tab should be Issues
+
+  @javascript
+  Scenario: Navigate to merge requests tab
+    Given I press "g" and "m"
+    Then the active main tab should be Merge Requests
+
+  @javascript
+  Scenario: Navigate to snippets tab
+    Given I press "g" and "s"
+    Then the active main tab should be Snippets
+
+  @javascript
+  Scenario: Navigate to wiki tab
+    Given I press "g" and "w"
+    Then the active main tab should be Wiki
diff --git a/features/steps/dashboard/active_tab.rb b/features/steps/dashboard/active_tab.rb
index 68d32ed971add..d5db3339df278 100644
--- a/features/steps/dashboard/active_tab.rb
+++ b/features/steps/dashboard/active_tab.rb
@@ -3,19 +3,7 @@ class DashboardActiveTab < Spinach::FeatureSteps
   include SharedPaths
   include SharedActiveTab
 
-  Then 'the active main tab should be Home' do
-    ensure_active_main_tab('Activity')
-  end
-
-  Then 'the active main tab should be Issues' do
-    ensure_active_main_tab('Issues')
-  end
-
-  Then 'the active main tab should be Merge Requests' do
-    ensure_active_main_tab('Merge Requests')
-  end
-
-  Then 'the active main tab should be Help' do
+  step 'the active main tab should be Help' do
     ensure_active_main_tab('Help')
   end
 end
diff --git a/features/steps/dashboard/shortcuts.rb b/features/steps/dashboard/shortcuts.rb
new file mode 100644
index 0000000000000..d4484e7a20f88
--- /dev/null
+++ b/features/steps/dashboard/shortcuts.rb
@@ -0,0 +1,6 @@
+class DashboardShortcuts < Spinach::FeatureSteps
+  include SharedAuthentication
+  include SharedPaths
+  include SharedProject
+  include SharedActiveTab
+end
diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb
index e39c0b65b91b4..2862256e03bcb 100644
--- a/features/steps/project/active_tab.rb
+++ b/features/steps/project/active_tab.rb
@@ -3,44 +3,7 @@ class ProjectActiveTab < Spinach::FeatureSteps
   include SharedPaths
   include SharedProject
   include SharedActiveTab
-
-  # Main Tabs
-
-  Then 'the active main tab should be Home' do
-    ensure_active_main_tab('Project')
-  end
-
-  Then 'the active main tab should be Settings' do
-    ensure_active_main_tab('Settings')
-  end
-
-  Then 'the active main tab should be Files' do
-    ensure_active_main_tab('Files')
-  end
-
-  Then 'the active main tab should be Commits' do
-    ensure_active_main_tab('Commits')
-  end
-
-  Then 'the active main tab should be Network' do
-    ensure_active_main_tab('Network')
-  end
-
-  Then 'the active main tab should be Issues' do
-    ensure_active_main_tab('Issues')
-  end
-
-  Then 'the active main tab should be Merge Requests' do
-    ensure_active_main_tab('Merge Requests')
-  end
-
-  Then 'the active main tab should be Wall' do
-    ensure_active_main_tab('Wall')
-  end
-
-  Then 'the active main tab should be Wiki' do
-    ensure_active_main_tab('Wiki')
-  end
+  include SharedProjectTab
 
   # Sub Tabs: Home
 
diff --git a/features/steps/project/project_shortcuts.rb b/features/steps/project/project_shortcuts.rb
new file mode 100644
index 0000000000000..ce6e21a4258de
--- /dev/null
+++ b/features/steps/project/project_shortcuts.rb
@@ -0,0 +1,36 @@
+class ProjectShortcuts < Spinach::FeatureSteps
+  include SharedAuthentication
+  include SharedPaths
+  include SharedProject
+  include SharedProjectTab
+
+  step 'I press "g" and "f"' do
+    find('body').native.send_key('g')
+    find('body').native.send_key('f')
+  end
+
+  step 'I press "g" and "c"' do
+    find('body').native.send_key('g')
+    find('body').native.send_key('c')
+  end
+
+  step 'I press "g" and "n"' do
+    find('body').native.send_key('g')
+    find('body').native.send_key('n')
+  end
+
+  step 'I press "g" and "g"' do
+    find('body').native.send_key('g')
+    find('body').native.send_key('g')
+  end
+
+  step 'I press "g" and "s"' do
+    find('body').native.send_key('g')
+    find('body').native.send_key('s')
+  end
+
+  step 'I press "g" and "w"' do
+    find('body').native.send_key('g')
+    find('body').native.send_key('w')
+  end
+end
diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb
index e3cd5fcfe8563..c776af14e046a 100644
--- a/features/steps/shared/active_tab.rb
+++ b/features/steps/shared/active_tab.rb
@@ -24,4 +24,24 @@ def ensure_active_sub_nav(content)
   And 'no other sub navs should be active' do
     page.should have_selector('div.content ul.nav-stacked-menu li.active', count: 1)
   end
+
+  step 'the active main tab should be Home' do
+    ensure_active_main_tab('Activity')
+  end
+
+  step 'the active main tab should be Projects' do
+    ensure_active_main_tab('Projects')
+  end
+
+  step 'the active main tab should be Issues' do
+    ensure_active_main_tab('Issues')
+  end
+
+  step 'the active main tab should be Merge Requests' do
+    ensure_active_main_tab('Merge Requests')
+  end
+
+  step 'the active main tab should be Help' do
+    ensure_active_main_tab('Help')
+  end
 end
diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb
new file mode 100644
index 0000000000000..00630da83a372
--- /dev/null
+++ b/features/steps/shared/project_tab.rb
@@ -0,0 +1,44 @@
+module SharedProjectTab
+  include Spinach::DSL
+  include SharedActiveTab
+
+  step 'the active main tab should be Home' do
+    ensure_active_main_tab('Activity')
+  end
+
+  step 'the active main tab should be Files' do
+    ensure_active_main_tab('Files')
+  end
+
+  step 'the active main tab should be Commits' do
+    ensure_active_main_tab('Commits')
+  end
+
+  step 'the active main tab should be Network' do
+    ensure_active_main_tab('Network')
+  end
+
+  step 'the active main tab should be Graphs' do
+    ensure_active_main_tab('Graphs')
+  end
+
+  step 'the active main tab should be Issues' do
+    ensure_active_main_tab('Issues')
+  end
+
+  step 'the active main tab should be Merge Requests' do
+    ensure_active_main_tab('Merge Requests')
+  end
+
+  step 'the active main tab should be Snippets' do
+    ensure_active_main_tab('Snippets')
+  end
+
+  step 'the active main tab should be Wiki' do
+    ensure_active_main_tab('Wiki')
+  end
+
+  step 'the active main tab should be Settings' do
+    ensure_active_main_tab('Settings')
+  end
+end
diff --git a/features/steps/shared/shortcuts.rb b/features/steps/shared/shortcuts.rb
new file mode 100644
index 0000000000000..bbb7afec0ad33
--- /dev/null
+++ b/features/steps/shared/shortcuts.rb
@@ -0,0 +1,18 @@
+module SharedActiveTab
+  include Spinach::DSL
+
+  step 'I press "g" and "p"' do
+    find('body').native.send_key('g')
+    find('body').native.send_key('p')
+  end
+
+  step 'I press "g" and "i"' do
+    find('body').native.send_key('g')
+    find('body').native.send_key('i')
+  end
+
+  step 'I press "g" and "m"' do
+    find('body').native.send_key('g')
+    find('body').native.send_key('m')
+  end
+end
-- 
GitLab