From 983c6c8d813a2350d6bc3f87df417c7d4a0d04e3 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Fri, 23 Oct 2015 18:18:39 +0100 Subject: [PATCH] Mobile fixes for tag management UI refs #5845, #5969 - when on mobile devices tag management UI will only display a list and when a tag is accessed the tag settings form will slide in from the right - tag settings form header has a 'back' button when on mobile to go back to tags list - switching from mobile to standard modes will auto load the first tag as per standard tags screen on desktop - if no tags are present then the blank-slate template will be shown when on mobile --- .../gh-tags-management-container.js | 60 +++++++++++++++++++ core/client/app/controllers/settings/tags.js | 36 +++++++++-- .../app/controllers/settings/tags/tag.js | 5 +- core/client/app/routes/settings/tags/index.js | 8 ++- core/client/app/routes/settings/tags/new.js | 5 ++ core/client/app/routes/settings/tags/tag.js | 5 ++ core/client/app/styles/layouts/flow.css | 2 +- core/client/app/styles/layouts/main.css | 13 +++- core/client/app/styles/layouts/tags.css | 34 +++++++++++ .../components/gh-tag-settings-form.hbs | 10 +++- .../gh-tags-management-container.hbs | 1 + core/client/app/templates/settings/tags.hbs | 6 +- .../app/templates/settings/tags/tag.hbs | 2 +- .../gh-tags-management-container-test.js | 43 +++++++++++++ 14 files changed, 214 insertions(+), 16 deletions(-) create mode 100644 core/client/app/components/gh-tags-management-container.js create mode 100644 core/client/app/templates/components/gh-tags-management-container.hbs create mode 100644 core/client/tests/integration/components/gh-tags-management-container-test.js diff --git a/core/client/app/components/gh-tags-management-container.js b/core/client/app/components/gh-tags-management-container.js new file mode 100644 index 000000000000..4bb491f430f0 --- /dev/null +++ b/core/client/app/components/gh-tags-management-container.js @@ -0,0 +1,60 @@ +import Ember from 'ember'; + +const {isBlank} = Ember; + +export default Ember.Component.extend({ + classNames: ['view-container'], + classNameBindings: ['isMobile'], + + mobileWidth: 600, + tags: null, + selectedTag: null, + + isMobile: false, + isEmpty: Ember.computed.equal('tags.length', 0), + + resizeService: Ember.inject.service('resize-service'), + + _resizeListener: null, + + displaySettingsPane: Ember.computed('isEmpty', 'selectedTag', 'isMobile', function () { + const isEmpty = this.get('isEmpty'), + selectedTag = this.get('selectedTag'), + isMobile = this.get('isMobile'); + + // always display settings pane for blank-slate on mobile + if (isMobile && isEmpty) { + return true; + } + + // display list if no tag is selected on mobile + if (isMobile && isBlank(selectedTag)) { + return false; + } + + // default to displaying settings pane + return true; + }), + + toggleMobile: function () { + let width = Ember.$(window).width(); + + if (width < this.get('mobileWidth')) { + this.set('isMobile', true); + this.sendAction('enteredMobile'); + } else { + this.set('isMobile', false); + this.sendAction('leftMobile'); + } + }, + + didInitAttrs: function () { + this._resizeListener = Ember.run.bind(this, this.toggleMobile); + this.get('resizeService').on('debouncedDidResize', this._resizeListener); + this.toggleMobile(); + }, + + willDestroyElement: function () { + this.get('resizeService').off('debouncedDidResize', this._resizeListener); + } +}); diff --git a/core/client/app/controllers/settings/tags.js b/core/client/app/controllers/settings/tags.js index 0b8197e33446..296a2fe1f483 100644 --- a/core/client/app/controllers/settings/tags.js +++ b/core/client/app/controllers/settings/tags.js @@ -1,11 +1,23 @@ import Ember from 'ember'; +const {computed, inject} = Ember, + {alias, equal, sort} = computed; + export default Ember.Controller.extend({ - tagListFocused: Ember.computed.equal('keyboardFocus', 'tagList'), - tagContentFocused: Ember.computed.equal('keyboardFocus', 'tagContent'), + tagController: inject.controller('settings.tags.tag'), + + // set at controller level because it's shared by routes and components + mobileWidth: 600, + + isMobile: false, + selectedTag: alias('tagController.tag'), + + tagListFocused: equal('keyboardFocus', 'tagList'), + tagContentFocused: equal('keyboardFocus', 'tagContent'), - tags: Ember.computed.sort('model', function (a, b) { + // TODO: replace with ordering by page count once supported by the API + tags: sort('model', function (a, b) { const idA = +a.get('id'), idB = +b.get('id'); @@ -16,6 +28,22 @@ export default Ember.Controller.extend({ } return 0; - }) + }), + + actions: { + enteredMobile: function () { + this.set('isMobile', true); + }, + + leftMobile: function () { + this.set('isMobile', false); + + // redirect to first tag if possible so that you're not left with + // tag settings blank slate when switching from portrait to landscape + if (this.get('tags.length') && !this.get('tagController.tag')) { + this.transitionToRoute('settings.tags.tag', this.get('tags.firstObject')); + } + } + } }); diff --git a/core/client/app/controllers/settings/tags/tag.js b/core/client/app/controllers/settings/tags/tag.js index 4e012dac41d9..e6d29e66362e 100644 --- a/core/client/app/controllers/settings/tags/tag.js +++ b/core/client/app/controllers/settings/tags/tag.js @@ -1,11 +1,14 @@ import Ember from 'ember'; -const {computed} = Ember, +const {computed, inject} = Ember, {alias} = computed; export default Ember.Controller.extend({ tag: alias('model'), + isMobile: alias('tagsController.isMobile'), + + tagsController: inject.controller('settings.tags'), saveTagProperty: function (propKey, newValue) { const tag = this.get('tag'), diff --git a/core/client/app/routes/settings/tags/index.js b/core/client/app/routes/settings/tags/index.js index 726e7e503bc5..189c4d8d00a1 100644 --- a/core/client/app/routes/settings/tags/index.js +++ b/core/client/app/routes/settings/tags/index.js @@ -1,11 +1,15 @@ +import Ember from 'ember'; import AuthenticatedRoute from 'ghost/routes/authenticated'; export default AuthenticatedRoute.extend({ + // HACK: ugly way of changing behaviour when on mobile beforeModel: function () { - const firstTag = this.modelFor('settings.tags').get('firstObject'); + const firstTag = this.modelFor('settings.tags').get('firstObject'), + mobileWidth = this.controllerFor('settings.tags').get('mobileWidth'), + viewportWidth = Ember.$(window).width(); - if (firstTag) { + if (firstTag && viewportWidth > mobileWidth) { this.transitionTo('settings.tags.tag', firstTag); } } diff --git a/core/client/app/routes/settings/tags/new.js b/core/client/app/routes/settings/tags/new.js index 69d79e3df197..6d44af6044f1 100644 --- a/core/client/app/routes/settings/tags/new.js +++ b/core/client/app/routes/settings/tags/new.js @@ -10,6 +10,11 @@ export default AuthenticatedRoute.extend({ renderTemplate: function () { this.render('settings.tags.tag'); + }, + + // reset the model so that mobile screens react to an empty selectedTag + deactivate: function () { + this.set('controller.model', null); } }); diff --git a/core/client/app/routes/settings/tags/tag.js b/core/client/app/routes/settings/tags/tag.js index 4041c048918a..ac75c44f4534 100644 --- a/core/client/app/routes/settings/tags/tag.js +++ b/core/client/app/routes/settings/tags/tag.js @@ -4,6 +4,11 @@ export default AuthenticatedRoute.extend({ model: function (params) { return this.store.findRecord('tag', params.tag_id); + }, + + // reset the model so that mobile screens react to an empty selectedTag + deactivate: function () { + this.set('controller.model', null); } }); diff --git a/core/client/app/styles/layouts/flow.css b/core/client/app/styles/layouts/flow.css index 9e019741bb02..97796703bec4 100644 --- a/core/client/app/styles/layouts/flow.css +++ b/core/client/app/styles/layouts/flow.css @@ -6,7 +6,7 @@ display: flex; flex-direction: column; overflow-y: auto; - min-height: 100vh; + min-height: 100%; } .gh-flow-head { diff --git a/core/client/app/styles/layouts/main.css b/core/client/app/styles/layouts/main.css index a3600419289c..7f0508992bde 100644 --- a/core/client/app/styles/layouts/main.css +++ b/core/client/app/styles/layouts/main.css @@ -1,12 +1,21 @@ /* Global Layout /* ---------------------------------------------------------- */ +/* + Ember's app container, set height so that .gh-app and .gh-viewport + don't need to use 100vh where bottom of screen gets covered by iOS menus + http://nicolas-hoizey.com/2015/02/viewport-height-is-taller-than-the-visible-part-of-the-document-in-some-mobile-browsers.html +*/ +body > .ember-view { + height: 100%; +} + /* Main viewport, contains main content, and alerts */ .gh-app { display: flex; flex-direction: column; overflow: hidden; - height: 100vh; + height: 100%; } /* Content viewport, contains everything else */ @@ -14,7 +23,7 @@ flex-grow: 1; display: flex; overflow: hidden; - max-height: 100vh; + max-height: 100%; } .gh-main { diff --git a/core/client/app/styles/layouts/tags.css b/core/client/app/styles/layouts/tags.css index 7ccc4724852d..891923d6084c 100644 --- a/core/client/app/styles/layouts/tags.css +++ b/core/client/app/styles/layouts/tags.css @@ -76,6 +76,17 @@ background: #fff; } +@media (max-width: 600px) { + .tag-list { + max-width: 100%; + width: 100%; + } + + .settings-tag .tag-edit-button.active { + border-left: none; + } +} + /* Tag Settings (Right pane) /* ---------------------------------------------------------- */ @@ -94,6 +105,29 @@ transform: none; } +.tag-settings .no-posts { + padding: 1em; +} + .tag-settings .no-posts h3 { text-align: center; } + +.tag-settings .settings-menu-pane { + transition: transform 0.4s cubic-bezier(0.1, 0.7, 0.1, 1); +} + +@media (max-width: 600px) { + .tag-settings { + min-width: 0; + width: 100%; + transition: transform 0.4s cubic-bezier(0.1, 0.7, 0.1, 1); + transform: translate3d(100%, 0px, 0px); + + transform-style: preserve-3d; + } + + .tag-settings-in { + transform: translate3d(0px, 0px, 0px); + } +} diff --git a/core/client/app/templates/components/gh-tag-settings-form.hbs b/core/client/app/templates/components/gh-tag-settings-form.hbs index fc44143cf607..db5b638082e1 100644 --- a/core/client/app/templates/components/gh-tag-settings-form.hbs +++ b/core/client/app/templates/components/gh-tag-settings-form.hbs @@ -1,6 +1,12 @@
-
-

{{title}}

+
+ {{#if isMobile}} + {{#link-to 'settings.tags' class="back icon-arrow-left settings-menu-header-action"}}{{/link-to}} +

{{title}}

+
{{!flexbox space-between}}
+ {{else}} +

{{title}}

+ {{/if}}
{{gh-uploader uploaded="setCoverImage" canceled="clearCoverImage" description="Add tag image" image=tag.image initUploader="setUploaderReference" tagName="section"}} diff --git a/core/client/app/templates/components/gh-tags-management-container.hbs b/core/client/app/templates/components/gh-tags-management-container.hbs new file mode 100644 index 000000000000..f4a0b59217af --- /dev/null +++ b/core/client/app/templates/components/gh-tags-management-container.hbs @@ -0,0 +1 @@ +{{yield this}} diff --git a/core/client/app/templates/settings/tags.hbs b/core/client/app/templates/settings/tags.hbs index 4b6e48bba7d3..b54f7b074c20 100644 --- a/core/client/app/templates/settings/tags.hbs +++ b/core/client/app/templates/settings/tags.hbs @@ -7,7 +7,7 @@ -
+ {{#gh-tags-management-container mobileWidth=mobileWidth tags=tags selectedTag=selectedTag enteredMobile="enteredMobile" leftMobile="leftMobile" as |container|}} {{#gh-infinite-scroll fetch="loadNextPage" isLoading=isLoading @@ -26,8 +26,8 @@ {{/each}} {{/gh-infinite-scroll}} -
+
{{outlet}}
-
+ {{/gh-tags-management-container}} diff --git a/core/client/app/templates/settings/tags/tag.hbs b/core/client/app/templates/settings/tags/tag.hbs index 78655f3a5c8d..9db93e6703f1 100644 --- a/core/client/app/templates/settings/tags/tag.hbs +++ b/core/client/app/templates/settings/tags/tag.hbs @@ -1 +1 @@ -{{gh-tag-settings-form tag=tag setProperty=(action "setProperty") openModal="openModal"}} +{{gh-tag-settings-form tag=tag setProperty=(action "setProperty") openModal="openModal" isMobile=isMobile}} diff --git a/core/client/tests/integration/components/gh-tags-management-container-test.js b/core/client/tests/integration/components/gh-tags-management-container-test.js new file mode 100644 index 000000000000..063de06d6e9f --- /dev/null +++ b/core/client/tests/integration/components/gh-tags-management-container-test.js @@ -0,0 +1,43 @@ +/* jshint expr:true */ +import { expect } from 'chai'; +import { + describeComponent, + it +} from 'ember-mocha'; +import hbs from 'htmlbars-inline-precompile'; +import Ember from 'ember'; + +const resizeStub = Ember.Service.extend(Ember.Evented, { + +}); + +describeComponent( + 'gh-tags-management-container', + 'Integration: Component: gh-tags-management-container', + { + integration: true + }, + function () { + beforeEach(function () { + this.register('service:resize-service', resizeStub); + this.inject.service('resize-service', {as: 'resize-service'}); + }); + + it('renders', function () { + this.set('mobileWidth', 600); + this.set('tags', []); + this.set('selectedTag', null); + this.on('enteredMobile', function () { + // noop + }); + this.on('leftMobile', function () { + // noop + }); + + this.render(hbs` + {{#gh-tags-management-container mobileWidth=mobileWidth tags=tags selectedTag=selectedTag enteredMobile="enteredMobile" leftMobile="leftMobile"}}{{/gh-tags-management-container}} + `); + expect(this.$()).to.have.length(1); + }); + } +);