-
Notifications
You must be signed in to change notification settings - Fork 342
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
Refactor #11
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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; | ||
|
||
/* | ||
* 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(); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
} | ||
|
||
/* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this We could maybe get away with it since we only support scrolling the entire document or 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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' ) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Firefox falls back to original method if behavior is |
||
// 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added |
||
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)); |
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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.