diff --git a/Gruntfile.js b/Gruntfile.js index 8486c90947..f61c12d18c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -507,6 +507,7 @@ var path = require('path'), 'bower_components/codemirror/mode/gfm/gfm.js', 'bower_components/showdown/src/showdown.js', 'bower_components/moment/moment.js', + 'bower_components/keymaster/keymaster.js', 'bower_components/jquery-ui/ui/jquery-ui.js', 'bower_components/jquery-file-upload/js/jquery.fileupload.js', diff --git a/bower.json b/bower.json index eef3301fa2..7a94f0b430 100644 --- a/bower.json +++ b/bower.json @@ -16,6 +16,7 @@ "jquery-file-upload": "9.5.6", "jquery-hammerjs": "1.0.1", "jquery-ui": "1.10.4", + "keymaster": "madrobby/keymaster#0f09fc1b7e66c2b7e07afe89a419366dcf2d1cd8", "lodash": "2.4.1", "moment": "2.4.0", "nprogress": "0.1.2", diff --git a/core/client/initializers/popover.js b/core/client/initializers/popover.js index 834bf7f266..58afaffc10 100644 --- a/core/client/initializers/popover.js +++ b/core/client/initializers/popover.js @@ -22,6 +22,7 @@ var popoverInitializer = { application.inject('component:gh-popover', 'popover', 'popover:service'); application.inject('component:gh-popover-button', 'popover', 'popover:service'); application.inject('controller:modals.delete-post', 'popover', 'popover:service'); + application.inject('route:application', 'popover', 'popover:service'); } }; diff --git a/core/client/mixins/editor-route-base.js b/core/client/mixins/editor-route-base.js new file mode 100644 index 0000000000..ee3ae5fa6d --- /dev/null +++ b/core/client/mixins/editor-route-base.js @@ -0,0 +1,25 @@ +import ShortcutsRoute from 'ghost/mixins/shortcuts-route'; +import styleBody from 'ghost/mixins/style-body'; + +var EditorRouteBase = Ember.Mixin.create(styleBody, ShortcutsRoute, { + shortcuts: { + 'ctrl+s, command+s': 'save', + 'ctrl+alt+p': 'publish', + 'ctrl+alt+z': 'toggleZenMode' + }, + actions: { + save: function () { + this.get('controller').send('save'); + }, + publish: function () { + var controller = this.get('controller'); + controller.send('setSaveType', 'publish'); + controller.send('save'); + }, + toggleZenMode: function () { + Ember.$('body').toggleClass('zen'); + } + } +}); + +export default EditorRouteBase; diff --git a/core/client/mixins/shortcuts-route.js b/core/client/mixins/shortcuts-route.js new file mode 100644 index 0000000000..21b258f2f7 --- /dev/null +++ b/core/client/mixins/shortcuts-route.js @@ -0,0 +1,61 @@ +/* global key, console */ + +//Configure KeyMaster to respond to all shortcuts, +//even inside of +//input, textarea, and select. +key.filter = function () { + return true; +}; + +/** + * Only routes can implement shortcuts. + * If you need to trigger actions on the controller, + * simply call them with `this.get('controller').send('action')`. + * + * To implement shortcuts, add this mixin to your `extend()`, + * and implement a `shortcuts` hash. + * In this hash, keys are shortcut combinations + * (see [keymaster docs](https://github.com/madrobby/keymaster/blob/master/README.markdown)), and values are controller action names. + * ```javascript + * shortcuts: { + * 'ctrl+s, command+s': 'save', + * 'ctrl+alt+p': 'toggleZenMode' + * } + * ``` + */ +var ShortcutsRoute = Ember.Mixin.create({ + registerShortcuts: function () { + var self = this, + shortcuts = this.get('shortcuts'); + + Ember.keys(shortcuts).forEach(function (shortcut) { + key(shortcut, function (event) { + //stop things like ctrl+s from actually opening a save dialogue + event.preventDefault(); + //map the shortcut to its action + self.send(shortcuts[shortcut], event); + }); + }); + }, + removeShortcuts: function () { + var shortcuts = this.get('shortcuts'); + + Ember.keys(shortcuts).forEach(function (shortcut) { + key.unbind(shortcut); + }); + }, + activate: function () { + this._super(); + if (!this.shortcuts) { + console.error('Shortcuts not found on route'); + return; + } + this.registerShortcuts(); + }, + deactivate: function () { + this._super(); + this.removeShortcuts(); + } +}); + +export default ShortcutsRoute; diff --git a/core/client/mixins/style-body.js b/core/client/mixins/style-body.js index 6b427272b0..44479359cd 100644 --- a/core/client/mixins/style-body.js +++ b/core/client/mixins/style-body.js @@ -2,6 +2,7 @@ var styleBody = Ember.Mixin.create({ activate: function () { + this._super(); var cssClasses = this.get('classNames'); if (cssClasses) { @@ -14,6 +15,7 @@ var styleBody = Ember.Mixin.create({ }, deactivate: function () { + this._super(); var cssClasses = this.get('classNames'); Ember.run.schedule('afterRender', null, function () { diff --git a/core/client/routes/application.js b/core/client/routes/application.js index 0e4c7a5a7e..ecd3b34543 100644 --- a/core/client/routes/application.js +++ b/core/client/routes/application.js @@ -1,5 +1,15 @@ -var ApplicationRoute = Ember.Route.extend({ +import ShortcutsRoute from 'ghost/mixins/shortcuts-route'; + +var ApplicationRoute = Ember.Route.extend(ShortcutsRoute, { + shortcuts: { + 'esc': 'closePopups' + }, actions: { + closePopups: function () { + this.get('popover').closePopovers(); + this.get('notifications').closeAll(); + // @todo close modals + }, signedIn: function (user) { // Update the user on all routes and controllers this.container.unregister('user:current'); diff --git a/core/client/routes/editor/edit.js b/core/client/routes/editor/edit.js index 173c61eea3..7548adc6c3 100644 --- a/core/client/routes/editor/edit.js +++ b/core/client/routes/editor/edit.js @@ -1,7 +1,7 @@ -import styleBody from 'ghost/mixins/style-body'; import AuthenticatedRoute from 'ghost/routes/authenticated'; +import base from 'ghost/mixins/editor-route-base'; -var EditorEditRoute = AuthenticatedRoute.extend(styleBody, { +var EditorEditRoute = AuthenticatedRoute.extend(base, { classNames: ['editor'], model: function (params) { diff --git a/core/client/routes/editor/new.js b/core/client/routes/editor/new.js index 92fb7e0e20..58b98abca0 100644 --- a/core/client/routes/editor/new.js +++ b/core/client/routes/editor/new.js @@ -1,7 +1,7 @@ import AuthenticatedRoute from 'ghost/routes/authenticated'; -import styleBody from 'ghost/mixins/style-body'; +import base from 'ghost/mixins/editor-route-base'; -var EditorNewRoute = AuthenticatedRoute.extend(styleBody, { +var EditorNewRoute = AuthenticatedRoute.extend(base, { classNames: ['editor'], model: function () { diff --git a/core/client/routes/posts.js b/core/client/routes/posts.js index 5f9153ccac..8af046f0c6 100644 --- a/core/client/routes/posts.js +++ b/core/client/routes/posts.js @@ -1,5 +1,6 @@ -import styleBody from 'ghost/mixins/style-body'; import AuthenticatedRoute from 'ghost/routes/authenticated'; +import styleBody from 'ghost/mixins/style-body'; +import ShortcutsRoute from 'ghost/mixins/shortcuts-route'; var paginationSettings = { status: 'all', @@ -8,7 +9,7 @@ var paginationSettings = { limit: 15 }; -var PostsRoute = AuthenticatedRoute.extend(styleBody, { +var PostsRoute = AuthenticatedRoute.extend(ShortcutsRoute, styleBody, { classNames: ['manage'], model: function () { @@ -24,9 +25,19 @@ var PostsRoute = AuthenticatedRoute.extend(styleBody, { controller.set('paginationSettings', paginationSettings); }, + shortcuts: { + 'up': 'moveUp', + 'down': 'moveDown' + }, actions: { openEditor: function (post) { this.transitionTo('editor', post); + }, + moveUp: function () { + window.alert('@todo keyboard post navigation: up'); + }, + moveDown: function () { + window.alert('@todo keyboard post navigation: down'); } } }); diff --git a/core/client/utils/notifications.js b/core/client/utils/notifications.js index 824a46f436..db7bb30ee5 100644 --- a/core/client/utils/notifications.js +++ b/core/client/utils/notifications.js @@ -46,6 +46,9 @@ var Notifications = Ember.ArrayProxy.extend({ type: 'warn', message: message }); + }, + closeAll: function () { + window.alert('@TODO implement closeALl notifications'); } });