Refactor settings routing and mobile interactions

Closes #3254, closes #3138, closes #3245
 ### Settings Routing and View refactoring
- Refactored `SettingsView` to handle transitions between mobile and desktop layouts
- `SettingsRoute` will only transition to `settings.general` if the screen is large enough to show both the menu and the content
- Added `SettingsIndexView` to handle showing the settings menu on mobile screens
- Added `SettingsContentBaseView` to be inherited by any settings view that is not index.
- Updated Settings templates appropriately to work with new views
- Removed extraneous `active` class from `settings-content`
- Changed settings menu to use `gh-activating-list-item`
- Retooled settings tests

 ### Mobile Utils
- Renamed file to `mobile.js`, since it's inside of `utils/`
- Added `mobileQuery` MediaQueryList to help detect layout changes
- Removed unused `hasTouchScreen`, `device.js` should be used instead.
- Removed unused `smallScreen` function
- Moved FastClickInit to codemirror-mobile
This commit is contained in:
Matt Enlow 2014-07-13 11:03:48 -06:00 committed by Matt Enlow
parent 7cc818d262
commit 14f29f5139
18 changed files with 145 additions and 117 deletions

View File

@ -1,8 +1,31 @@
import {mobileQuery} from 'ghost/utils/mobile';
var SettingsIndexRoute = Ember.Route.extend(Ember.SimpleAuth.AuthenticatedRouteMixin, {
// redirect to general tab
redirect: function () {
this.transitionTo('settings.general');
activate: function () {
this._super();
},
// redirect to general tab, unless on a mobile phone
beforeModel: function () {
if (!mobileQuery.matches) {
this.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
this.set('fillOutlet', _.bind(function fillOutlet(mq) {
if (!mq.matches) {
this.transitionTo('settings.general');
}
}, this));
mobileQuery.addListener(this.fillOutlet);
}
},
deactivate: function () {
if (this.get('fillOutlet')) {
mobileQuery.removeListener(this.fillOutlet);
}
}
});

View File

@ -4,23 +4,13 @@
</header>
<nav class="settings-menu">
<ul>
{{#view "item-view" tagName="li" class="general"}}
{{#link-to "settings.general"}}General{{/link-to}}
{{/view}}
{{#view "item-view" tagName="li" class="users"}}
{{#link-to "settings.users"}}Users{{/link-to}}
{{/view}}
{{gh-activating-list-item route="settings.general" title="General" classNames="general"}}
{{gh-activating-list-item route="settings.users" title="Users" classNames="users"}}
{{#if showApps}}
{{#view "item-view" tagName="li" class="apps"}}
{{#link-to "settings.apps"}}Apps{{/link-to}}
{{/view}}
{{gh-activating-list-item route="settings.apps" title="Apps" classNames="apps"}}
{{/if}}
</ul>
</nav>
</aside>
<section class="settings-content active">
{{outlet}}
</section>
{{outlet}}

View File

@ -1,9 +1,9 @@
<header class="fade-in">
<button class="button-back">Back</button>
<header>
{{#link-to 'settings' class='button-back button'}}Back{{/link-to}}
<h2 class="title">Apps</h2>
</header>
<section class="content settings-apps fade-in">
<section class="content settings-apps">
<table class="js-apps">
<thead>
<th>App name</th>

View File

@ -1,8 +1,8 @@
<header class="fade-in">
<header>
<h2 class="title">General</h2>
<div class="settings-header-inner">
<button class="button-back">Back</button>
{{#link-to 'settings' class='button-back button'}}Back{{/link-to}}
<section class="page-actions">
<button class="button-save" {{action "save"}}>Save</button>
@ -10,7 +10,7 @@
</div>
</header>
<section class="content settings-general fade-in">
<section class="content settings-general">
<form id="settings-general" novalidate="novalidate">
<fieldset>

View File

@ -0,0 +1,6 @@
{{!
Yes, this is the template default,
but for some reason things break without it in this instance.
@TODO Find a better fix?
}}
{{outlet}}

View File

@ -1,13 +1,12 @@
<header class="fade-in">
<button class="button-back">Back</button>
<header>
{{#link-to 'settings' class='button-back button'}}Back{{/link-to}}
<h2 class="title">Users</h2>
<section class="page-actions">
<a class="button-add" href="" {{action "openModal" "invite-new-user" this}} >New&nbsp;User</a>
</section>
</header>
<section class="content fade-in settings-users">
<section class="content settings-users">
{{#if invitedUsers}}
<section class="object-list">

View File

@ -1,11 +1,9 @@
<header class="fade-in user-settings-header">
<header class="user-settings-header">
<h2 class="hidden">Your Profile</h2>
<div class="settings-header-inner">
<button class="button-back">Back</button>
{{#link-to 'settings' class='button-back button'}}Back{{/link-to}}
<section class="page-actions page-actions-alt">
{{#link-to "settings.users" class="button has-icon users-back" tagName="button"}}<i class="icon-chevron-left"></i>Users{{/link-to}}
</section>
@ -26,7 +24,7 @@
</header>
<section class="content settings-user no-padding fade-in">
<section class="content settings-user no-padding">
<header class="user-profile-header">
<img id="user-cover" class="cover-image" {{bind-attr src=cover title=coverTitle}} />

View File

@ -1,5 +1,4 @@
/*global CodeMirror, device*/
import mobileUtils from 'ghost/utils/mobile-utils';
/*global CodeMirror, device, FastClick*/
import createTouchEditor from 'ghost/assets/lib/touch-editor';
var setupMobileCodeMirror,
@ -36,7 +35,10 @@ init = function init() {
});
Ember.touchEditor = true;
mobileUtils.initFastClick();
//initialize FastClick to remove touch delays
Ember.run.scheduleOnce('afterRender', null, function () {
FastClick.attach(document.body);
});
TouchEditor = createTouchEditor();
setupMobileCodeMirror();
}

View File

@ -1,48 +0,0 @@
/*global DocumentTouch,FastClick*/
var hasTouchScreen,
smallScreen,
initFastClick,
responsiveAction;
// Taken from "Responsive design & the Guardian" with thanks to Matt Andrews
// Added !window._phantom so that the functional tests run as though this is not a touch screen.
// In future we can do something more advanced here for testing both touch and non touch
hasTouchScreen = function () {
return !window._phantom &&
(
('ontouchstart' in window) ||
(window.DocumentTouch && document instanceof DocumentTouch)
);
};
smallScreen = function () {
if (window.matchMedia('(max-width: 1000px)').matches) {
return true;
}
return false;
};
initFastClick = function () {
Ember.run.scheduleOnce('afterRender', null, function () {
FastClick.attach(document.body);
});
};
responsiveAction = function responsiveAction(event, mediaCondition, cb) {
if (!window.matchMedia(mediaCondition).matches) {
return;
}
event.preventDefault();
event.stopPropagation();
cb();
};
export { hasTouchScreen, smallScreen, responsiveAction };
export default {
hasTouchScreen: hasTouchScreen,
smallScreen: smallScreen,
initFastClick: initFastClick,
responsiveAction: responsiveAction
};

View File

@ -0,0 +1,17 @@
var mobileQuery = matchMedia('(max-width: 800px)'),
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
};

View File

@ -1,4 +1,4 @@
import {responsiveAction} from 'ghost/utils/mobile-utils';
import {responsiveAction} from 'ghost/utils/mobile';
var ApplicationView = Ember.View.extend({

View File

@ -1,12 +1,10 @@
import mobileUtils from 'ghost/utils/mobile-utils';
import {responsiveAction} from 'ghost/utils/mobile';
var PostsView = Ember.View.extend({
classNames: ['content-view-container'],
tagName: 'section',
mobileInteractions: function () {
var responsiveAction = mobileUtils.responsiveAction;
Ember.run.scheduleOnce('afterRender', this, function () {
// ### Show content preview when swiping left on content list
$('.manage').on('click', '.content-list ol li', function (event) {

View File

@ -1,39 +1,44 @@
import mobileUtils from 'ghost/utils/mobile-utils';
import {mobileQuery} from 'ghost/utils/mobile';
var SettingsView = Ember.View.extend({
classNames: ['wrapper'],
// used by SettingsContentBaseView and on resize to mobile from desktop
showSettingsContent: function () {
if (mobileQuery.matches) {
$('.settings-sidebar').animate({right: '100%', left: '-110%', 'margin-right': '15px'}, 300);
$('.settings-content').animate({right: '0', left: '0', 'margin-left': '0'}, 300);
$('.settings-header-inner').css('display', 'block');
}
},
// used by SettingsIndexView
showSettingsMenu: function () {
if (mobileQuery.matches) {
$('.settings-header-inner').css('display', 'none');
$('.settings-sidebar').animate({right: '0', left: '0', 'margin-right': '0'}, 300);
$('.settings-content').animate({right: '-100%', left: '100%', 'margin-left': '15'}, 300);
}
},
showAll: function () {
//Remove any styles applied by jQuery#animate
$('.settings-sidebar, .settings-content').removeAttr('style');
},
mobileInteractions: function () {
var responsiveAction = mobileUtils.responsiveAction;
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'),
Ember.run.scheduleOnce('afterRender', this, function () {
// ### Hide settings page nav items (save, back etc) if the menu is showing
responsiveAction(event, '(max-width: 650px)', function () {
if ($('.settings-sidebar[style]').length === 0) {
$('.settings-header-inner').css('display', 'none');
}
});
// ### Show settings options page when swiping left on settings menu link
$('.settings').on('click', '.settings-menu li', function (event) {
responsiveAction(event, '(max-width: 800px)', function () {
$('.settings-sidebar').animate({right: '100%', left: '-110%', 'margin-right': '15px'}, 300);
$('.settings-content').animate({right: '0', left: '0', 'margin-left': '0'}, 300);
$('.settings-header-inner').css('display', 'block');
});
});
// ### Hide settings options page
$('.settings').on('click', '.settings-content .button-back', function (event) {
responsiveAction(event, '(max-width: 800px)', function () {
$('.settings-header-inner').css('display', 'none');
$('.settings-sidebar').animate({right: '0', left: '0', 'margin-right': '0'}, 300);
$('.settings-content').animate({right: '-100%', left: '100%', 'margin-left': '15'}, 300);
});
});
});
}.on('didInsertElement')
removeMobileInteractions: function () {
mobileQuery.removeListener(this.changeLayout);
}.on('willDestroyElement')
});
export default SettingsView;

View File

@ -0,0 +1,5 @@
import BaseView from 'ghost/views/settings/content-base';
var SettingsAppsView = BaseView.extend();
export default SettingsAppsView;

View File

@ -0,0 +1,15 @@
/**
* 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({
tagName: 'section',
classNames: ['settings-content', 'fade-in'],
showContent: function () {
this.get('parentView').showSettingsContent();
}.on('didInsertElement')
});
export default SettingsContentBaseView;

View File

@ -0,0 +1,5 @@
import BaseView from 'ghost/views/settings/content-base';
var SettingsGeneralView = BaseView.extend();
export default SettingsGeneralView;

View File

@ -0,0 +1,8 @@
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')
});
export default SettingsIndexView;

View File

@ -0,0 +1,5 @@
import BaseView from 'ghost/views/settings/content-base';
var SettingsUsersView = BaseView.extend();
export default SettingsUsersView;