diff --git a/README.md b/README.md index c071c672..6f705a9a 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,19 @@ ember install ember-router-scroll ``` ### Options -You can specify the id of an element for which the scroll position is saved and set. Default is `window` for using the scroll position of the whole viewport. You can pass an options object in your application's `config/environment.js` file. +If you need to scroll to the top of an area that generates a vertical scroll bar, you can specify the id of an element of the scrollable area. Default is `window` for using the scroll position of the whole viewport. You can pass an options object in your application's `config/environment.js` file. ```javascript ENV['routerScroll'] = { scrollElement: '#mainScrollElement' }; +If you want to scroll to a target element on the page, you can specify the id or class of the element on the page. This is particularly useful if instead of scrolling to the top of the window, you want to scroll to the top of the main content area (that does not generate a vertical scrollbar). + +ENV['routerScroll'] = { + targetElement: '#main-target-element' // or .main-target-element +}; + Moreover, if your route breaks up render into multiple phases, you may need to delay scrollTop functionality until after the First Meaningful Paint using `delayScrollTop: true` in your config. `delayScrollTop` defaults to `false`. ```javascript @@ -84,7 +90,7 @@ historySupportMiddleware: true, This location type inherits from Ember's `HistoryLocation`. -4. Tests +4. If using old style QUnit tests. If tests based on [RFC](https://github.com/emberjs/rfcs/pull/232), you can ignore this. In your router and controller tests, add `'service:router-scroll'` and `'service:scheduler'` as dependencies in the `needs: []` block: ```js @@ -92,15 +98,6 @@ In your router and controller tests, add `'service:router-scroll'` and `'service needs:[ 'service:router-scroll', 'service:scheduler' ], ``` -### Options -You can specify the id of an element for which the scroll position is saved and set. Default is `window` for using the scroll position of the whole viewport. You can pass an options object in your application's `config/environment.js` file. - -```javascript -ENV['routerScroll'] = { - scrollElement: '#mainScrollElement' -}; -``` - ## Issues with nested routes ### Before: @@ -124,9 +121,9 @@ Add `preserveScrollPosition` as a queryParam in the controller for the route tha Example: ```javascript -import Ember from 'ember'; +import Controller from '@ember/controller'; -export default Ember.Controller.extend({ +export default Controller.extend({ queryParams: [ 'preserveScrollPosition', ], @@ -154,9 +151,9 @@ In this example we have `preserveScrollPosition` initially set to false so that Example: ```javascript -import Ember from 'ember'; +import Controller from '@ember/controller'; -export default Ember.Controller.extend({ +export default Controller.extend({ queryParams: ['filter'], preserveScrollPosition: false, @@ -175,9 +172,9 @@ export default Ember.Controller.extend({ If your controller is changing the preserveScrollPosition property, you'll probably need to reset `preserveScrollPosition` back to the default behavior whenever the controller is reset. This is not necceary on routes where `preserveScrollPosition` is always set to true. ```javascript -import Ember from 'ember'; +import Router from '@ember/routing/route'; -export default Ember.Route.extend({ +export default Route.extend({ resetController(controller) { controller.set('preserveScrollPosition', false); } diff --git a/addon/index.js b/addon/index.js index 85ec46f1..d061932e 100644 --- a/addon/index.js +++ b/addon/index.js @@ -39,7 +39,7 @@ export default Mixin.create({ } }, - updateScrollPosition (transitions) { + updateScrollPosition(transitions) { const lastTransition = transitions[transitions.length - 1]; let routerPath @@ -58,12 +58,15 @@ export default Mixin.create({ } else { scrollPosition = get(this, 'service.position'); } - const scrollElement = get(this, 'service.scrollElement'); - const preserveScrollPosition = get(lastTransition, 'handler.controller.preserveScrollPosition'); if (!preserveScrollPosition) { - if ('window' === scrollElement) { + const scrollElement = get(this, 'service.scrollElement'); + const targetElement = get(this, 'service.targetElement'); + + if (targetElement) { + window.scrollTo(scrollPosition.x, scrollPosition.y); + } else if ('window' === scrollElement) { window.scrollTo(scrollPosition.x, scrollPosition.y); } else if ('#' === scrollElement.charAt(0)) { const element = document.getElementById(scrollElement.substring(1)); diff --git a/addon/services/router-scroll.js b/addon/services/router-scroll.js index 2e05d55b..25114bc5 100644 --- a/addon/services/router-scroll.js +++ b/addon/services/router-scroll.js @@ -1,6 +1,7 @@ import Service from '@ember/service'; import { getWithDefault, computed, set, get } from '@ember/object'; import { typeOf } from '@ember/utils'; +import { assert } from '@ember/debug'; import { getOwner } from '@ember/application'; export default Service.extend({ @@ -10,23 +11,39 @@ export default Service.extend({ }), scrollElement: 'window', + targetElement: null, delayScrollTop: false, init(...args) { this._super(...args); this._loadConfig(); - set(this, 'scrollMap', {}); + set(this, 'scrollMap', { default: { x: 0, y: 0 }}); set(this, 'key', null); }, update() { const scrollElement = get(this, 'scrollElement'); + const targetElement = get(this, 'targetElement'); const scrollMap = get(this, 'scrollMap'); const key = get(this, 'key'); let x; let y; - if ('window' === scrollElement) { + if (targetElement) { + if (get(this, 'isFastBoot')) { + return; + } + + let element = document.querySelector(targetElement); + if (element) { + x = element.offsetLeft; + y = element.offsetTop; + + // if we are looking to where to transition to next, we need to set the default to the position + // of the targetElement on screen + set(scrollMap, 'default', { x, y }); + } + } else if ('window' === scrollElement) { x = window.scrollX; y = window.scrollY; } else if ('#' === scrollElement.charAt(0)) { @@ -54,19 +71,26 @@ export default Service.extend({ set(this, 'key', stateUuid); // eslint-disable-line ember/no-side-effects const key = getWithDefault(this, 'key', '-1'); - return getWithDefault(scrollMap, key, { x: 0, y: 0 }); + return getWithDefault(scrollMap, key, scrollMap.default); }).volatile(), _loadConfig() { const config = getOwner(this).resolveRegistration('config:environment'); - if (config && config.routerScroll && config.routerScroll.scrollElement) { + if (config && config.routerScroll) { const scrollElement = config.routerScroll.scrollElement; + const targetElement = config.routerScroll.targetElement; + + assert('You defined both scrollElement and targetElement in your config. We currently only support definining one of them', !(scrollElement && targetElement)); if ('string' === typeOf(scrollElement)) { set(this, 'scrollElement', scrollElement); } + if ('string' === typeOf(targetElement)) { + set(this, 'targetElement', targetElement); + } + const delayScrollTop = config.routerScroll.delayScrollTop; if (delayScrollTop === true) { set(this, 'delayScrollTop', true); diff --git a/package.json b/package.json index c6f2f84d..be4b3309 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-router-scroll", - "version": "0.6.1", + "version": "0.7.0", "description": "Scroll to top with preserved browser history scroll position", "keywords": [ "ember-addon", @@ -62,7 +62,7 @@ "test:all": "ember try:each" }, "dependencies": { - "ember-app-scheduler": "^0.2.0", + "ember-app-scheduler": "^0.2.2", "ember-cli-babel": "^6.6.0", "ember-getowner-polyfill": "^2.0.1" }, diff --git a/tests/acceptance/basic-functionality-test.js b/tests/acceptance/basic-functionality-test.js index bccf1d74..cee13b66 100644 --- a/tests/acceptance/basic-functionality-test.js +++ b/tests/acceptance/basic-functionality-test.js @@ -1,15 +1,55 @@ import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; -import { visit, click, currentURL } from '@ember/test-helpers'; +import { visit, click, currentURL, triggerEvent } from '@ember/test-helpers'; +import config from 'dummy/config/environment'; module('Acceptance | basic functionality', function(hooks) { setupApplicationTest(hooks); + hooks.beforeEach(function() { + document.getElementById('ember-testing-container').scrollTop = 0; + }); + hooks.afterEach(function() { + config['routerScroll'] = {}; + }); + test('The application should work when loading a page and clicking a link', async function(assert) { + config['routerScroll'] = { + scrollElement: '#ember-testing-container' + } + await visit('/'); + // testing specific + let container = document.getElementById('ember-testing-container'); + assert.equal(container.scrollTop, 0); + + await document.getElementById('monster').scrollIntoView(false); + await triggerEvent(window, 'scroll'); + + assert.ok(container.scrollTop > 0); + await click('a[href="/next-page"]'); assert.equal(currentURL(), '/next-page'); }); + + test('The application should work when loading a page and clicking a link to target an element to scroll to', async function(assert) { + config['routerScroll'] = { + scrollElement: '#target-main' + } + + await visit('/target'); + + // testing specific + let container = document.getElementById('ember-testing-container'); + assert.equal(container.scrollTop, 0); + + await document.getElementById('monster').scrollIntoView(false); + assert.ok(container.scrollTop > 0); + + await click('a[href="/target-next-page"]'); + + assert.equal(currentURL(), '/target-next-page'); + }); }); diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js index 01edfcb0..e91532c7 100644 --- a/tests/dummy/app/router.js +++ b/tests/dummy/app/router.js @@ -9,6 +9,8 @@ const Router = EmberRouter.extend(RouterScroll, { Router.map(function () { this.route('next-page'); + this.route('target'); + this.route('target-next-page'); }); export default Router; diff --git a/tests/dummy/app/routes/index.js b/tests/dummy/app/routes/index.js new file mode 100644 index 00000000..6c74252a --- /dev/null +++ b/tests/dummy/app/routes/index.js @@ -0,0 +1,4 @@ +import Route from '@ember/routing/route'; + +export default Route.extend({ +}); diff --git a/tests/dummy/app/routes/target-next-page.js b/tests/dummy/app/routes/target-next-page.js new file mode 100644 index 00000000..6c74252a --- /dev/null +++ b/tests/dummy/app/routes/target-next-page.js @@ -0,0 +1,4 @@ +import Route from '@ember/routing/route'; + +export default Route.extend({ +}); diff --git a/tests/dummy/app/routes/target.js b/tests/dummy/app/routes/target.js new file mode 100644 index 00000000..6c74252a --- /dev/null +++ b/tests/dummy/app/routes/target.js @@ -0,0 +1,4 @@ +import Route from '@ember/routing/route'; + +export default Route.extend({ +}); diff --git a/tests/dummy/app/styles/app.css b/tests/dummy/app/styles/app.css index 2b178068..e4fc7ec0 100644 --- a/tests/dummy/app/styles/app.css +++ b/tests/dummy/app/styles/app.css @@ -1,5 +1,9 @@ @import url(https://fonts.googleapis.com/css?family=VT323); +body { + margin: 0; +} + #death-star { position:absolute; margin: 1550px auto 0 60%; @@ -52,6 +56,12 @@ div { margin: 12% 10% -200px 10%; } +#nav { + height: 44px; + background-color: white; + color: black; +} + .depth { font-size: 26px; } @@ -74,6 +84,6 @@ div { .left { margin: 300px 0 0 10%; } - .right { - margin: 300px 0 0 75%; - } \ No newline at end of file +.right { + margin: 300px 0 0 75%; +} diff --git a/tests/dummy/app/templates/application.hbs b/tests/dummy/app/templates/application.hbs index 0a3e622a..49a00a7d 100644 --- a/tests/dummy/app/templates/application.hbs +++ b/tests/dummy/app/templates/application.hbs @@ -2,7 +2,7 @@