Add Global Keyboard Shortcuts

Not all of these have been fully implemented yet.

**Jump To**
* `g` then `h` - Home (Latest)
* `g` then `l` - Latest
* `g` then `n` - New
* `g` then `u` - Unread
* `g` then `f` - Favorited
* `g` then `c` - Categories List

**Navigation**
* `u` - Back to topic list
* `k` / `j` - Newer/Older conversation or post
* `o` or `Enter` - Open selected conversation
* <code>`</code> - Go to next section
* `~` - Go to previous section

**Application**
* `c` - Create a new topic
* `n` - Open notifications menu
* `/` - Search
* `?` - Open keyboard shortcut help

**Actions**
* `f` - Favorite topic
* `s` - Share topic
* `<Shift>` + `s` - Share selected post
* `r` - Reply to topic
* `<Shift>` + `r` - Reply to selected post
* `l` - Like selected post
* `!` - Flag selected post
* `b` - Bookmark selected post
* `e` - Edit selected post
* `d` - Delete selected post
* `m` then `m` - Mark topic as muted
* `m` then `r` - Mark topic as regular
* `m` then `t` - Mark topic as tracking
* `m` then `w` - Mark topic as watching
This commit is contained in:
Ryan Sullivan
2013-09-20 16:33:49 -07:00
parent e44f51f9fa
commit 5100c2bbd2
16 changed files with 471 additions and 5 deletions
@@ -0,0 +1,145 @@
/**
Keyboard Shortcut related functions.
@class KeyboardShortcuts
@namespace Discourse
@module Discourse
**/
Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({
PATH_BINDINGS: {
'g h': '/',
'g l': '/latest',
'g n': '/new',
'g u': '/unread',
'g f': '/favorited',
'g c': '/categories'
},
CLICK_BINDINGS: {
'b': 'article.selected button.bookmark', // bookmark current post
'c': '#create-topic', // create new topic
'd': 'article.selected button.delete', // delete selected post
'e': 'article.selected button.edit', // edit selected post
// favorite topic
'f': '#topic-footer-buttons button.favorite, #topic-list tr.topic-list-item.selected a.star',
'l': 'article.selected button.like', // like selected post
'm m': 'div.notification-options li[data-id="0"] a', // mark topic as muted
'm r': 'div.notification-options li[data-id="1"] a', // mark topic as regular
'm t': 'div.notification-options li[data-id="2"] a', // mark topic as tracking
'm w': 'div.notification-options li[data-id="3"] a', // mark topic as watching
'n': '#user-notifications', // open notifictions menu
'o,enter': '#topic-list tr.topic-list-item.selected a.title', // open selected topic
'r': '#topic-footer-buttons button.create', // reply to topic
'R': 'article.selected button.create', // reply to selected post
's': '#topic-footer-buttons button.share', // share topic
'S': 'article.selected button.share', // share selected post
'/': '#search-button', // focus search
'!': 'article.selected button.flag', // flag selected post
'?': '#keyboard-help' // open keyboard shortcut help
},
FUNCTION_BINDINGS: {
'j': 'selectDown',
'k': 'selectUp',
'u': 'goBack',
'`': 'nextSection',
'~': 'prevSection'
},
bindEvents: function(keyTrapper) {
this.keyTrapper = keyTrapper;
_.each(this.PATH_BINDINGS, this._bindToPath, this);
_.each(this.CLICK_BINDINGS, this._bindToClick, this);
_.each(this.FUNCTION_BINDINGS, this._bindToFunction, this);
},
selectDown: function() {
this._moveSelection(1);
},
selectUp: function() {
this._moveSelection(-1);
},
goBack: function() {
history.back();
},
nextSection: function() {
this._changeSection(1);
},
prevSection: function() {
this._changeSection(-1);
},
_bindToPath: function(path, binding) {
this.keyTrapper.bind(binding, function() {
Discourse.URL.routeTo(path);
});
},
_bindToClick: function(selector, binding) {
binding = binding.split(',');
this.keyTrapper.bind(binding, function(e) {
if (!_.isUndefined(e) && _.isFunction(e.preventDefault)) {
e.preventDefault();
}
$(selector).click();
});
},
_bindToFunction: function(func, binding) {
if (typeof this[func] === 'function') {
this.keyTrapper.bind(binding, _.bind(this[func], this));
}
},
_moveSelection: function(num) {
var $articles = this._findArticles();
if (typeof $articles === 'undefined') {
return;
}
var $selected = $articles.filter('.selected'),
index = $articles.index($selected),
$article = $articles.eq(index + num);
if ($article.size() > 0) {
$articles.removeClass('selected');
$article.addClass('selected');
this._scrollList($article);
}
},
_scrollList: function($article) {
var $body = $('body'),
distToElement = $article.position().top + $article.height() - $(window).height() - $body.scrollTop();
$('html, body').scrollTop($body.scrollTop() + distToElement);
},
_findArticles: function() {
var $topicList = $('#topic-list'),
$topicArea = $('.posts-wrapper');
if ($topicArea.size() > 0) {
return $topicArea.find('.topic-post');
}
else if ($topicList.size() > 0) {
return $topicList.find('.topic-list-item');
}
},
_changeSection: function(num) {
var $sections = $('#category-filter').find('li'),
$active = $sections.filter('.active'),
index = $sections.index('.active');
$sections.eq(index + num).find('a').click();
}
});
@@ -0,0 +1,12 @@
/**
This controller is used to display the Keyboard Shortcuts Help Modal
@class KeyboardShortcutsHelpController
@extends Discourse.Controller
@namespace Discourse
@uses Discourse.ModalFunctionality
@module Discourse
**/
Discourse.KeyboardShortcutsHelpController = Discourse.Controller.extend(Discourse.ModalFunctionality, {
needs: ['modal']
});
@@ -0,0 +1,8 @@
/*global Mousetrap:true*/
/**
Initialize Global Keyboard Shortcuts
**/
Discourse.addInitializer(function(){
Discourse.KeyboardShortcuts.bindEvents(Mousetrap);
})
@@ -31,6 +31,10 @@ Discourse.ApplicationRoute = Em.Route.extend({
this.controllerFor('uploadSelector').setProperties({ composerView: composerView });
},
showKeyboardShortcutsHelp: function() {
Discourse.Route.showModal(this, 'keyboardShortcutsHelp');
},
/**
Close the current modal, and destroy its state.
@@ -52,11 +52,12 @@
</li>
<li>
{{#if Discourse.loginRequired}}
<a class='icon expand' href='#' {{action showLogin}}>
<a id='search-button' class='icon expand' href='#' {{action showLogin}}>
<i class='fa fa-search'></i>
</a>
{{else}}
<a class='icon expand'
<a id='search-button'
class='icon expand'
href='#'
data-dropdown="search-dropdown"
title='{{i18n search.title}}'>
@@ -10,7 +10,7 @@
</ul>
{{#if canCreateTopic}}
<button class='btn btn-default' {{action createTopic}}><i class='fa fa-plus'></i>{{i18n topic.create}}</button>
<button id="create-topic" class='btn btn-default' {{action createTopic}}><i class='fa fa-plus'></i>{{i18n topic.create}}</button>
{{/if}}
{{#if canEditCategory}}
@@ -0,0 +1,50 @@
<div id="keyboard-shortcuts-help" class="modal-body">
<div class="row">
<div class="span6">
<h4>{{i18n keyboard_shortcuts_help.jump_to.title}}</h4>
<ul>
<li>{{{i18n keyboard_shortcuts_help.jump_to.home}}}</li>
<li>{{{i18n keyboard_shortcuts_help.jump_to.latest}}}</li>
<li>{{{i18n keyboard_shortcuts_help.jump_to.new}}}</li>
<li>{{{i18n keyboard_shortcuts_help.jump_to.unread}}}</li>
<li>{{{i18n keyboard_shortcuts_help.jump_to.favorited}}}</li>
<li>{{{i18n keyboard_shortcuts_help.jump_to.categories}}}</li>
</ul>
<h4>{{i18n keyboard_shortcuts_help.navigation.title}}</h4>
<ul>
<li>{{{i18n keyboard_shortcuts_help.navigation.back}}}</li>
<li>{{{i18n keyboard_shortcuts_help.navigation.up_down}}}</li>
<li>{{{i18n keyboard_shortcuts_help.navigation.open}}}</li>
<li>{{{i18n keyboard_shortcuts_help.navigation.next_prev}}}</li>
</ul>
<h4>{{i18n keyboard_shortcuts_help.application.title}}</h4>
<ul>
<li>{{{i18n keyboard_shortcuts_help.application.create}}}</li>
<li>{{{i18n keyboard_shortcuts_help.application.notifications}}}</li>
<li>{{{i18n keyboard_shortcuts_help.application.search}}}</li>
<li>{{{i18n keyboard_shortcuts_help.application.help}}}</li>
</ul>
</div>
<div class="span6">
<h4>{{i18n keyboard_shortcuts_help.actions.title}}</h4>
<ul>
<li>{{{i18n keyboard_shortcuts_help.actions.favorite}}}</li>
<li>{{{i18n keyboard_shortcuts_help.actions.share_topic}}}</li>
<li>{{{i18n keyboard_shortcuts_help.actions.share_post}}}</li>
<li>{{{i18n keyboard_shortcuts_help.actions.reply_topic}}}</li>
<li>{{{i18n keyboard_shortcuts_help.actions.reply_post}}}</li>
<li>{{{i18n keyboard_shortcuts_help.actions.like}}}</li>
<li>{{{i18n keyboard_shortcuts_help.actions.flag}}}</li>
<li>{{{i18n keyboard_shortcuts_help.actions.bookmark}}}</li>
<li>{{{i18n keyboard_shortcuts_help.actions.edit}}}</li>
<li>{{{i18n keyboard_shortcuts_help.actions.delete}}}</li>
<li>{{{i18n keyboard_shortcuts_help.actions.mark_muted}}}</li>
<li>{{{i18n keyboard_shortcuts_help.actions.mark_regular}}}</li>
<li>{{{i18n keyboard_shortcuts_help.actions.mark_tracking}}}</li>
<li>{{{i18n keyboard_shortcuts_help.actions.mark_watching}}}</li>
</ul>
</div>
</div>
</div>
@@ -6,6 +6,7 @@
{{/if}}
<li>{{partial "siteMap/latestTopicsLink"}}</li>
<li>{{partial "siteMap/faqLink"}}</li>
<li>{{partial "siteMap/keyboardShortcutsHelpLink"}}</li>
{{#if showMobileToggle}}
<li>{{partial "siteMap/mobileToggleLink"}}</li>
{{/if}}
@@ -22,4 +23,4 @@
{{/each}}
</ul>
{{/if}}
</section>
</section>
@@ -0,0 +1 @@
<a id="keyboard-help" {{action showKeyboardShortcutsHelp}}>{{i18n keyboard_shortcuts_help.title}}</a>
@@ -7,6 +7,7 @@
@module Discourse
**/
Discourse.FavoriteButton = Discourse.ButtonView.extend({
classNames: ['favorite'],
textKey: 'favorite.title',
helpKeyBinding: 'controller.favoriteTooltipKey',
attributeBindings: ['disabled'],
@@ -7,6 +7,7 @@
@module Discourse
**/
Discourse.NotificationsButton = Discourse.DropdownButtonView.extend({
classNames: ['notification-options'],
title: I18n.t('topic.notifications.title'),
longDescriptionBinding: 'topic.details.notificationReasonText',
topic: Em.computed.alias('controller.model'),
@@ -7,6 +7,7 @@
@module Discourse
**/
Discourse.ShareButton = Discourse.ButtonView.extend({
classNames: ['share'],
textKey: 'topic.share.title',
helpKey: 'topic.share.help',
'data-share-url': Em.computed.alias('topic.shareUrl'),
@@ -0,0 +1,12 @@
/**
A modal view for displaying Keyboard Shortcut Help
@class KeyboardShortcutsHelpView
@extends Discourse.ModalBodyView
@namespace Discourse
@module Discourse
**/
Discourse.KeyboardShortcutsHelpView = Discourse.ModalBodyView.extend({
templateName: 'modal/keyboard_shortcuts_help',
title: I18n.t('keyboard_shortcuts_help.title')
});