From 402b27c7e9147c66aaccb9f5554c72225ebed569 Mon Sep 17 00:00:00 2001 From: Matthew Beale Date: Fri, 20 Nov 2015 12:26:34 -0500 Subject: [PATCH] Unify mobile state in JS, drop resize In `gh-content-view-container` the visibility of another DOM node was being used to detect if a given view was mobile or not. This means the UI needed to have layout forced (and DOM rendered) before the content view container would render a second time. This is slow interaction with the DOM (forcing layout) and slow for Ember's renderer (it needs to render the container once with a default, then again when the value changes). Additionally there were two ways resize was being observed. The `Window.matchMedia` API was used for some styles and the `ember-resize` addon used to detect other changes. Here I've unified around just the `Window.matcheMedia` API but abstracted it behind a service. Sizes are exposed as properties that can be bound to or used directly in templates. --- .../components/gh-content-view-container.js | 24 +--------- core/client/app/components/gh-menu-toggle.js | 16 +------ .../app/components/gh-tag-settings-form.js | 3 ++ .../gh-tags-management-container.js | 34 ++++---------- core/client/app/controllers/settings/tags.js | 15 ++---- core/client/app/routes/mobile-index-route.js | 34 +++++++------- core/client/app/routes/posts/index.js | 10 ++-- core/client/app/routes/settings/tags/index.js | 10 ++-- core/client/app/services/media-queries.js | 46 +++++++++++++++++++ core/client/app/templates/settings/tags.hbs | 2 +- .../app/templates/settings/tags/tag.hbs | 2 +- core/client/app/utils/mobile.js | 1 - core/client/config/environment.js | 7 --- core/client/package.json | 1 - .../gh-tags-management-container-test.js | 12 +---- 15 files changed, 96 insertions(+), 121 deletions(-) create mode 100644 core/client/app/services/media-queries.js delete mode 100644 core/client/app/utils/mobile.js diff --git a/core/client/app/components/gh-content-view-container.js b/core/client/app/components/gh-content-view-container.js index aefe15beec..7dd5602437 100644 --- a/core/client/app/components/gh-content-view-container.js +++ b/core/client/app/components/gh-content-view-container.js @@ -4,26 +4,6 @@ export default Ember.Component.extend({ tagName: 'section', classNames: ['gh-view', 'content-view-container'], - previewIsHidden: false, - - resizeService: Ember.inject.service(), - - _resizeListener: null, - - calculatePreviewIsHidden: function () { - if (this.$('.content-preview').length) { - this.set('previewIsHidden', !this.$('.content-preview').is(':visible')); - } - }, - - didInsertElement: function () { - this._super(...arguments); - this._resizeListener = Ember.run.bind(this, this.calculatePreviewIsHidden); - this.get('resizeService').on('debouncedDidResize', this._resizeListener); - this.calculatePreviewIsHidden(); - }, - - willDestroy: function () { - this.get('resizeService').off('debouncedDidResize', this._resizeListener); - } + mediaQueries: Ember.inject.service(), + previewIsHidden: Ember.computed.reads('mediaQueries.maxWidth900') }); diff --git a/core/client/app/components/gh-menu-toggle.js b/core/client/app/components/gh-menu-toggle.js index 6ea270ae0e..de6a005992 100644 --- a/core/client/app/components/gh-menu-toggle.js +++ b/core/client/app/components/gh-menu-toggle.js @@ -10,12 +10,12 @@ closes the mobile menu */ import Ember from 'ember'; -import mobileQuery from 'ghost/utils/mobile'; export default Ember.Component.extend({ classNames: ['gh-menu-toggle'], - isMobile: false, + mediaQueries: Ember.inject.service(), + isMobile: Ember.computed.reads('mediaQueries.isMobile'), maximise: false, iconClass: Ember.computed('maximise', 'isMobile', function () { @@ -26,18 +26,6 @@ export default Ember.Component.extend({ } }), - didInsertElement: function () { - this.set('isMobile', mobileQuery.matches); - this.set('mqListener', Ember.run.bind(this, function (mql) { - this.set('isMobile', mql.matches); - })); - mobileQuery.addListener(this.get('mqListener')); - }, - - willDestroyElement: function () { - mobileQuery.removeListener(this.get('mqListener')); - }, - click: function () { if (this.get('isMobile')) { this.sendAction('mobileAction'); diff --git a/core/client/app/components/gh-tag-settings-form.js b/core/client/app/components/gh-tag-settings-form.js index e3d6891424..2ad9461e05 100644 --- a/core/client/app/components/gh-tag-settings-form.js +++ b/core/client/app/components/gh-tag-settings-form.js @@ -18,6 +18,9 @@ export default Ember.Component.extend({ config: Ember.inject.service(), + mediaQueries: Ember.inject.service(), + isMobile: Ember.computed.reads('mediaQueries.maxWidth600'), + title: Ember.computed('tag.isNew', function () { if (this.get('tag.isNew')) { return 'New Tag'; diff --git a/core/client/app/components/gh-tags-management-container.js b/core/client/app/components/gh-tags-management-container.js index 4bb491f430..b212a5bd1a 100644 --- a/core/client/app/components/gh-tags-management-container.js +++ b/core/client/app/components/gh-tags-management-container.js @@ -6,16 +6,18 @@ export default Ember.Component.extend({ classNames: ['view-container'], classNameBindings: ['isMobile'], - mobileWidth: 600, + mediaQueries: Ember.inject.service(), + tags: null, selectedTag: null, - isMobile: false, + isMobile: Ember.computed.reads('mediaQueries.maxWidth600'), isEmpty: Ember.computed.equal('tags.length', 0), - resizeService: Ember.inject.service('resize-service'), - - _resizeListener: null, + init: function () { + this._super(...arguments); + Ember.run.schedule('actions', this, this.fireMobileChangeActions); + }, displaySettingsPane: Ember.computed('isEmpty', 'selectedTag', 'isMobile', function () { const isEmpty = this.get('isEmpty'), @@ -36,25 +38,9 @@ export default Ember.Component.extend({ 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); + fireMobileChangeActions: Ember.observer('isMobile', function () { + if (!this.get('isMobile')) { 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 296a2fe1f4..9cd93991e7 100644 --- a/core/client/app/controllers/settings/tags.js +++ b/core/client/app/controllers/settings/tags.js @@ -7,10 +7,6 @@ export default Ember.Controller.extend({ 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'), @@ -31,17 +27,12 @@ export default Ember.Controller.extend({ }), actions: { - enteredMobile: function () { - this.set('isMobile', true); - }, - leftMobile: function () { - this.set('isMobile', false); - + let firstTag = this.get('tags.firstObject'); // 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')); + if (firstTag && !this.get('tagController.tag')) { + this.transitionToRoute('settings.tags.tag', firstTag); } } } diff --git a/core/client/app/routes/mobile-index-route.js b/core/client/app/routes/mobile-index-route.js index 5b72a673f4..78626dbd3a 100644 --- a/core/client/app/routes/mobile-index-route.js +++ b/core/client/app/routes/mobile-index-route.js @@ -1,28 +1,28 @@ import Ember from 'ember'; -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. export default Ember.Route.extend({ desktopTransition: Ember.K, + _callDesktopTransition: null, - activate: function attachDesktopTransition() { - this._super(); - mobileQuery.addListener(this.desktopTransitionMQ); - }, + mediaQueries: Ember.inject.service(), - deactivate: function removeDesktopTransition() { - this._super(); - mobileQuery.removeListener(this.desktopTransitionMQ); - }, - - setDesktopTransitionMQ: Ember.on('init', function () { - var self = this; - this.set('desktopTransitionMQ', function desktopTransitionMQ() { - if (!mobileQuery.matches) { - self.desktopTransition(); + activate: function () { + this._callDesktopTransition = () => { + if (!this.get('mediaQueries.isMobile')) { + this.desktopTransition(); } - }); - }) + }; + Ember.addObserver(this, 'mediaQueries.isMobile', this._callDesktopTransition); + }, + + deactivate: function () { + if (this._callDesktopTransition) { + Ember.removeObserver(this, 'mediaQueries.isMobile', this._callDesktopTransition); + this._callDesktopTransition = null; + } + } + }); diff --git a/core/client/app/routes/posts/index.js b/core/client/app/routes/posts/index.js index eec441dad8..3adf759f15 100644 --- a/core/client/app/routes/posts/index.js +++ b/core/client/app/routes/posts/index.js @@ -1,19 +1,21 @@ +import Ember from 'ember'; import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; import MobileIndexRoute from 'ghost/routes/mobile-index-route'; -import mobileQuery from 'ghost/utils/mobile'; export default MobileIndexRoute.extend(AuthenticatedRouteMixin, { noPosts: false, + mediaQueries: Ember.inject.service(), + isMobile: Ember.computed.reads('mediaQueries.isMobile'), + // Transition to a specific post if we're not on mobile beforeModel: function () { - if (!mobileQuery.matches) { + if (!this.get('isMobile')) { return this.goToPost(); } }, - setupController: function (controller, model) { - /*jshint unused:false*/ + setupController: function (controller) { controller.set('noPosts', this.get('noPosts')); }, diff --git a/core/client/app/routes/settings/tags/index.js b/core/client/app/routes/settings/tags/index.js index 189c4d8d00..deb02a36b8 100644 --- a/core/client/app/routes/settings/tags/index.js +++ b/core/client/app/routes/settings/tags/index.js @@ -3,13 +3,11 @@ 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'), - mobileWidth = this.controllerFor('settings.tags').get('mobileWidth'), - viewportWidth = Ember.$(window).width(); + mediaQueries: Ember.inject.service(), - if (firstTag && viewportWidth > mobileWidth) { + beforeModel: function () { + let firstTag = this.modelFor('settings.tags').get('firstObject'); + if (firstTag && !this.get('mediaQueries.maxWidth600')) { this.transitionTo('settings.tags.tag', firstTag); } } diff --git a/core/client/app/services/media-queries.js b/core/client/app/services/media-queries.js new file mode 100644 index 0000000000..85a1c4917d --- /dev/null +++ b/core/client/app/services/media-queries.js @@ -0,0 +1,46 @@ +import Ember from 'ember'; + +const MEDIA_QUERIES = { + maxWidth600: '(max-width: 600px)', + isMobile: '(max-width: 800px)', + maxWidth900: '(max-width: 900px)', + maxWidth1000: '(max-width: 1000px)' +}; + +export default Ember.Service.extend({ + init: function () { + this._super(...arguments); + this._handlers = []; + this.loadQueries(MEDIA_QUERIES); + }, + + loadQueries: function (queries) { + Object.keys(queries).forEach(key => { + this.loadQuery(key, queries[key]); + }); + }, + + loadQuery: function (key, queryString) { + let query = window.matchMedia(queryString); + + this.set(key, query.matches); + + let handler = Ember.run.bind(this, () => { + let lastValue = this.get(key); + let newValue = query.matches; + if (lastValue !== newValue) { + this.set(key, query.matches); + } + }); + query.addListener(handler); + this._handlers.push([query, handler]); + }, + + willDestroy: function () { + this._handlers.forEach(([query, handler]) => { + query.removeListener(handler); + }); + this._super(...arguments); + } + +}); diff --git a/core/client/app/templates/settings/tags.hbs b/core/client/app/templates/settings/tags.hbs index 44b1d51fd2..dae378ac96 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-tags-management-container tags=tags selectedTag=selectedTag enteredMobile="enteredMobile" leftMobile="leftMobile" as |container|}} {{#gh-infinite-scroll fetch="loadNextPage" isLoading=isLoading diff --git a/core/client/app/templates/settings/tags/tag.hbs b/core/client/app/templates/settings/tags/tag.hbs index 9db93e6703..78655f3a5c 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" isMobile=isMobile}} +{{gh-tag-settings-form tag=tag setProperty=(action "setProperty") openModal="openModal"}} diff --git a/core/client/app/utils/mobile.js b/core/client/app/utils/mobile.js deleted file mode 100644 index d9f4720b8a..0000000000 --- a/core/client/app/utils/mobile.js +++ /dev/null @@ -1 +0,0 @@ -export default matchMedia('(max-width: 800px)'); diff --git a/core/client/config/environment.js b/core/client/config/environment.js index d0a3181399..092009b609 100644 --- a/core/client/config/environment.js +++ b/core/client/config/environment.js @@ -23,13 +23,6 @@ module.exports = function (environment) { authenticationRoute: 'signin', routeAfterAuthentication: 'posts', routeIfAlreadyAuthenticated: 'posts' - }, - - resizeServiceDefaults: { - debounceTimeout: 100, - heightSensitive: false, - widthSensitive: true, - injectionFactories: [] } }; diff --git a/core/client/package.json b/core/client/package.json index 2f3013cce9..983360e241 100644 --- a/core/client/package.json +++ b/core/client/package.json @@ -43,7 +43,6 @@ "ember-disable-proxy-controllers": "^1.0.0", "ember-export-application-global": "^1.0.3", "ember-myth": "0.1.1", - "ember-resize": "0.0.10", "ember-simple-auth": "1.0.0", "ember-sinon": "0.2.1", "ember-watson": "^0.6.4", 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 index 063de06d6e..68e4e05e03 100644 --- a/core/client/tests/integration/components/gh-tags-management-container-test.js +++ b/core/client/tests/integration/components/gh-tags-management-container-test.js @@ -7,10 +7,6 @@ import { 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', @@ -18,13 +14,7 @@ describeComponent( 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 () { @@ -35,7 +25,7 @@ describeComponent( }); this.render(hbs` - {{#gh-tags-management-container mobileWidth=mobileWidth tags=tags selectedTag=selectedTag enteredMobile="enteredMobile" leftMobile="leftMobile"}}{{/gh-tags-management-container}} + {{#gh-tags-management-container tags=tags selectedTag=selectedTag enteredMobile="enteredMobile" leftMobile="leftMobile"}}{{/gh-tags-management-container}} `); expect(this.$()).to.have.length(1); });