From 71bee2fd46df5a29a317b2544fd53b149eca9878 Mon Sep 17 00:00:00 2001 From: Matt Enlow Date: Thu, 19 Jun 2014 13:44:44 -0600 Subject: [PATCH] Implement Shortcuts in Ember Closes #2988, #2752 Ref #1463, #2984, # Shortcuts via Keymaster - Added KeyMaster to bower dependencies. KeyMaster is a minimal keyboard shortcuts library. - Added `ShortcutsRouteMixin` for routes that will use shortcuts. Currently, only routes can have shortcuts. See the extensive comment at the top of `core/client/mixins/shortcuts-route.js` for a description of how to implement shortcuts. ## Other Changes - Injected popover service into ApplicationRoute - Created `EditorRouteBase` mixin for the `editor.new` and `editor.edit` routes to mixin. - `StyleBodyMixin` now calls `this._super()` on `activate` and `deactivate` to play nicely with other mixins. ## Shortcuts and Stubs implemented #### Application-Wide - `'esc':'closePopups'` shortcut **stub** to close popovers, modals, and notifcations #### Editor Shortcuts - `'ctrl+s, command+s': 'save'` note that `command` is the `meta` key. - `'ctrl+alt+p': 'publish'` - `'ctrl+alt+z': 'toggleZenMode'` --- ghost/admin/initializers/popover.js | 1 + ghost/admin/mixins/editor-route-base.js | 25 ++++++++++ ghost/admin/mixins/shortcuts-route.js | 61 +++++++++++++++++++++++++ ghost/admin/mixins/style-body.js | 2 + ghost/admin/routes/application.js | 12 ++++- ghost/admin/routes/editor/edit.js | 4 +- ghost/admin/routes/editor/new.js | 4 +- ghost/admin/routes/posts.js | 15 +++++- ghost/admin/utils/notifications.js | 3 ++ 9 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 ghost/admin/mixins/editor-route-base.js create mode 100644 ghost/admin/mixins/shortcuts-route.js diff --git a/ghost/admin/initializers/popover.js b/ghost/admin/initializers/popover.js index 834bf7f266..58afaffc10 100644 --- a/ghost/admin/initializers/popover.js +++ b/ghost/admin/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/ghost/admin/mixins/editor-route-base.js b/ghost/admin/mixins/editor-route-base.js new file mode 100644 index 0000000000..ee3ae5fa6d --- /dev/null +++ b/ghost/admin/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/ghost/admin/mixins/shortcuts-route.js b/ghost/admin/mixins/shortcuts-route.js new file mode 100644 index 0000000000..21b258f2f7 --- /dev/null +++ b/ghost/admin/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/ghost/admin/mixins/style-body.js b/ghost/admin/mixins/style-body.js index 6b427272b0..44479359cd 100644 --- a/ghost/admin/mixins/style-body.js +++ b/ghost/admin/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/ghost/admin/routes/application.js b/ghost/admin/routes/application.js index 0e4c7a5a7e..ecd3b34543 100644 --- a/ghost/admin/routes/application.js +++ b/ghost/admin/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/ghost/admin/routes/editor/edit.js b/ghost/admin/routes/editor/edit.js index 173c61eea3..7548adc6c3 100644 --- a/ghost/admin/routes/editor/edit.js +++ b/ghost/admin/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/ghost/admin/routes/editor/new.js b/ghost/admin/routes/editor/new.js index 92fb7e0e20..58b98abca0 100644 --- a/ghost/admin/routes/editor/new.js +++ b/ghost/admin/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/ghost/admin/routes/posts.js b/ghost/admin/routes/posts.js index 5f9153ccac..8af046f0c6 100644 --- a/ghost/admin/routes/posts.js +++ b/ghost/admin/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/ghost/admin/utils/notifications.js b/ghost/admin/utils/notifications.js index 824a46f436..db7bb30ee5 100644 --- a/ghost/admin/utils/notifications.js +++ b/ghost/admin/utils/notifications.js @@ -46,6 +46,9 @@ var Notifications = Ember.ArrayProxy.extend({ type: 'warn', message: message }); + }, + closeAll: function () { + window.alert('@TODO implement closeALl notifications'); } });