mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-08 02:52:39 -05:00
Merge pull request #3259 from novaugust/settings-mobile-routing
Refactor Settings routing and mobile interactions
This commit is contained in:
commit
4d610268c6
19 changed files with 154 additions and 127 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
6
core/client/templates/settings/users.hbs
Normal file
6
core/client/templates/settings/users.hbs
Normal 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}}
|
|
@ -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 User</a>
|
||||
</section>
|
||||
</header>
|
||||
|
||||
<section class="content fade-in settings-users">
|
||||
|
||||
<section class="content settings-users">
|
||||
{{#if invitedUsers}}
|
||||
|
||||
<section class="object-list">
|
||||
|
|
|
@ -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}} />
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
17
core/client/utils/mobile.js
Normal file
17
core/client/utils/mobile.js
Normal 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
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
import {responsiveAction} from 'ghost/utils/mobile-utils';
|
||||
import {responsiveAction} from 'ghost/utils/mobile';
|
||||
|
||||
var ApplicationView = Ember.View.extend({
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
5
core/client/views/settings/apps.js
Normal file
5
core/client/views/settings/apps.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import BaseView from 'ghost/views/settings/content-base';
|
||||
|
||||
var SettingsAppsView = BaseView.extend();
|
||||
|
||||
export default SettingsAppsView;
|
15
core/client/views/settings/content-base.js
Normal file
15
core/client/views/settings/content-base.js
Normal 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;
|
5
core/client/views/settings/general.js
Normal file
5
core/client/views/settings/general.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import BaseView from 'ghost/views/settings/content-base';
|
||||
|
||||
var SettingsGeneralView = BaseView.extend();
|
||||
|
||||
export default SettingsGeneralView;
|
8
core/client/views/settings/index.js
Normal file
8
core/client/views/settings/index.js
Normal 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;
|
5
core/client/views/settings/users.js
Normal file
5
core/client/views/settings/users.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import BaseView from 'ghost/views/settings/content-base';
|
||||
|
||||
var SettingsUsersView = BaseView.extend();
|
||||
|
||||
export default SettingsUsersView;
|
|
@ -8,7 +8,7 @@
|
|||
var generalTabDetector = '.settings-content form#settings-general',
|
||||
usersTabDetector = '.settings-content .settings-users';
|
||||
|
||||
CasperTest.begin('Settings screen is correct', 17, function suite(test) {
|
||||
CasperTest.begin('Settings screen is correct', 16, function suite(test) {
|
||||
casper.thenOpenAndWaitForPageLoad('settings', function testTitleAndUrl() {
|
||||
test.assertTitle('Ghost Admin', 'Ghost admin has no title');
|
||||
test.assertUrlMatch(/ghost\/settings\/general\/$/, 'Landed on the correct URL');
|
||||
|
@ -18,29 +18,28 @@ CasperTest.begin('Settings screen is correct', 17, function suite(test) {
|
|||
test.assertExists('.wrapper', 'Settings main view is present');
|
||||
test.assertExists('.settings-sidebar', 'Settings sidebar view is present');
|
||||
test.assertExists('.settings-menu', 'Settings menu is present');
|
||||
test.assertExists('.settings-menu .general', 'General tab is present');
|
||||
test.assertExists('.settings-menu .users', 'Users tab is present');
|
||||
test.assertNotExists('.settings-menu .apps', 'Apps is present');
|
||||
test.assertExists('.settings-menu .general a', 'General link is present');
|
||||
test.assertExists('.settings-menu .users a', 'Users link is present');
|
||||
test.assertNotExists('.settings-menu .apps a', 'Apps link is present');
|
||||
test.assertExists('.wrapper', 'Settings main view is present');
|
||||
test.assertExists('.settings-content', 'Settings content view is present');
|
||||
test.assertExists('.settings-menu .general.active', 'General tab is marked active');
|
||||
test.assertExists(generalTabDetector, 'Form is present');
|
||||
test.assertSelectorHasText('.settings-content.active h2.title', 'General', 'Title is general');
|
||||
test.assertSelectorHasText('.settings-content h2.title', 'General', 'Title is "General"');
|
||||
});
|
||||
|
||||
casper.then(function testSwitchingTabs() {
|
||||
casper.thenClick('.settings-menu .users a');
|
||||
casper.waitForSelector(usersTabDetector, function then () {
|
||||
// assert that the right menu item is active
|
||||
test.assertExists('.settings-menu .users.active', 'User tab is active');
|
||||
test.assertDoesntExist('.settings-menu .general.active', 'General tab is not active');
|
||||
test.assertExists('.settings-menu .users.active a', 'Users link is active');
|
||||
test.assertDoesntExist('.settings-menu .general.active a', 'General link is not active');
|
||||
}, casper.failOnTimeout(test, 'waitForSelector `usersTabDetector` timed out'));
|
||||
|
||||
casper.thenClick('.settings-menu .general a');
|
||||
casper.waitForSelector(generalTabDetector, function then () {
|
||||
// assert that the right menu item is active
|
||||
test.assertExists('.settings-menu .general.active', 'General tab is active');
|
||||
test.assertDoesntExist('.settings-menu .users.active', 'User tab is not active');
|
||||
test.assertExists('.settings-menu .general.active a', 'General link is active');
|
||||
test.assertDoesntExist('.settings-menu .users.active a', 'User link is not active');
|
||||
}, casper.failOnTimeout(test, 'waitForSelector `generalTabDetector` timed out'));
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue