From 6d26685b657f92a935d4dd39795b79a9c1471e47 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Fri, 3 Feb 2017 18:35:13 +0000 Subject: [PATCH] bring keyboard navigation back to the content screen refs https://github.com/TryGhost/Ghost/issues/7860 - restores the previous up/down/enter/cmd+backspace functionality - modifies the `delete-post` modal to accept a hash with an `onSuccess` action --- app/components/gh-posts-list-item.js | 32 +++++++++++++- app/components/modals/delete-post.js | 9 ++-- app/routes/editor/edit.js | 4 ++ app/routes/posts/index.js | 62 ++++++++++++++++++++++++---- app/templates/editor/edit.hbs | 2 +- app/templates/posts/index.hbs | 6 ++- 6 files changed, 101 insertions(+), 14 deletions(-) diff --git a/app/components/gh-posts-list-item.js b/app/components/gh-posts-list-item.js index 9e88dd33a..65bc37a50 100644 --- a/app/components/gh-posts-list-item.js +++ b/app/components/gh-posts-list-item.js @@ -14,9 +14,10 @@ const ObjectPromiseProxy = ObjectProxy.extend(PromiseProxyMixin); export default Component.extend({ tagName: 'li', classNames: ['gh-posts-list-item'], + classNameBindings: ['active'], post: null, - previewIsHidden: false, + active: false, isFeatured: alias('post.featured'), isPage: alias('post.page'), @@ -63,11 +64,40 @@ export default Component.extend({ return htmlSafe(`${text.slice(0, 80)}…`); }), + didReceiveAttrs() { + if (this.get('active')) { + this.scrollIntoView(); + } + }, + click() { this.sendAction('onClick', this.get('post')); }, doubleClick() { this.sendAction('onDoubleClick', this.get('post')); + }, + + scrollIntoView() { + let element = this.$(); + let offset = element.offset().top; + let elementHeight = element.height(); + let container = $('.content-list'); + let containerHeight = container.height(); + let currentScroll = container.scrollTop(); + let isBelowTop, isAboveBottom, isOnScreen; + + isAboveBottom = offset < containerHeight; + isBelowTop = offset > elementHeight; + + isOnScreen = isBelowTop && isAboveBottom; + + if (!isOnScreen) { + // Scroll so that element is centered in container + // 40 is the amount of padding on the container + container.clearQueue().animate({ + scrollTop: currentScroll + offset - 40 - containerHeight / 2 + }); + } } }); diff --git a/app/components/modals/delete-post.js b/app/components/modals/delete-post.js index c817f0247..bfd838676 100644 --- a/app/components/modals/delete-post.js +++ b/app/components/modals/delete-post.js @@ -5,7 +5,8 @@ import {task} from 'ember-concurrency'; export default ModalComponent.extend({ - post: alias('model'), + post: alias('model.post'), + onSuccess: alias('model.onSuccess'), notifications: injectService(), routing: injectService('-routing'), @@ -24,8 +25,10 @@ export default ModalComponent.extend({ // clear any previous error messages this.get('notifications').closeAlerts('post.delete'); - // redirect to content screen - this.get('routing').transitionTo('posts'); + // trigger the success action + if (this.get('onSuccess')) { + this.get('onSuccess')(); + } }, _failure(error) { diff --git a/app/routes/editor/edit.js b/app/routes/editor/edit.js index 2ffcef8a1..f0b1475d0 100644 --- a/app/routes/editor/edit.js +++ b/app/routes/editor/edit.js @@ -49,6 +49,10 @@ export default AuthenticatedRoute.extend(base, { actions: { authorizationFailed() { this.get('controller').send('toggleReAuthenticateModal'); + }, + + redirectToContentScreen() { + this.transitionTo('posts'); } } }); diff --git a/app/routes/posts/index.js b/app/routes/posts/index.js index 004658ee0..9f0010b00 100644 --- a/app/routes/posts/index.js +++ b/app/routes/posts/index.js @@ -12,6 +12,7 @@ export default AuthenticatedRoute.extend(InfinityRoute, ShortcutsRoute, { totalPagesParam: 'meta.pagination.pages', _type: null, + _selectedPostIndex: null, model(params) { this.set('_type', params.type); @@ -60,10 +61,18 @@ export default AuthenticatedRoute.extend(InfinityRoute, ShortcutsRoute, { }), stepThroughPosts(step) { - let currentPost = this.get('controller.currentPost'); - let posts = this.get('controller.sortedPosts'); + let currentPost = this.get('controller.selectedPost'); + let posts = this.get('controller.model'); let length = posts.get('length'); - let newPosition = posts.indexOf(currentPost) + step; + let newPosition; + + // when the currentPost is deleted we won't be able to use indexOf. + // we keep track of the index locally so we can select next after deletion + if (this._selectedPostIndex !== null && length) { + newPosition = this._selectedPostIndex + step; + } else { + newPosition = posts.indexOf(currentPost) + step; + } // if we are on the first or last item // just do nothing (desired behavior is to not @@ -74,20 +83,40 @@ export default AuthenticatedRoute.extend(InfinityRoute, ShortcutsRoute, { return; } - // TODO: highlight post - // this.transitionTo('posts.post', posts.objectAt(newPosition)); + this._selectedPostIndex = newPosition; + this.set('controller.selectedPost', posts.objectAt(newPosition)); }, shortcuts: { 'up, k': 'moveUp', 'down, j': 'moveDown', - c: 'newPost' + 'enter': 'editPost', + 'c': 'newPost', + 'command+backspace, ctrl+backspace': 'deletePost' + }, + + resetController() { + this.set('controller.selectedPost', null); + this.set('controller.showDeletePostModal', false); }, actions: { + willTransition() { + this._selectedPostIndex = null; + + if (this.get('controller')) { + this.resetController(); + } + }, + queryParamsDidChange() { - this.refresh(); - // reset the scroll position + // on direct page load controller won't exist so we want to + // avoid a double transition + if (this.get('controller')) { + this.refresh(); + } + + // scroll back to the top $('.content-list').scrollTop(0); }, @@ -101,6 +130,23 @@ export default AuthenticatedRoute.extend(InfinityRoute, ShortcutsRoute, { moveDown() { this.stepThroughPosts(1); + }, + + editPost() { + let selectedPost = this.get('controller.selectedPost'); + + if (selectedPost) { + this.transitionTo('editor.edit', selectedPost.get('id')); + } + }, + + deletePost() { + this.get('controller').send('toggleDeletePostModal'); + }, + + onPostDeletion() { + // select next post (re-select the current index) + this.stepThroughPosts(0); } } }); diff --git a/app/templates/editor/edit.hbs b/app/templates/editor/edit.hbs index d1719c9a4..8d5822414 100644 --- a/app/templates/editor/edit.hbs +++ b/app/templates/editor/edit.hbs @@ -58,7 +58,7 @@ {{#if showDeletePostModal}} {{gh-fullscreen-modal "delete-post" - model=model + model=(hash post=model onSuccess=(route-action 'redirectToContentScreen')) close=(action "toggleDeletePostModal") modifier="action wide"}} {{/if}} diff --git a/app/templates/posts/index.hbs b/app/templates/posts/index.hbs index a4d72a65e..61484dc4c 100644 --- a/app/templates/posts/index.hbs +++ b/app/templates/posts/index.hbs @@ -3,6 +3,7 @@ {{#each model as |post|}} {{gh-posts-list-item post=post + active=(eq post selectedPost) onDoubleClick="openEditor" data-test-posts-list-item-id=post.id}} {{else}} @@ -28,7 +29,10 @@ {{#if showDeletePostModal}} {{gh-fullscreen-modal "delete-post" - model=currentPost + model=(hash + post=selectedPost + onSuccess=(route-action 'onPostDeletion') + ) close=(action "toggleDeletePostModal") modifier="action wide"}} {{/if}}