From 6c80d7e5e1dc2319a11d1303456f2fe67c96225e Mon Sep 17 00:00:00 2001 From: Daniel Heath Date: Wed, 21 Sep 2016 10:59:56 +1000 Subject: [PATCH 1/6] Add test harness --- test/cases/smoothScrollIntoViewEnd.js | 10 ++++ test/cases/smoothScrollIntoViewStart.js | 9 +++ test/harness.html | 61 ++++++++++++++++++++ test/index.html | 74 +++++++++++++++++++++++++ test/support/raf.js | 32 +++++++++++ test/support/test-core.js | 36 ++++++++++++ test/support/test.css | 41 ++++++++++++++ 7 files changed, 263 insertions(+) create mode 100644 test/cases/smoothScrollIntoViewEnd.js create mode 100644 test/cases/smoothScrollIntoViewStart.js create mode 100644 test/harness.html create mode 100644 test/index.html create mode 100644 test/support/raf.js create mode 100644 test/support/test-core.js create mode 100644 test/support/test.css diff --git a/test/cases/smoothScrollIntoViewEnd.js b/test/cases/smoothScrollIntoViewEnd.js new file mode 100644 index 0000000..0c347cd --- /dev/null +++ b/test/cases/smoothScrollIntoViewEnd.js @@ -0,0 +1,10 @@ + + // $('.scrollable-parent p:last-child').scrollIntoView(); + later(function() { + $('.hello').scrollIntoView({behavior: 'smooth', block: 'end'}); + later(function() { + // assertScroll(window.scrollY, 80) + // assertScroll($('.scrollable-parent').scrollTop, 673) + done() + }) + }) diff --git a/test/cases/smoothScrollIntoViewStart.js b/test/cases/smoothScrollIntoViewStart.js new file mode 100644 index 0000000..b68de6e --- /dev/null +++ b/test/cases/smoothScrollIntoViewStart.js @@ -0,0 +1,9 @@ +// $('.scrollable-parent p:last-child').scrollIntoView(); +later(function() { + $('.hello').scrollIntoView({behavior: 'smooth', block: 'start'}); + later(function() { + // assertScroll(window.scrollY, 80) + // assertScroll($('.scrollable-parent').scrollTop, 905) + done() + }) +}) diff --git a/test/harness.html b/test/harness.html new file mode 100644 index 0000000..d9a0455 --- /dev/null +++ b/test/harness.html @@ -0,0 +1,61 @@ + + + + + smoothscroll tests - basic + + + + + + + + + +
+ +
+

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Qui iure obcaecati, repudiandae aspernatur cumque recusandae + adipisci consequuntur maiores, quo in nulla ratione facere distinctio beatae, quae consequatur ab labore dolorum.

+

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Qui iure obcaecati, repudiandae aspernatur cumque recusandae + adipisci consequuntur maiores, quo in nulla ratione facere distinctio beatae, quae consequatur ab labore dolorum.

+

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Qui iure obcaecati, repudiandae aspernatur cumque recusandae + adipisci consequuntur maiores, quo in nulla ratione facere distinctio beatae, quae consequatur ab labore dolorum.

+

hello!

+

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Qui iure obcaecati, repudiandae aspernatur cumque recusandae + adipisci consequuntur maiores, quo in nulla ratione facere distinctio beatae, quae consequatur ab labore dolorum.

+

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Qui iure obcaecati, repudiandae aspernatur cumque recusandae + adipisci consequuntur maiores, quo in nulla ratione facere distinctio beatae, quae consequatur ab labore dolorum.

+

Lorem ipsum dolor sit amet, consectetur adipisicing elit. Qui iure obcaecati, repudiandae aspernatur cumque recusandae + adipisci consequuntur maiores, quo in nulla ratione facere distinctio beatae, quae consequatur ab labore dolorum.

+
+
+ + + + + + + + + diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..1654fc3 --- /dev/null +++ b/test/index.html @@ -0,0 +1,74 @@ + + + + + smooth scroll polyfill tests + + + + + + + + + + + + diff --git a/test/support/raf.js b/test/support/raf.js new file mode 100644 index 0000000..5f4a187 --- /dev/null +++ b/test/support/raf.js @@ -0,0 +1,32 @@ + +// http://paulirish.com/2011/requestanimationframe-for-smart-animating/ +// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating + +// requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel + +// MIT license + +(function() { +var lastTime = 0; +var vendors = ['ms', 'moz', 'webkit', 'o']; +for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; + window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] + || window[vendors[x]+'CancelRequestAnimationFrame']; +} + +if (!window.requestAnimationFrame) + window.requestAnimationFrame = function(callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function() { callback(currTime + timeToCall); }, + timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + +if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; +}()); diff --git a/test/support/test-core.js b/test/support/test-core.js new file mode 100644 index 0000000..b616760 --- /dev/null +++ b/test/support/test-core.js @@ -0,0 +1,36 @@ +function $(selector) { + return document.querySelector(selector); +} + +function later(cb) { + setTimeout(cb, 600) +} + +function fail(msg) { + document.querySelector('.test-result').textContent += msg +} + +function done(msg) { + var resultCtr = document.querySelector('.test-result') + if (resultCtr.textContent === "") { + resultCtr.classList.add('success') + } + resultCtr.textContent += "complete" +} + +function assertScroll(actual, expected) { + // Allow a little bit of leeway + if (Math.abs(actual - expected) > 3) { + fail(":failure: Expected '" + actual + "' to be '" + expected + "':") + } +} + +function parseQuery(qstr) { + var query = {}; + var a = qstr.substr(1).split('&'); + for (var i = 0; i < a.length; i++) { + var b = a[i].split('='); + query[decodeURIComponent(b[0])] = decodeURIComponent(b[1] || ''); + } + return query; +} diff --git a/test/support/test.css b/test/support/test.css new file mode 100644 index 0000000..e87988e --- /dev/null +++ b/test/support/test.css @@ -0,0 +1,41 @@ + +body { + background-color: #fefefe; + color: #212121; + font-family: 'Roboto Condensed', Arial, sans-serif; + font-size: 20px; + + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.top-padder { + width: 100%; + height: 400px; +} + +.scrollable-parent { + background-color: #efefef; + border-radius: 4px; + margin: 20px 0 0; + max-height: 200px; + overflow: scroll; + padding: 30px 50px; +} +.hello { + text-align: center; +} + +.test-result { + position: fixed; + top: 50%; + left: 0; + right: 0; + text-align: center; + color: red; +} + +.test-result.success { + color: green; +} From e7430bc827b4b0bc1a03a7579809288fd85fdabe Mon Sep 17 00:00:00 2001 From: Daniel Heath Date: Wed, 21 Sep 2016 12:54:11 +1000 Subject: [PATCH 2/6] Add support for overriding browser builtin smooth-scrolling --- src/smoothscroll.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/smoothscroll.js b/src/smoothscroll.js index 6fdf9b2..ae76fc2 100644 --- a/src/smoothscroll.js +++ b/src/smoothscroll.js @@ -9,10 +9,12 @@ */ // polyfill - function polyfill() { + function polyfill(overrideBrowserImplementation) { // return when scrollBehavior interface is supported if ('scrollBehavior' in d.documentElement.style) { - return; + if (!overrideBrowserImplementation) { + return; + } } /* @@ -266,6 +268,6 @@ module.exports = { polyfill: polyfill }; } else { // global - polyfill(); + polyfill(window.__smoothscrollForcePolyfill); } })(window, document); From c2061a5ce3df7adb24c10229693e31fb29816bc5 Mon Sep 17 00:00:00 2001 From: Daniel Heath Date: Tue, 20 Sep 2016 11:49:10 +1000 Subject: [PATCH 3/6] rename shouldBailOut -> scrollIsInstant --- src/smoothscroll.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/smoothscroll.js b/src/smoothscroll.js index ae76fc2..96438f5 100644 --- a/src/smoothscroll.js +++ b/src/smoothscroll.js @@ -61,11 +61,11 @@ /** * indicates if a smooth behavior should be applied - * @method shouldBailOut + * @method scrollIsInstant * @param {Number|Object} x * @returns {Boolean} */ - function shouldBailOut(x) { + function scrollIsInstant(x) { if (typeof x !== 'object' || x.behavior === undefined || x.behavior === 'auto' @@ -186,7 +186,7 @@ // w.scroll and w.scrollTo w.scroll = w.scrollTo = function() { // avoid smooth behavior if not required - if (shouldBailOut(arguments[0])) { + if (scrollIsInstant(arguments[0])) { original.scroll.call( w, arguments[0].left || arguments[0], @@ -207,7 +207,7 @@ // w.scrollBy w.scrollBy = function() { // avoid smooth behavior if not required - if (shouldBailOut(arguments[0])) { + if (scrollIsInstant(arguments[0])) { original.scrollBy.call( w, arguments[0].left || arguments[0], @@ -228,7 +228,7 @@ // Element.prototype.scrollIntoView Element.prototype.scrollIntoView = function() { // avoid smooth behavior if not required - if (shouldBailOut(arguments[0])) { + if (scrollIsInstant(arguments[0])) { original.scrollIntoView.call(this, arguments[0] || true); return; } From 71ac7f69b41a09a48dcc56e18f2d05faa193d5cc Mon Sep 17 00:00:00 2001 From: Daniel Heath Date: Tue, 20 Sep 2016 11:48:10 +1000 Subject: [PATCH 4/6] Support 'block: end' scroll behavior --- src/smoothscroll.js | 82 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 67 insertions(+), 15 deletions(-) diff --git a/src/smoothscroll.js b/src/smoothscroll.js index 96438f5..13f1868 100644 --- a/src/smoothscroll.js +++ b/src/smoothscroll.js @@ -59,6 +59,54 @@ return 0.5 * (1 - Math.cos(Math.PI * k)); } + /** + * Normalizes valid scrollIntoView arguments into an arguments object + * @method normalizeArgs + * @param {Boolean|Object=} x + * @returns {Object} + */ + function normalizeArgs(x) { + if (typeof x === 'undefined') { + return { + block: 'start', + behavior: 'auto' + }; + } + + if (typeof x === 'boolean') { + return { + block: (x ? 'start' : 'end'), + behavior: 'auto' + }; + } + + if (typeof x === 'object') { + if ( + (x.behavior !== undefined) && + (x.behavior !== 'auto') && + (x.behavior !== 'instant') && + (x.behavior !== 'smooth') + ) { + throw new TypeError('behavior not valid'); + } + + if ( + (x.block !== undefined) && + (x.block !== 'start') && + (x.block !== 'end') + ) { + throw new TypeError('block not valid'); + } + + return { + block: x.block === 'end' ? 'end' : 'start', + behavior: x.behavior === 'smooth' ? 'smooth' : 'auto' + } + } + + throw new TypeError('scrollIntoView accepts undefined, boolean or object as its first argument'); + } + /** * indicates if a smooth behavior should be applied * @method scrollIsInstant @@ -227,8 +275,10 @@ // Element.prototype.scrollIntoView Element.prototype.scrollIntoView = function() { + var opts = normalizeArgs(arguments[0]); + // avoid smooth behavior if not required - if (scrollIsInstant(arguments[0])) { + if (scrollIsInstant(arguments[0]) && opts.block == "top") { original.scrollIntoView.call(this, arguments[0] || true); return; } @@ -239,27 +289,29 @@ var clientRects = this.getBoundingClientRect(); if (scrollableParent !== d.body) { + var clientAdj = clientRects.top; + if (opts.block === 'end') { + var scrollbarHeight = scrollableParent.offsetHeight - scrollableParent.clientHeight + clientAdj = clientRects.bottom - parentRects.height + scrollbarHeight; + } + // reveal element inside parent smoothScroll.call( this, scrollableParent, scrollableParent.scrollLeft + clientRects.left - parentRects.left, - scrollableParent.scrollTop + clientRects.top - parentRects.top + scrollableParent.scrollTop + clientAdj - parentRects.top ); - // reveal parent in viewport - w.scrollBy({ - left: parentRects.left, - top: parentRects.top, - behavior: 'smooth' - }); - } else { - // reveal element in viewport - w.scrollBy({ - left: clientRects.left, - top: clientRects.top, - behavior: 'smooth' - }); + + // scroll parent into view + return scrollableParent.scrollIntoView(arguments[0]); } + + w.scrollBy({ + left: clientRects.left, + top: opts.block !== 'end' ? clientRects.top : clientRects.bottom - w.innerHeight, + behavior: opts.behavior + }); }; } From b6ea3416ae035cf1aaa85835fa6fb52ca38833c2 Mon Sep 17 00:00:00 2001 From: Daniel Heath Date: Wed, 21 Sep 2016 14:35:34 +1000 Subject: [PATCH 5/6] Compile and bump version --- dist/smoothscroll.js | 98 ++++++++++++++++++++++++++++++++++---------- package.json | 2 +- 2 files changed, 77 insertions(+), 23 deletions(-) diff --git a/dist/smoothscroll.js b/dist/smoothscroll.js index 523a13b..b91fa94 100644 --- a/dist/smoothscroll.js +++ b/dist/smoothscroll.js @@ -15,10 +15,12 @@ */ // polyfill - function polyfill() { + function polyfill(overrideBrowserImplementation) { // return when scrollBehavior interface is supported if ('scrollBehavior' in d.documentElement.style) { - return; + if (!overrideBrowserImplementation) { + return; + } } /* @@ -63,13 +65,61 @@ return 0.5 * (1 - Math.cos(Math.PI * k)); } + /** + * Normalizes valid scrollIntoView arguments into an arguments object + * @method normalizeArgs + * @param {Boolean|Object=} x + * @returns {Object} + */ + function normalizeArgs(x) { + if (typeof x === 'undefined') { + return { + block: 'start', + behavior: 'auto' + }; + } + + if (typeof x === 'boolean') { + return { + block: (x ? 'start' : 'end'), + behavior: 'auto' + }; + } + + if (typeof x === 'object') { + if ( + (x.behavior !== undefined) && + (x.behavior !== 'auto') && + (x.behavior !== 'instant') && + (x.behavior !== 'smooth') + ) { + throw new TypeError('behavior not valid'); + } + + if ( + (x.block !== undefined) && + (x.block !== 'start') && + (x.block !== 'end') + ) { + throw new TypeError('block not valid'); + } + + return { + block: x.block === 'end' ? 'end' : 'start', + behavior: x.behavior === 'smooth' ? 'smooth' : 'auto' + } + } + + throw new TypeError('scrollIntoView accepts undefined, boolean or object as its first argument'); + } + /** * indicates if a smooth behavior should be applied - * @method shouldBailOut + * @method scrollIsInstant * @param {Number|Object} x * @returns {Boolean} */ - function shouldBailOut(x) { + function scrollIsInstant(x) { if (typeof x !== 'object' || x.behavior === undefined || x.behavior === 'auto' @@ -190,7 +240,7 @@ // w.scroll and w.scrollTo w.scroll = w.scrollTo = function() { // avoid smooth behavior if not required - if (shouldBailOut(arguments[0])) { + if (scrollIsInstant(arguments[0])) { original.scroll.call( w, arguments[0].left || arguments[0], @@ -211,7 +261,7 @@ // w.scrollBy w.scrollBy = function() { // avoid smooth behavior if not required - if (shouldBailOut(arguments[0])) { + if (scrollIsInstant(arguments[0])) { original.scrollBy.call( w, arguments[0].left || arguments[0], @@ -231,8 +281,10 @@ // Element.prototype.scrollIntoView Element.prototype.scrollIntoView = function() { + var opts = normalizeArgs(arguments[0]); + // avoid smooth behavior if not required - if (shouldBailOut(arguments[0])) { + if (scrollIsInstant(arguments[0]) && opts.block == "top") { original.scrollIntoView.call(this, arguments[0] || true); return; } @@ -243,27 +295,29 @@ var clientRects = this.getBoundingClientRect(); if (scrollableParent !== d.body) { + var clientAdj = clientRects.top; + if (opts.block === 'end') { + var scrollbarHeight = scrollableParent.offsetHeight - scrollableParent.clientHeight + clientAdj = clientRects.bottom - parentRects.height + scrollbarHeight; + } + // reveal element inside parent smoothScroll.call( this, scrollableParent, scrollableParent.scrollLeft + clientRects.left - parentRects.left, - scrollableParent.scrollTop + clientRects.top - parentRects.top + scrollableParent.scrollTop + clientAdj - parentRects.top ); - // reveal parent in viewport - w.scrollBy({ - left: parentRects.left, - top: parentRects.top, - behavior: 'smooth' - }); - } else { - // reveal element in viewport - w.scrollBy({ - left: clientRects.left, - top: clientRects.top, - behavior: 'smooth' - }); + + // scroll parent into view + return scrollableParent.scrollIntoView(arguments[0]); } + + w.scrollBy({ + left: clientRects.left, + top: opts.block !== 'end' ? clientRects.top : clientRects.bottom - w.innerHeight, + behavior: opts.behavior + }); }; } @@ -272,6 +326,6 @@ module.exports = { polyfill: polyfill }; } else { // global - polyfill(); + polyfill(window.__smoothscrollForcePolyfill); } })(window, document); diff --git a/package.json b/package.json index b3eb770..f6754c1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "title": "smoothscroll", "name": "smoothscroll-polyfill", - "version": "0.3.3", + "version": "0.4.0", "author": { "name": "Dustan Kasten", "email": "dustan.kasten@gmail.com", From 8240145179affc1e5bbf0232b3563d474bb62aac Mon Sep 17 00:00:00 2001 From: Daniel Heath Date: Wed, 21 Sep 2016 16:10:07 +1000 Subject: [PATCH 6/6] Returns to only supporting a single level of scrollable nesting, but now it works. --- dist/smoothscroll.js | 47 +++++++++++++++++++++++--------------------- src/smoothscroll.js | 45 ++++++++++++++++++++++-------------------- 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/dist/smoothscroll.js b/dist/smoothscroll.js index b91fa94..cf93589 100644 --- a/dist/smoothscroll.js +++ b/dist/smoothscroll.js @@ -1,5 +1,5 @@ /* - * smoothscroll polyfill - v0.3.3 + * smoothscroll polyfill - v0.4.0 * https://iamdustan.github.io/smoothscroll * 2016 (c) Dustan Kasten, Jeremias Menichelli - MIT License */ @@ -233,6 +233,29 @@ }); } + function scrollWithinParentElem(el, opts) { + var scrollableParent = findScrollableParent(el); + if (scrollableParent === d.body) { + return; + } + + var clientRects = el.getBoundingClientRect(); + var parentRects = scrollableParent.getBoundingClientRect(); + var clientAdj = clientRects.top; + if (opts.block === 'end') { + var scrollbarHeight = scrollableParent.offsetHeight - scrollableParent.clientHeight + clientAdj = clientRects.bottom - parentRects.height + scrollbarHeight; + } + + // reveal element inside parent + smoothScroll.call( + this, + scrollableParent, + scrollableParent.scrollLeft + clientRects.left - parentRects.left, + scrollableParent.scrollTop + clientAdj - parentRects.top + ); + } + /* * ORIGINAL METHODS OVERRIDES */ @@ -290,29 +313,9 @@ } // LET THE SMOOTHNESS BEGIN! - var scrollableParent = findScrollableParent(this); - var parentRects = scrollableParent.getBoundingClientRect(); var clientRects = this.getBoundingClientRect(); - if (scrollableParent !== d.body) { - var clientAdj = clientRects.top; - if (opts.block === 'end') { - var scrollbarHeight = scrollableParent.offsetHeight - scrollableParent.clientHeight - clientAdj = clientRects.bottom - parentRects.height + scrollbarHeight; - } - - // reveal element inside parent - smoothScroll.call( - this, - scrollableParent, - scrollableParent.scrollLeft + clientRects.left - parentRects.left, - scrollableParent.scrollTop + clientAdj - parentRects.top - ); - - // scroll parent into view - return scrollableParent.scrollIntoView(arguments[0]); - } - + scrollWithinParentElem(this, opts); w.scrollBy({ left: clientRects.left, top: opts.block !== 'end' ? clientRects.top : clientRects.bottom - w.innerHeight, diff --git a/src/smoothscroll.js b/src/smoothscroll.js index 13f1868..4e54d0d 100644 --- a/src/smoothscroll.js +++ b/src/smoothscroll.js @@ -227,6 +227,29 @@ }); } + function scrollWithinParentElem(el, opts) { + var scrollableParent = findScrollableParent(el); + if (scrollableParent === d.body) { + return; + } + + var clientRects = el.getBoundingClientRect(); + var parentRects = scrollableParent.getBoundingClientRect(); + var clientAdj = clientRects.top; + if (opts.block === 'end') { + var scrollbarHeight = scrollableParent.offsetHeight - scrollableParent.clientHeight + clientAdj = clientRects.bottom - parentRects.height + scrollbarHeight; + } + + // reveal element inside parent + smoothScroll.call( + this, + scrollableParent, + scrollableParent.scrollLeft + clientRects.left - parentRects.left, + scrollableParent.scrollTop + clientAdj - parentRects.top + ); + } + /* * ORIGINAL METHODS OVERRIDES */ @@ -284,29 +307,9 @@ } // LET THE SMOOTHNESS BEGIN! - var scrollableParent = findScrollableParent(this); - var parentRects = scrollableParent.getBoundingClientRect(); var clientRects = this.getBoundingClientRect(); - if (scrollableParent !== d.body) { - var clientAdj = clientRects.top; - if (opts.block === 'end') { - var scrollbarHeight = scrollableParent.offsetHeight - scrollableParent.clientHeight - clientAdj = clientRects.bottom - parentRects.height + scrollbarHeight; - } - - // reveal element inside parent - smoothScroll.call( - this, - scrollableParent, - scrollableParent.scrollLeft + clientRects.left - parentRects.left, - scrollableParent.scrollTop + clientAdj - parentRects.top - ); - - // scroll parent into view - return scrollableParent.scrollIntoView(arguments[0]); - } - + scrollWithinParentElem(this, opts); w.scrollBy({ left: clientRects.left, top: opts.block !== 'end' ? clientRects.top : clientRects.bottom - w.innerHeight,