Abstract mobile transition interactions

Closes #4032
- Created "mobile" views: `parent-view`, `content-view` and `index-view`
- `mobile/parent-view` has three callbacks for managing layout, and a mediaQuery listener to keep in sync with the user
- content-view and index-view use their parent-views callbacks to bring themselves into and out of the viewport as appropriate
- fixed media queries for post content list from 800px to 900px
- Created `mobile-index-route` to intelligently transition to a new route on desktops (used by both PostsIndexRoute and SettingsIndexRoute)
- Extract mobile interactions from settings views to new mobile utility views
- `js-` prefixed settings view transitions
- removed unused openEditor action from PostsRoute
- removed unused mobile util "responsiveAction"
This commit is contained in:
Matt Enlow 2014-09-15 22:55:37 -06:00
parent 511307a9db
commit a2a766ba1b
26 changed files with 190 additions and 153 deletions

View File

@ -48,7 +48,7 @@
z-index: 1050;
pointer-events: auto;
@media (max-width: 800px) {
@media (max-width: 900px) {
width: auto;
padding: 10px;
};
@ -57,7 +57,7 @@
min-width: 100px;
}
@media (max-width: 800px) {
@media (max-width: 900px) {
width: 100%;
margin-left: 0;
}
@ -77,7 +77,7 @@
@extend %modal;
padding: 60px 0 30px;
@media (max-width: 800px) {
@media (max-width: 900px) {
padding: 30px 0;
}
}
@ -134,11 +134,11 @@
.modal-style-wide {
width: 550px;
@media (max-width: 800px) {
@media (max-width: 900px) {
width: 100%;
}
}
.modal-style-centered {
text-align: center;
}
}

View File

@ -9,7 +9,7 @@
height: 100%;
width: 100%;
@media (max-width: 800px) {
@media (max-width: 900px) {
overflow-x: hidden;
}
}
@ -24,7 +24,7 @@
border-right: $lightbrown 1px solid;
background: #fff;
@media (max-width: 800px) {
@media (max-width: 900px) {
width: auto;
right: 0;
z-index: 500;
@ -77,7 +77,7 @@
float: right;
text-align: right;
margin-left: 15px;
@media (max-width: 800px) {
@media (max-width: 900px) {
float: none;
}
}
@ -118,7 +118,7 @@
@media (max-width: 400px) {
padding: 15px;
}
@media (max-width: 800px) {
@media (max-width: 900px) {
padding-right: 40px;
}
@ -128,7 +128,7 @@
margin-top: -6px;
right: 15px;
}
@media (min-width: 801px) {
@media (min-width: 901px) {
&:after {
display: none;
}
@ -143,7 +143,7 @@
} // li
li.active {
@media (min-width: 801px) {
@media (min-width: 901px) {
// only apply active styles on larger devices
border-bottom: lighten($midgrey, 40%) 1px solid;
@ -178,7 +178,7 @@
right:0;
overflow: auto;
background: #fff;
@media (max-width: 800px) {
@media (max-width: 900px) {
width: auto;
left: 100%;
right: -100%;
@ -245,7 +245,7 @@
padding: 0px;
display: table;
z-index: 600;
@media (max-width: 800px) {
@media (max-width: 900px) {
position: fixed;
top: 45%;
left: 50%;
@ -255,7 +255,7 @@
vertical-align: middle;
display: table-cell;
text-align: center;
@media (max-width: 800px) {
@media (max-width: 900px) {
display: block;
position: relative;
left: -50%;
@ -268,4 +268,4 @@
}
} // ,no-posts
} // .no-posts-box
} // .no-posts-box

View File

@ -49,7 +49,7 @@
top: 50%;
left: 30px;
transform: translateY(-50%);
transition: color 0.1s;
color: $brown;
@ -139,8 +139,8 @@
// The main content panel on the right
.settings-content {
margin-left: 25%;
@media (max-width: 800px) {
@media (max-width: 900px) {
&.fade-in {
animation: none;
}
@ -312,4 +312,4 @@
float: right;
}
}
}
}

View File

@ -72,7 +72,7 @@
border-width: 10px 8px 10px 0;
}
@media (max-width: 800px) {
@media (max-width: 900px) {
display: inline-block;
}
}
@ -142,4 +142,4 @@
.ghost-popover.open {
display: block !important;
}
}

View File

@ -1,4 +1,3 @@
import {mobileQuery} from 'ghost/utils/mobile';
var PostController = Ember.ObjectController.extend({
isPublished: Ember.computed.equal('status', 'published'),
classNameBindings: ['featured'],
@ -13,17 +12,8 @@ var PostController = Ember.ObjectController.extend({
self.notifications.showErrors(errors);
});
},
hidePostContent: function () {
if (mobileQuery.matches) {
$('.js-content-list').animate({right: '0', left: '0', 'margin-right': '0'}, 300);
$('.js-content-preview').animate({right: '-100%', left: '100%', 'margin-left': '15px'}, 300);
}
},
showPostContent: function () {
if (mobileQuery.matches) {
$('.js-content-list').animate({right: '100%', left: '-100%', 'margin-right': '15px'}, 300);
$('.js-content-preview').animate({right: '0', left: '0', 'margin-left': '0'}, 300);
}
this.transitionToRoute('posts.post', this.get('model'));
}
}
});

View File

@ -0,0 +1,29 @@
import mobileQuery from 'ghost/utils/mobile';
//Routes that extend MobileIndexRoute need to implement
// desktopTransition, a function which is called when
// the user resizes to desktop levels.
var MobileIndexRoute = Ember.Route.extend({
desktopTransition: Ember.K,
activate: function attachDesktopTransition() {
this._super();
mobileQuery.addListener(this.desktopTransitionMQ);
},
deactivate: function removeDesktopTransition() {
this._super();
mobileQuery.removeListener(this.desktopTransitionMQ);
},
setDesktopTransitionMQ: function () {
var self = this;
this.set('desktopTransitionMQ', function desktopTransitionMQ() {
if (!mobileQuery.matches) {
self.desktopTransition();
}
});
}.on('init')
});
export default MobileIndexRoute;

View File

@ -60,9 +60,6 @@ var PostsRoute = Ember.Route.extend(SimpleAuth.AuthenticatedRouteMixin, Shortcut
'down': 'moveDown'
},
actions: {
openEditor: function (post) {
this.transitionTo('editor.edit', post);
},
moveUp: function () {
this.stepThroughPosts(-1);
},

View File

@ -1,28 +1,38 @@
import MobileIndexRoute from 'ghost/routes/mobile-index-route';
import loadingIndicator from 'ghost/mixins/loading-indicator';
import mobileQuery from 'ghost/utils/mobile';
var PostsIndexRoute = Ember.Route.extend(SimpleAuth.AuthenticatedRouteMixin, loadingIndicator, {
//Transition to posts.post if there are any posts the user can see
var PostsIndexRoute = MobileIndexRoute.extend(SimpleAuth.AuthenticatedRouteMixin, loadingIndicator, {
// Transition to a specific post if we're not on mobile
beforeModel: function () {
if (!mobileQuery.matches) {
return this.goToPost();
}
},
goToPost: function () {
var self = this,
// the store has been populated so we can work with the local copy
// the store has been populated by PostsRoute
posts = this.store.all('post'),
post;
return this.store.find('user', 'me').then(function (user) {
// return the first post find that matches the following criteria:
// * User is an author, and is the author of this post
// * User has a role other than author
post = posts.find(function (post) {
// Authors can only see posts they've written
if (user.get('isAuthor')) {
return post.isAuthoredByUser(user);
} else {
return true;
}
return true;
});
if (post) {
return self.transitionTo('posts.post', post);
}
self.get('controller').set('noPosts', true);
});
},
//Mobile posts route callback
desktopTransition: function () {
this.goToPost();
}
});

View File

@ -1,6 +1,5 @@
import loadingIndicator from 'ghost/mixins/loading-indicator';
import ShortcutsRoute from 'ghost/mixins/shortcuts-route';
import {mobileQuery} from 'ghost/utils/mobile';
var PostsPostRoute = Ember.Route.extend(SimpleAuth.AuthenticatedRouteMixin, loadingIndicator, ShortcutsRoute, {
model: function (params) {
@ -53,10 +52,6 @@ var PostsPostRoute = Ember.Route.extend(SimpleAuth.AuthenticatedRouteMixin, load
this._super(controller, model);
this.controllerFor('posts').set('currentPost', model);
if (mobileQuery.matches) {
this.controllerFor('posts.post').send('hidePostContent');
}
},
shortcuts: {

View File

@ -1,36 +1,25 @@
import {mobileQuery} from 'ghost/utils/mobile';
import MobileIndexRoute from 'ghost/routes/mobile-index-route';
import CurrentUserSettings from 'ghost/mixins/current-user-settings';
import mobileQuery from 'ghost/utils/mobile';
var SettingsIndexRoute = Ember.Route.extend(SimpleAuth.AuthenticatedRouteMixin, CurrentUserSettings, {
// redirect to general tab, unless on a mobile phone
var SettingsIndexRoute = MobileIndexRoute.extend(SimpleAuth.AuthenticatedRouteMixin, CurrentUserSettings, {
// Redirect users without permission to view settings,
// and show the settings.general route unless the user
// is mobile
beforeModel: function () {
var self = this;
this.currentUser()
return this.currentUser()
.then(this.transitionAuthor())
.then(this.transitionEditor())
.then(function () {
if (!mobileQuery.matches) {
self.transitionTo('settings.general');
} else {
//fill the empty {{outlet}} in settings.hbs if the user
//goes to fullscreen
//fillOutlet needs special treatment so that it is
//properly bound to this when called from a MQ event
self.set('fillOutlet', _.bind(function fillOutlet(mq) {
if (!mq.matches) {
self.transitionTo('settings.general');
}
}, self));
mobileQuery.addListener(self.fillOutlet);
}
});
},
deactivate: function () {
if (this.get('fillOutlet')) {
mobileQuery.removeListener(this.fillOutlet);
}
desktopTransition: function () {
this.transitionTo('settings.general');
}
});

View File

@ -1,6 +1,8 @@
{{#if noPosts}}
<div class="no-posts-box">
<div class="no-posts">
<h3>You Haven't Written Any Posts Yet!</h3>
{{#link-to "editor.new"}}<button type="button" class="btn btn-green btn-lg" title="New Post">Write a new Post</button>{{/link-to}}
</div>
</div>
{{/if}}

View File

@ -1,5 +1,5 @@
<header class="post-preview-header">
<button type="button" class="btn btn-default btn-back" {{action "hidePostContent"}}>Back</button>
{{#link-to "posts" tagName="button" class="btn btn-default btn-back"}}Back{{/link-to}}
<button type="button" {{bind-attr class="featured:featured:unfeatured"}} title="Feature this post" {{action "toggleFeatured"}}>
<span class="hidden">Star</span>
</button>

View File

@ -4,7 +4,7 @@
</header>
<div class="page-content">
<nav class="settings-menu">
<nav class="settings-menu js-settings-menu">
<ul>
{{#unless session.user.isAuthor}}
{{#unless session.user.isEditor}}

View File

@ -1,6 +1,6 @@
<header class="settings-view-header">
<h2 class="page-title">About</h2>
<div class="settings-header-inner">
<div class="js-settings-header-inner settings-header-inner">
{{#link-to 'settings' class='btn btn-default btn-back'}}Back{{/link-to}}
</div>
</header>

View File

@ -1,17 +1,3 @@
var mobileQuery = matchMedia('(max-width: 900px)'),
var mobileQuery = matchMedia('(max-width: 900px)');
responsiveAction = function responsiveAction(event, mediaCondition, cb) {
if (!window.matchMedia(mediaCondition).matches) {
return;
}
event.preventDefault();
event.stopPropagation();
cb();
};
export { mobileQuery, responsiveAction };
export default {
mobileQuery: mobileQuery,
responsiveAction: responsiveAction
};
export default mobileQuery;

View File

@ -1,4 +1,4 @@
import {mobileQuery} from 'ghost/utils/mobile';
import mobileQuery from 'ghost/utils/mobile';
var ApplicationView = Ember.View.extend({
blogRoot: Ember.computed.alias('controller.ghostPaths.blogRoot'),
@ -33,7 +33,7 @@ var ApplicationView = Ember.View.extend({
$('.js-user-menu-dropdown-menu').removeClass('dropdown-triangle-bottom').addClass('dropdown-triangle-top-right');
}
},
showGlobalMobileNavObserver: function () {
if (this.get('controller.showGlobalMobileNav')) {
$('body').addClass('global-nav-expanded');

View File

@ -0,0 +1,12 @@
import mobileQuery from 'ghost/utils/mobile';
var MobileContentView = Ember.View.extend({
//Ensure that loading this view brings it into view on mobile
showContent: function () {
if (mobileQuery.matches) {
this.get('parentView').showContent();
}
}.on('didInsertElement')
});
export default MobileContentView;

View File

@ -0,0 +1,12 @@
import mobileQuery from 'ghost/utils/mobile';
var MobileIndexView = Ember.View.extend({
//Ensure that going to the index brings the menu into view on mobile.
showMenu: function () {
if (mobileQuery.matches) {
this.get('parentView').showMenu();
}
}.on('didInsertElement')
});
export default MobileIndexView;

View File

@ -0,0 +1,33 @@
import mobileQuery from 'ghost/utils/mobile';
//A mobile parent view needs to implement three methods,
// showContent, showAll, and showMenu
// Which are called by MobileIndex and MobileContent views
var MobileParentView = Ember.View.extend({
showContent: Ember.K,
showMenu: Ember.K,
showAll: Ember.K,
setChangeLayout: function () {
var self = this;
this.set('changeLayout', function changeLayout() {
if (mobileQuery.matches) {
//transitioned to mobile layout, so show content
self.showContent();
} else {
//went from mobile to desktop
self.showAll();
}
});
}.on('init'),
attachChangeLayout: function () {
mobileQuery.addListener(this.changeLayout);
}.on('didInsertElement'),
detachChangeLayout: function () {
mobileQuery.removeListener(this.changeLayout);
}.on('willDestroyElement')
});
export default MobileParentView;

View File

@ -7,9 +7,8 @@ var PostItemView = itemView.extend({
isPage: Ember.computed.alias('controller.model.page'),
//Edit post on double click
doubleClick: function () {
this.get('controller').send('openEditor', this.get('controller.model'));
this.get('controller').send('openEditor');
},
click: function () {

View File

@ -1,21 +1,22 @@
import {mobileQuery} from 'ghost/utils/mobile';
import MobileParentView from 'ghost/views/mobile/parent-view';
var PostsView = Ember.View.extend({
var PostsView = MobileParentView.extend({
classNames: ['content-view-container'],
tagName: 'section',
resetMobileView: function (mq) {
if (!mq.matches) {
$('.js-content-list').removeAttr('style');
$('.js-content-preview').removeAttr('style');
}
// Mobile parent view callbacks
showMenu: function () {
$('.js-content-list').animate({right: '0', left: '0', 'margin-right': '0'}, 300);
$('.js-content-preview').animate({right: '-100%', left: '100%', 'margin-left': '15px'}, 300);
},
attachResetMobileView: function () {
mobileQuery.addListener(this.resetMobileView);
}.on('didInsertElement'),
detachResetMobileView: function () {
mobileQuery.removeListener(this.resetMobileView);
}.on('willDestroyElement')
showContent: function () {
$('.js-content-list').animate({right: '100%', left: '-100%', 'margin-right': '15px'}, 300);
$('.js-content-preview').animate({right: '0', left: '0', 'margin-left': '0'}, 300);
},
showAll: function () {
$('.js-content-list').removeAttr('style');
$('.js-content-preview').removeAttr('style');
}
});
export default PostsView;

5
views/posts/index.js Normal file
View File

@ -0,0 +1,5 @@
import MobileIndexView from 'ghost/views/mobile/index-view';
var PostsIndexView = MobileIndexView.extend();
export default PostsIndexView;

5
views/posts/post.js Normal file
View File

@ -0,0 +1,5 @@
import MobileContentView from 'ghost/views/mobile/content-view';
var PostsPostView = MobileContentView.extend();
export default PostsPostView;

View File

@ -1,43 +1,20 @@
import {mobileQuery} from 'ghost/utils/mobile';
import MobileParentView from 'ghost/views/mobile/parent-view';
var SettingsView = Ember.View.extend({
// used by SettingsContentBaseView and on resize to mobile from desktop
showSettingsContent: function () {
if (mobileQuery.matches) {
$('.settings-menu').css({right: '100%', left: '-110%', 'margin-right': '15px'});
$('.settings-content').css({right: '0', left: '0', 'margin-left': '0'});
$('.settings-header-inner').css('display', 'block');
}
var SettingsView = MobileParentView.extend({
// MobileParentView callbacks
showMenu: function () {
$('.js-settings-header-inner').css('display', 'none');
$('.js-settings-menu').css({right: '0', left: '0', 'margin-right': '0'});
$('.js-settings-content').css({right: '-100%', left: '100%', 'margin-left': '15'});
},
// used by SettingsIndexView
showSettingsMenu: function () {
if (mobileQuery.matches) {
$('.settings-header-inner').css('display', 'none');
$('.settings-menu').css({right: '0', left: '0', 'margin-right': '0'});
$('.settings-content').css({right: '-100%', left: '100%', 'margin-left': '15'});
}
showContent: function () {
$('.js-settings-menu').css({right: '100%', left: '-110%', 'margin-right': '15px'});
$('.js-settings-content').css({right: '0', left: '0', 'margin-left': '0'});
$('.js-settings-header-inner').css('display', 'block');
},
showAll: function () {
//Remove any styles applied by jQuery#css
$('.settings-menu, .settings-content').removeAttr('style');
},
mobileInteractions: function () {
this.set('changeLayout', _.bind(function changeLayout(mq) {
if (mq.matches) {
//transitioned to mobile layout, so show content
this.showSettingsContent();
} else {
//went from mobile to desktop
this.showAll();
}
}, this));
mobileQuery.addListener(this.changeLayout);
}.on('didInsertElement'),
removeMobileInteractions: function () {
mobileQuery.removeListener(this.changeLayout);
}.on('willDestroyElement')
$('.js-settings-menu, .js-settings-content').removeAttr('style');
}
});
export default SettingsView;

View File

@ -1,15 +1,13 @@
import MobileContentView from 'ghost/views/mobile/content-view';
/**
* All settings views other than the index should inherit from this base class.
* It ensures that the correct screen is showing when a mobile user navigates
* to a `settings.someRouteThatIsntIndex` route.
*/
var SettingsContentBaseView = Ember.View.extend({
var SettingsContentBaseView = MobileContentView.extend({
tagName: 'section',
classNames: ['settings-content', 'fade-in'],
showContent: function () {
this.get('parentView').showSettingsContent();
}.on('didInsertElement')
classNames: ['settings-content', 'js-settings-content', 'fade-in']
});
export default SettingsContentBaseView;

View File

@ -1,8 +1,5 @@
var SettingsIndexView = Ember.View.extend({
//Ensure that going to the index brings the menu into view on mobile.
showMenu: function () {
this.get('parentView').showSettingsMenu();
}.on('didInsertElement')
});
import MobileIndexView from 'ghost/views/mobile/index-view';
var SettingsIndexView = MobileIndexView.extend();
export default SettingsIndexView;