Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor #11

Merged
merged 2 commits into from
Jun 12, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dist/smoothscroll.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/smoothscroll.raf.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

267 changes: 174 additions & 93 deletions smoothscroll.js
Original file line number Diff line number Diff line change
@@ -1,156 +1,237 @@
(function () {
(function (w, doc, undefined) {
'use strict';

if ('scrollBehavior' in document.documentElement.style) return;

// TODO: make this intelligent according to distance.
var SCROLL_TIME = 300;

var originalScrollTo = window.scrollTo;
var originalScrollBy = window.scrollBy;
var originalScrollIntoView = Element.prototype.scrollIntoView;

// store generally accessible frame id in case a new scroll animation is triggered before the previous
// completes, we can cancel the previous scroll.
var frame;

var startY, startX, endX, endY;

/*
* alias
* w: window global object
* doc: document
* undefined: undefined
*/

// return if scrollBehavior is supported
if ('scrollBehavior' in doc.documentElement.style) return;

var SCROLL_TIME = 768,
// legacy scrolling methods
originalScrollTo = w.scrollTo,
originalScrollBy = w.scrollBy,
originalScrollIntoView = w.Element.prototype.scrollIntoView,
// global frame variable to avoid collision
frame,
// global metric variables
startY, startX, endX, endY;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to have multiple scrollBy/scrollTo’s going at once? I guess we’re only supporting those 3 scrolling methods and not element-with-overflow-scrolling.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not on the window object, but it could be possible that browsers allow scrolling in the window and an element, but I would have to test. Either way, functionality was not extended or modified.


/*
* returns actual time
* @method now
* @returns {Date}
*/
function now() {
return window.performance !== undefined && window.performance.now !== undefined ? window.performance.now() : Date.now !== undefined ? Date.now() : new Date().getTime();
return w.performance !== undefined && w.performance.now !== undefined ?
// if performance object supported return now, if not fallback to date object
w.performance.now() : Date.now !== undefined ? Date.now() : new Date().getTime();
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added scrollX and scrollY for modern browsers.

// ease-in-out
/*
* returns result of applying ease math function to a number
* @method ease
* @param {Number} k
* @returns {Number}
*/
function ease(k) {
return 0.5 * (1 - Math.cos(Math.PI * k));
}

/*
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this TODO would still be a nice to have. I have what the perf implications of looking up a CSS property on every element is though.

We could maybe get away with it since we only support scrolling the entire document or Element.prototype.scrollIntoView.

First thought is that in either window.scrollTo/scrollBy method we look at the html and body tags for that css property and if it is set we return calling the same method with the scroll behavior set.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, by that point we could easily support it when Element.scrollBy support is added by checking for that behavior when called. No need to traverse the DOM looking for all of those elements. JIT property checks.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeap, I think is nitpicky but it would a nice-to-have and not that hard to implement.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for that right now. I’ll take this PR as is. I made another issue (#12) for that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Roger that 👍

* returns true if first argument is an options object and contains a smooth behavior
* @method shouldBailOut
* @param {Number|Object} x
* @returns {Boolean}
*/
function shouldBailOut(x) {
return typeof x === 'undefined' || x.behavior !== 'smooth';
if (typeof x !== 'object' || x.behavior === undefined || x.behavior === 'auto' ) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Firefox falls back to original method if behavior is undefinedor auto.

// first arg not an object, or behavior is auto or undefined
return true;
} else if (x.behavior === 'smooth') {
// first argument is an object and behavior is smooth
return false;
} else {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

x is an object but behavior property is not supported.

// behavior not supported, throw error as Firefox implementation 37.0.2
throw new TypeError(x.behavior + ' is not a valid value for enumeration ScrollBehavior');
}
}

/*
* changes scroll position inside an element
* @method scrollElement
* @params {Node} el
* @params {Number} x
* @params {Number} y
*/
function scrollElement(el, x, y) {
el.scrollTop = y;
el.scrollLeft = x;
}

/*
* finds scrollable parent of an element
* @method findScrollableParent
* @params {Node} el
*/
function findScrollableParent(el) {
if (el.clientHeight < el.scrollHeight ||
el.clientWidth < el.scrollWidth) {
return el;
}

// only continue scaling if parentNode is valid
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if el.parentNode.parentNodeis not defined is because el.parentNode is html tag so we should stop looking up.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as an aside: I think there is some other process we’ll need for support in shadow DOM, but I don’t know anything more than that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I say @ebidel issue. Seriously I don't know that either. It could be a fix in a future version if you agree.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Shadow DOM/Web Components aren’t really high on my priority list these days. Of course to be a proper polyfill it should support that. I’ll personally neglect it until requests/issues come in for it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally agree on this.

if (el.parentNode.parentNode) {
return findScrollableParent(el.parentNode);
}
}

/*
* scrolls window with a smooth behavior
* @method smoothScroll
* @params {Number} x
* @params {Number} y
*/
function smoothScroll(x, y) {
var sx = window.pageXOffset;
var sy = window.pageYOffset;
var sx = w.scrollX || w.pageXOffset,
sy = w.scrollY || w.pageYOffset,
startTime = now();

if (typeof startX === 'undefined') {
if (startX === undefined) {
startX = sx;
startY = sy;
endX = x;
endY = y;
}

var startTime = now();
// cancel frame is there is an scroll event happening
if (frame) {
w.cancelAnimationFrame(frame);
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this so that if a user starts manually scrolling in the midst of an auto-scroll, we return control back to them?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, but I don't know if that's expected neither cause it happens so fast in Firefox that I can really test. I guess it should. I can cacnel the animation frame onscroll if you want.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, idk if it’s that common of a use case since the scroll happens in <= 400ms.

Maybe I’ll read the spec again someday to see if there is a recommendation or requirement around that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, also I've seen that 400ms sometimes is like too fast, considering usual numbers in frame rates I think the best would be to increase it to 768.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scrolling box is scrolled in a smooth fashion using a user-agent-defined timing function over a user-agent-defined period of time. User agents should follow platform conventions, if any.

https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior#Browser_compatibility

400ms is ~12 steps, right? My preference is to do something that makes the scroll time proportional to the distance covered with some min and max action and find something that “feels” natural.

Maybe between 5 and 25 steps (768 is ~23 steps) as a first pass? I’ll make another issue for that as well.


frame = w.requestAnimationFrame(step);

// TODO: look into polyfilling scroll-behavior: smooth css property
var step = function() {
var time = now();
var elapsed = (time - startTime) / SCROLL_TIME;
// scroll looping over a frame
function step() {
var time = now(), value, cx, cy,
elapsed = (time - startTime) / SCROLL_TIME;

// avoid elapsed times higher than one
elapsed = elapsed > 1 ? 1 : elapsed;

var value = ease(elapsed);
var cx = sx + ( x - sx ) * value;
var cy = sy + ( y - sy ) * value;
value = ease(elapsed);
cx = sx + ( x - sx ) * value;
cy = sy + ( y - sy ) * value;

originalScrollTo(cx, cy);

// return if end points have been reached
if (cx === endX && cy === endY) {
startX = startY = endX = endY = undefined;
return;
}

frame = requestAnimationFrame(step);
};

if (frame) cancelAnimationFrame(frame);
frame = requestAnimationFrame(step);
}

window.scroll = window.scrollTo = function(x, y, scrollOptions) {
if (shouldBailOut(scrollOptions))
return originalScrollTo(x, y);
return smoothScroll(x, y);
};

window.scrollBy = function(x, y, scrollOptions) {
if (shouldBailOut(scrollOptions))
return originalScrollBy(x, y);

var sx = window.pageXOffset;
var sy = window.pageYOffset;

return smoothScroll(x + sx, y + sy);
};

var elementRects, scrollableParent;
function scrollElement(el, x, y) {
el.scrollTop = y;
el.scrollLeft = x;
frame = w.requestAnimationFrame(step);
}
}

function scroll(el, endCoords) {
var sx = el.scrollLeft;
var sy = el.scrollTop;

var x = endCoords.left;
var y = endCoords.top;

if (typeof startX === 'undefined') {
/*
* scrolls inside an element with a smooth behavior
* @method smoothScrollElement
* @params {Node} el
* @params {Object} endCoords
*/
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename function to match naming criteria.

function scrollSmoothElement(el, endCoords) {
var sx = el.scrollLeft,
sy = el.scrollTop,
x = endCoords.left,
y = endCoords.top,
startTime = now();

if (startX === undefined) {
startX = sx;
startY = sy;
endX = endCoords.left;
endY = endCoords.top;
}

var startTime = now();
// cancel frame is there is an scroll event happening
if (frame) {
w.cancelAnimationFrame(frame);
}

var step = function() {
var time = now();
var elapsed = (time - startTime) / SCROLL_TIME;
frame = w.requestAnimationFrame(step);

// scroll looping over a frame
function step() {
var time = now(), value, cx, cy,
elapsed = (time - startTime) / SCROLL_TIME;

// avoid elapsed times higher than one
elapsed = elapsed > 1 ? 1 : elapsed;

var value = ease(elapsed);
var cx = sx + ( x - sx ) * value;
var cy = sy + ( y - sy ) * value;
value = ease(elapsed);
cx = sx + ( x - sx ) * value;
cy = sy + ( y - sy ) * value;

scrollElement(el, cx, cy);

// return if end points have been reached
if (cx === endX && cy === endY) {
startX = startY = endX = endY = undefined;
return;
}

frame = requestAnimationFrame(step);
};

if (frame) cancelAnimationFrame(frame);
frame = requestAnimationFrame(step);
frame = w.requestAnimationFrame(step);
}
}

function findScrollableParent(el) {
if (el.clientHeight < el.scrollHeight ||
el.clientWidth < el.scrollWidth)
return el;
return findScrollableParent(el.parentNode);
}
// ORIGINAL METHODS OVERRIDES
// window.scroll and window.scrollTo
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • arguments[0] can be object or number, if it's not obejct default behavior will be called
  • argumentscan be an object with auto or undefined behavior or both coordinates, this strucure adds support for both situations

w.scroll = w.scrollTo = function() {
if (shouldBailOut(arguments[0])) {
// if first argument is an object with auto behavior send left and top coordenates
return originalScrollTo.call(w, arguments[0].left || arguments[0], arguments[0].top || arguments[1]);
} else {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added ~~ operators to support strings like '10' as Firefox implementation.

return smoothScroll.call(w, ~~arguments[0].left, ~~arguments[0].top);
}
};

// window.scrollBy
w.scrollBy = function() {
if (shouldBailOut(arguments[0])) {
// if first argument is an object with auto behavior send left and top coordenates
return originalScrollBy.call(w, arguments[0].left || arguments[0], arguments[0].top || arguments[1]);
} else {
var sx = w.scrollX || w.pageXOffset,
sy = w.scrollY || w.pageYOffset;

var origElementScrollIntoView = Element.prototype.scrollIntoView;
return smoothScroll(~~arguments[0].left + sx, ~~arguments[0].top + sy);
}
};

Element.prototype.scrollIntoView = function(toTop, scrollOptions) {
if (shouldBailOut(scrollOptions)) return origElementScrollIntoView.call(this, toTop);
// Element.scrollIntoView
Element.prototype.scrollIntoView = function() {
if (shouldBailOut(arguments[0])) {
return originalScrollIntoView.call(this, arguments[0] || true);
}

scrollableParent = findScrollableParent(this);
var style = window.getComputedStyle(scrollableParent, null);
var paddingLeft = parseInt(style.getPropertyValue('padding-left'), 10);
var paddingTop = parseInt(style.getPropertyValue('padding-top'), 10);
var elementRects,
scrollableParent = findScrollableParent(this),
style = w.getComputedStyle(scrollableParent, null),
paddingLeft = parseInt(style.getPropertyValue('padding-left'), 10),
paddingTop = parseInt(style.getPropertyValue('padding-top'), 10);

elementRects = {
top: this.offsetTop - (paddingTop * 2),
left: this.offsetLeft - (paddingLeft * 2)
};

return scroll(scrollableParent, elementRects);
return scrollSmoothElement(scrollableParent, elementRects);
};

}());

}(window, document));