diff --git a/addon/index.js b/addon/index.js deleted file mode 100644 index 2d07d846..00000000 --- a/addon/index.js +++ /dev/null @@ -1,172 +0,0 @@ -import EmberRouter from '@ember/routing/router'; -import { get, computed } from '@ember/object'; -import { inject } from '@ember/service'; -import { getOwner } from '@ember/application'; -import { scheduleOnce } from '@ember/runloop'; -import { setupRouter, reset, whenRouteIdle } from 'ember-app-scheduler'; - -let ATTEMPTS = 0; -const MAX_ATTEMPTS = 100; // rAF runs every 16ms ideally, so 60x a second - -let requestId; -let callbackRequestId; - -/** - * By default, we start checking to see if the document height is >= the last known `y` position - * we want to scroll to. This is important for content heavy pages that might try to scrollTo - * before the content has painted - * - * @method tryScrollRecursively - * @param {Function} fn - * @param {Object} scrollHash - * @param {Element} [element] - * @void - */ -function tryScrollRecursively(fn, scrollHash, element) { - let documentHeight; - // read DOM outside of rAF - if (element) { - documentHeight = Math.max(element.scrollHeight, element.offsetHeight, element.clientHeight); - } else { - const body = document.body; - const html = document.documentElement; - documentHeight = Math.max(body.scrollHeight, body.offsetHeight, - html.clientHeight, html.scrollHeight, html.offsetHeight); - } - - callbackRequestId = window.requestAnimationFrame(() => { - // write DOM (scrollTo causes reflow) - if (documentHeight >= scrollHash.y || ATTEMPTS >= MAX_ATTEMPTS) { - ATTEMPTS = 0; - fn.call(null, scrollHash.x, scrollHash.y); - } else { - ATTEMPTS++; - tryScrollRecursively(fn, scrollHash) - } - }) -} - -// to prevent scheduleOnce calling multiple times, give it the same ref to this function -const CALLBACK = function(transition) { - this.updateScrollPosition(transition); -} - -class EmberRouterScroll extends EmberRouter { - @inject('router-scroll') service; - - @computed - get isFastBoot() { - const fastboot = getOwner(this).lookup('service:fastboot'); - return fastboot ? fastboot.get('isFastBoot') : false; - } - - init() { - super.init(...arguments); - - setupRouter(this); - - this.on('routeWillChange', () => { - this._routeWillChange(); - }); - - this.on('routeDidChange', (transition) => { - this._routeDidChange(transition); - }); - } - - willDestroy() { - reset(); - - if (requestId) { - window.cancelAnimationFrame(requestId); - } - - if (callbackRequestId) { - window.cancelAnimationFrame(callbackRequestId); - } - - super.willDestroy(...arguments); - } - - /** - * Updates the scroll position - * it will be a single transition - * @method updateScrollPosition - * @param {transition|transition[]} transition If before Ember 3.6, this will be an array of transitions, otherwise - */ - updateScrollPosition(transition) { - const url = get(this, 'currentURL'); - const hashElement = url ? document.getElementById(url.split('#').pop()) : null; - - if (get(this, 'service.isFirstLoad')) { - get(this, 'service').unsetFirstLoad(); - } - - let scrollPosition; - - if (url && url.indexOf('#') > -1 && hashElement) { - scrollPosition = { x: hashElement.offsetLeft, y: hashElement.offsetTop }; - } else { - scrollPosition = get(this, 'service.position'); - } - - let preserveScrollPosition = (get(transition, 'router.currentRouteInfos') || []).some((routeInfo) => get(routeInfo, 'route.controller.preserveScrollPosition')); - - // If `preserveScrollPosition` was not set on the controller, attempt fallback to `preserveScrollPosition` which was set on the router service. - if(!preserveScrollPosition) { - preserveScrollPosition = get(this, 'service.preserveScrollPosition') - } - - if (!preserveScrollPosition) { - const scrollElement = get(this, 'service.scrollElement'); - const targetElement = get(this, 'service.targetElement'); - - if (targetElement || 'window' === scrollElement) { - tryScrollRecursively(window.scrollTo, scrollPosition); - } else if ('#' === scrollElement.charAt(0)) { - const element = document.getElementById(scrollElement.substring(1)); - - if (element) { - let fn = (x, y) => { - element.scrollLeft = x; - element.scrollTop = y; - } - tryScrollRecursively(fn, scrollPosition, element); - } - } - } - - this.trigger('didScroll', transition); - } - - _routeWillChange() { - if (get(this, 'isFastBoot')) { - return; - } - - get(this, 'service').update(); - } - - _routeDidChange(transition) { - if (get(this, 'isFastBoot')) { - return; - } - - const scrollWhenIdle = get(this, 'service.scrollWhenIdle'); - const scrollWhenAfterRender = get(this, 'service.scrollWhenAfterRender'); - - if (!scrollWhenIdle && !scrollWhenAfterRender) { - // out of the option, this happens on the tightest schedule - scheduleOnce('render', this, CALLBACK, transition); - } else if (scrollWhenAfterRender && !scrollWhenIdle) { - // out of the option, this happens on the second tightest schedule - scheduleOnce('afterRender', this, CALLBACK, transition); - } else { - whenRouteIdle().then(() => { - this.updateScrollPosition(transition); - }); - } - } -} - -export default EmberRouterScroll; diff --git a/tests/unit/router-scroll-test.js b/tests/unit/router-scroll-test.js index 1ecf3c34..b964ac23 100644 --- a/tests/unit/router-scroll-test.js +++ b/tests/unit/router-scroll-test.js @@ -1,11 +1,8 @@ import { run } from '@ember/runloop'; import EmberObject from '@ember/object'; -import Evented from '@ember/object/evented'; -import EmberRouterScroll from 'ember-router-scroll'; import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import { settled } from '@ember/test-helpers'; -import { gte } from 'ember-compatibility-helpers'; let scrollTo, subject; @@ -42,7 +39,7 @@ module('router-scroll', function(hooks) { } }; - return gte('3.6.0-beta.1') ? transition : [transition]; + return transition; } test('when the application is FastBooted', async function(assert) { @@ -50,21 +47,15 @@ module('router-scroll', function(hooks) { const done = assert.async(); this.owner.register('service:fastboot', EmberObject.extend({ isFastBoot: true })); - this.owner.register('router:main', EmberRouterScroll.extend({ - updateScrollPosition() { - assert.notOk(true, 'it should not call updateScrollPosition.'); - done(); - } - })); - - subject = this.owner.factoryFor('router:main').create(); - - if(gte('3.6.0-beta.1')) { - subject.trigger('routeDidChange'); - } else { - subject.didTransition(); + const routerScrollService = this.owner.lookup('service:router-scroll'); + routerScrollService.updateScrollPosition = () => { + assert.notOk(true, 'it should not call updateScrollPosition.'); + done(); } + const subject = this.owner.lookup('service:router'); + subject.trigger('routeDidChange'); + assert.ok(true, 'it should not call updateScrollPosition.'); await settled(); done(); @@ -75,23 +66,15 @@ module('router-scroll', function(hooks) { const done = assert.async(); this.owner.register('service:fastboot', EmberObject.extend({ isFastBoot: false })); - this.owner.register('service:router-scroll', EmberObject.extend({ scrollWhenIdle: false })); - this.owner.register('router:main', EmberRouterScroll.extend({ - updateScrollPosition() { - assert.ok(true, 'it should call updateScrollPosition.'); - done(); - } - })); - - subject = this.owner.factoryFor('router:main').create(); + const routerScrollService = this.owner.lookup('service:router-scroll'); + routerScrollService.scrollWhenIdle = false; + routerScrollService.updateScrollPosition = () => { + assert.ok(true, 'it should call updateScrollPosition.'); + done(); + } - run(() => { - if(gte('3.6.0-beta.1')) { - subject.trigger('routeDidChange'); - } else { - subject.didTransition(); - } - }); + const subject = this.owner.lookup('service:router'); + subject.trigger('routeDidChange'); }); test('when the application is not FastBooted with targetElement', function(assert) { @@ -99,23 +82,15 @@ module('router-scroll', function(hooks) { const done = assert.async(); this.owner.register('service:fastboot', EmberObject.extend({ isFastBoot: false })); - this.owner.register('service:router-scroll', EmberObject.extend({ targetElement: '#myElement' })); - this.owner.register('router:main', EmberRouterScroll.extend({ - updateScrollPosition() { - assert.ok(true, 'it should call updateScrollPosition.'); - done(); - } - })); - - subject = this.owner.factoryFor('router:main').create(); + const routerScrollService = this.owner.lookup('service:router-scroll'); + routerScrollService.targetElement = '#myElement'; + routerScrollService.updateScrollPosition = () => { + assert.ok(true, 'it should call updateScrollPosition.'); + done(); + } - run(() => { - if(gte('3.6.0-beta.1')) { - subject.trigger('routeDidChange'); - } else { - subject.didTransition(); - } - }); + const subject = this.owner.lookup('service:router'); + subject.trigger('routeDidChange'); }); test('when the application is not FastBooted with scrollWhenIdle', function(assert) { @@ -123,19 +98,15 @@ module('router-scroll', function(hooks) { const done = assert.async(); this.owner.register('service:fastboot', EmberObject.extend({ isFastBoot: false })); - this.owner.register('service:router-scroll', EmberObject.extend({ scrollWhenIdle: true })); - this.owner.register('router:main', EmberRouterScroll.extend({ - updateScrollPosition() { - assert.ok(true, 'it should call updateScrollPosition.'); - done(); - } - })); - - subject = this.owner.factoryFor('router:main').create(); + const routerScrollService = this.owner.lookup('service:router-scroll'); + routerScrollService.scrollWhenIdle = true; + routerScrollService.updateScrollPosition = () => { + assert.ok(true, 'it should call updateScrollPosition.'); + done(); + } - run(() => { - subject.trigger('routeDidChange'); - }); + const subject = this.owner.lookup('service:router'); + subject.trigger('routeDidChange'); }); test('when the application is not FastBooted with scrollWhenAfterRender', function(assert) { @@ -143,95 +114,49 @@ module('router-scroll', function(hooks) { const done = assert.async(); this.owner.register('service:fastboot', EmberObject.extend({ isFastBoot: false })); - this.owner.register('service:router-scroll', EmberObject.extend({ scrollWhenAfterRender: true })); - this.owner.register('router:main', EmberRouterScroll.extend({ - updateScrollPosition() { - assert.ok(true, 'it should call updateScrollPosition.'); - done(); - } - })); - - subject = this.owner.factoryFor('router:main').create(); + const routerScrollService = this.owner.lookup('service:router-scroll'); + routerScrollService.scrollWhenAfterRender = true; + routerScrollService.updateScrollPosition = () => { + assert.ok(true, 'it should call updateScrollPosition.'); + done(); + } - run(() => { - subject.trigger('routeDidChange'); - }); + const subject = this.owner.lookup('service:router'); + subject.trigger('routeDidChange'); }); test('Update Scroll Position: Position is preserved', function(assert) { assert.expect(0); const done = assert.async(); - window.scrollTo = () => assert.ok(false, 'Scroll To should not be called'); - - this.owner.register('service:fastboot', EmberObject.extend({ isFastBoot: false })); - this.owner.register('service:router-scroll', EmberObject.extend({ - position: null, - scrollElement: 'window' - })); - this.owner.register('router:main', EmberRouterScroll.extend()); - - subject = this.owner.factoryFor('router:main').create(); - - run(() => { - if(gte('3.6.0-beta.1')) { - subject.trigger('routeDidChange', getTransitionsMock('Hello/World', true)); - } else { - subject.didTransition(getTransitionsMock('Hello/World', true)); - } + window.scrollTo = () => { + assert.ok(false, 'Scroll To should not be called'); done(); - }); - }); - - test('when the application is not FastBooted with scrollWhenPainted', function(assert) { - assert.expect(1); - const done = assert.async(); + } this.owner.register('service:fastboot', EmberObject.extend({ isFastBoot: false })); - this.owner.register('service:router-scroll', EmberObject.extend({ scrollWhenPainted: true })); - this.owner.register('router:main', EmberRouterScroll.extend({ - updateScrollPosition() { - assert.ok(true, 'it should call updateScrollPosition.'); - done(); - } - })); - - subject = this.owner.factoryFor('router:main').create(); + const routerScrollService = this.owner.lookup('service:router-scroll'); + routerScrollService.position = null; + routerScrollService.scrollElement = 'window'; - run(() => { - if(gte('3.6.0-beta.1')) { - subject.trigger('routeDidChange'); - } else { - subject.didTransition(); - } - }); + const subject = this.owner.lookup('service:router'); + subject.trigger('routeDidChange', getTransitionsMock('Hello/World', true)); }); test('Update Scroll Position: Can preserve position using routerService', function(assert) { assert.expect(0); const done = assert.async(); - window.scrollTo = () => assert.ok(false, 'Scroll To should not be called'); + window.scrollTo = () => { + assert.ok(false, 'Scroll To should not be called'); + done(); + } - const EmberRouterScrollObject = EmberRouterScroll.extend(); - subject = EmberRouterScrollObject.create({ - isFastBoot: false, - service: { - position: null, - scrollElement: 'window', - } - }); + const routerScrollService = this.owner.lookup('service:router-scroll'); + routerScrollService.preserveScrollPosition = true; - run(() => { - subject.service.preserveScrollPosition = true; - - if(gte('3.6.0-beta.1')) { - subject.trigger('routeDidChange', getTransitionsMock('Hello/World', false)); - } else { - subject.didTransition(getTransitionsMock('Hello/World', false)); - } - done(); - }); + const subject = this.owner.lookup('service:router'); + subject.trigger('routeDidChange', getTransitionsMock('Hello/World', true)); }); test('Update Scroll Position: URL is an anchor', async function(assert) { @@ -247,13 +172,11 @@ module('router-scroll', function(hooks) { } this.owner.register('service:fastboot', EmberObject.extend({ isFastBoot: false })); - this.owner.register('service:router-scroll', EmberObject.extend({ - position: null, - scrollElement: 'window' - })); - this.owner.register('router:main', EmberRouterScroll.extend(Evented)); + const routerScrollService = this.owner.lookup('service:router-scroll'); + routerScrollService.position = null; + routerScrollService.scrollElement = 'window'; - subject = this.owner.factoryFor('router:main').create(); + subject = this.owner.factoryFor('service:router-scroll').create(); run(() => { subject.trigger('routeDidChange', getTransitionsMock('Hello/#World', false)); @@ -273,13 +196,11 @@ module('router-scroll', function(hooks) { } this.owner.register('service:fastboot', EmberObject.extend({ isFastBoot: false })); - this.owner.register('service:router-scroll', EmberObject.extend({ - position: null, - scrollElement: 'window' - })); - this.owner.register('router:main', EmberRouterScroll.extend()); + const routerScrollService = this.owner.lookup('service:router-scroll'); + routerScrollService.position = null; + routerScrollService.scrollElement = 'window'; - subject = this.owner.factoryFor('router:main').create(); + subject = this.owner.factoryFor('service:router-scroll').create(); run(() => { subject.trigger('routeDidChange', getTransitionsMock('Hello/#World', false)); @@ -296,15 +217,11 @@ module('router-scroll', function(hooks) { } this.owner.register('service:fastboot', EmberObject.extend({ isFastBoot: false })); - this.owner.register('service:router-scroll', EmberObject.extend({ - get position() { - return { x: 1, y: 2 }; - }, - scrollElement: 'window' - })); - this.owner.register('router:main', EmberRouterScroll.extend()); + const routerScrollService = this.owner.lookup('service:router-scroll'); + routerScrollService.position = { x: 1, y: 2, }; + routerScrollService.scrollElement = 'window'; - subject = this.owner.factoryFor('router:main').create(); + subject = this.owner.factoryFor('service:router-scroll').create(); run(() => { subject.trigger('routeDidChange', getTransitionsMock('Hello/#')); @@ -324,15 +241,11 @@ module('router-scroll', function(hooks) { } this.owner.register('service:fastboot', EmberObject.extend({ isFastBoot: false })); - this.owner.register('service:router-scroll', EmberObject.extend({ - get position() { - return { x: 1, y: 2 }; - }, - scrollElement: 'window' - })); - this.owner.register('router:main', EmberRouterScroll.extend()); + const routerScrollService = this.owner.lookup('service:router-scroll'); + routerScrollService.position = { x: 1, y: 2, }; + routerScrollService.scrollElement = 'window'; - subject = this.owner.factoryFor('router:main').create(); + subject = this.owner.factoryFor('service:router-scroll').create(); run(() => { subject.trigger('routeDidChange', getTransitionsMock('Hello/#Bar')); @@ -349,15 +262,11 @@ module('router-scroll', function(hooks) { } this.owner.register('service:fastboot', EmberObject.extend({ isFastBoot: false })); - this.owner.register('service:router-scroll', EmberObject.extend({ - get position() { - return { x: 1, y: 20 }; - }, - scrollElement: 'window' - })); - this.owner.register('router:main', EmberRouterScroll.extend()); + const routerScrollService = this.owner.lookup('service:router-scroll'); + routerScrollService.position = { x: 1, y: 20, }; + routerScrollService.scrollElement = 'window'; - subject = this.owner.factoryFor('router:main').create(); + subject = this.owner.factoryFor('service:router-scroll').create(); run(() => { subject.trigger('routeDidChange', getTransitionsMock('Hello/World'));