diff --git a/dist/smoothscroll.js b/dist/smoothscroll.js
index 523a13b..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
*/
@@ -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'
@@ -183,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
*/
@@ -190,7 +263,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 +284,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,39 +304,23 @@
// 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;
}
// LET THE SMOOTHNESS BEGIN!
- var scrollableParent = findScrollableParent(this);
- var parentRects = scrollableParent.getBoundingClientRect();
var clientRects = this.getBoundingClientRect();
- if (scrollableParent !== d.body) {
- // reveal element inside parent
- smoothScroll.call(
- this,
- scrollableParent,
- scrollableParent.scrollLeft + clientRects.left - parentRects.left,
- scrollableParent.scrollTop + clientRects.top - parentRects.top
- );
- // reveal parent in viewport
- w.scrollBy({
- left: parentRects.left,
- top: parentRects.top,
- behavior: 'smooth'
- });
- } else {
- // reveal element in viewport
- w.scrollBy({
+ scrollWithinParentElem(this, opts);
+ w.scrollBy({
left: clientRects.left,
- top: clientRects.top,
- behavior: 'smooth'
- });
- }
+ top: opts.block !== 'end' ? clientRects.top : clientRects.bottom - w.innerHeight,
+ behavior: opts.behavior
+ });
};
}
@@ -272,6 +329,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",
diff --git a/src/smoothscroll.js b/src/smoothscroll.js
index 6fdf9b2..4e54d0d 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;
+ }
}
/*
@@ -57,13 +59,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'
@@ -177,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
*/
@@ -184,7 +257,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],
@@ -205,7 +278,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],
@@ -225,39 +298,23 @@
// 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;
}
// LET THE SMOOTHNESS BEGIN!
- var scrollableParent = findScrollableParent(this);
- var parentRects = scrollableParent.getBoundingClientRect();
var clientRects = this.getBoundingClientRect();
- if (scrollableParent !== d.body) {
- // reveal element inside parent
- smoothScroll.call(
- this,
- scrollableParent,
- scrollableParent.scrollLeft + clientRects.left - parentRects.left,
- scrollableParent.scrollTop + clientRects.top - parentRects.top
- );
- // reveal parent in viewport
- w.scrollBy({
- left: parentRects.left,
- top: parentRects.top,
- behavior: 'smooth'
- });
- } else {
- // reveal element in viewport
- w.scrollBy({
+ scrollWithinParentElem(this, opts);
+ w.scrollBy({
left: clientRects.left,
- top: clientRects.top,
- behavior: 'smooth'
- });
- }
+ top: opts.block !== 'end' ? clientRects.top : clientRects.bottom - w.innerHeight,
+ behavior: opts.behavior
+ });
};
}
@@ -266,6 +323,6 @@
module.exports = { polyfill: polyfill };
} else {
// global
- polyfill();
+ polyfill(window.__smoothscrollForcePolyfill);
}
})(window, document);
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;
+}