flamingo-carotene-smooth-scroll-to
Version:
The smooth scroll module enables to scroll smoothly to a given y coordinate or dom element
291 lines (245 loc) • 7.54 kB
JavaScript
/**
* Scroll the page to a given Y Coordinate or to a dom element
* Animation is timebased, animation will skip frames, if client could not handle the speed
* */
class SmoothScrollTo {
constructor () {
// animation duration
this.duration = 500
// Y Position offset to browsers top
this.offset = 0
// animation update interval
this.updateInterval = 25
// internal property - starttime of the animation
this.animationStartTime = null
// interval reference
this.timer = null
// internal property - when animation was started - target position
this.currentTargetY = null
// internal property - when animation was started - start position
this.currentStartY = null
// callback that is called when a animation step was done
this.animationStepCallback = null
// callback that is called when a animation is finished
this.finishCallback = null
// config switch - cancel scroll animation, if users manually scrolls while smooth scrolling is active
this.cancelAnimationOnUserScroll = true
// internal state - is a scrolling currently in progress
this.scrollingInProgress = false
// the last position to which scrolling happens
this.lastScrollPosition = null
// direction to which the current scrolling will take place
this.scrollDirection = null
window.onscroll = this.onScrollHandler.bind(this)
}
/**
* Determines the Scroll Direction (up/down)
*
* @param fromY
* @param toY
* @returns {string}
*/
getScrollDirection (fromY, toY) {
if (fromY < toY) {
return 'down'
}
else {
return 'up'
}
}
/**
* Event Listener for "onscroll", to detect when user is scrolling...
* @param event
*/
onScrollHandler (event) {
// if no scrolling is in progress by this function - get outta here
if (!this.scrollingInProgress) {
return
}
// if scrolling is not cancelable on manual scrolling - leave instantly
if (!this.cancelAnimationOnUserScroll) {
return
}
const currentY = window.pageYOffset
const direction = this.getScrollDirection(this.lastScrollPosition, currentY)
// scrolling direction changed - stop automatic scrolling
if (this.scrollDirection !== direction) {
this.stop()
}
this.lastScrollPosition = currentY
}
/**
* Change the overall animation duration
*
* @param {number} duration
* @returns {SmoothScrollTo}
*/
setDuration (duration) {
this.duration = duration
return this
}
/**
* Sets the behaviour, that manually scrolling cancels automatic scrolling
*
* @param {boolean} state
* @returns {SmoothScrollTo}
*/
setCancelAnimationOnUserScroll (state) {
this.cancelAnimationOnUserScroll = state
return this
}
/**
* Set Scrolling target Offset
* @param {number} offset
* @returns {SmoothScrollTo}
*/
setOffset (offset) {
this.offset = offset
return this
}
/**
* Set AnimationStep Callback
* Function will be called with one parameter, which is the instance of this class -> callback(this)
* @param {function} callback
* @returns {SmoothScrollTo}
*/
setAnimationStepCallback(callback) {
this.animationStepCallback = callback
return this
}
/**
* Sets the currentTargetY
* This function can be used inside AnimationStepCallback to correct the targetPosition while animation is running
* @param {number} currentTargetY
* @returns {SmoothScrollTo}
*/
setCurrentTargetY (currentTargetY) {
this.currentTargetY = currentTargetY
return this
}
/**
* Returns the current scroll Targetposition if animation is running
* @returns {number}
*/
getCurrentTargetY () {
return this.currentTargetY
}
/**
* Scrolls to a domNode or a position
* @param {HTMLElement|number} target
* @returns {SmoothScrollTo}
*/
scrollTo(target) {
if (target instanceof HTMLElement) {
this.scrollToElement(target)
return this
}
if (typeof target === 'number') {
this.scrollToY(target)
return this
}
return this
}
/**
* Scrolls to position
* @param {number} scrollY
*/
scrollToY (scrollY) {
this.currentStartY = window.pageYOffset
this.currentTargetY = scrollY - this.offset
this._startAnimation(this.animationStepCallback)
}
/**
* Scrolls to a domNode
* @param {HTMLElement} domElement
*/
scrollToElement (domElement) {
this.currentStartY = window.pageYOffset
this.setCurrentTargetY(this._getElementY(domElement) - this.offset)
this._startAnimation(function() {
this.setCurrentTargetY(this._getElementY(domElement) - this.offset)
if (typeof this.animationStepCallback == 'function') {
this.animationStepCallback(this)
}
}.bind(this))
}
/**
* Stops the current animation
*/
stop () {
this.lastScrollPosition = null
this.scrollDirection = null
this.scrollingInProgress = false
if (this.timer !== null) {
clearInterval(this.timer)
if (typeof finishCallback === 'function') {
this.finishCallback(this)
}
}
}
/**
* Returns the Y-position (relative to page)
* @param {HTMLElement} domElement
* @returns {number}
* @private
*/
_getElementY (domElement) {
const bounds = domElement.getBoundingClientRect()
return (bounds.top + window.pageYOffset)
}
/**
* Starts the Animation
* @param {function} afterAnimationStepCallback
* @private
*/
_startAnimation (afterAnimationStepCallback) {
// stopping running animation (if any)
this.stop()
// sets animation start time
this.animationStartTime = new Date().getTime()
const currentY = window.pageYOffset
this.lastScrollPosition = currentY
this.scrollingInProgress = true
this.scrollDirection = this.getScrollDirection(currentY, this.currentTargetY)
// start timer
this.timer = setInterval(() => {
const currentTime = new Date().getTime()
const startTime = this.animationStartTime
const endTime = this.animationStartTime + this.duration
const scrollToX = window.pageXOffset
let scrollToY = this.currentTargetY
// animation time is up - jump directly to target
if (currentTime > endTime) {
window.scrollTo(scrollToX, scrollToY)
this.stop(this.timer)
return
}
// animation time is in bounds - calculate timing fraction
const fraction = (currentTime - startTime) / this.duration
// correct position fraction with easing (quintEaseOut)
const easingPower = 5
const easingFraction = 1 - Math.abs(Math.pow(fraction-1, easingPower))
// calculate scrollToY by fraction
scrollToY = this._lerp(this.currentStartY, this.currentTargetY, easingFraction)
// scroll to
window.scrollTo(scrollToX, scrollToY)
if (typeof afterAnimationStepCallback === 'function') {
afterAnimationStepCallback(this)
}
}, this.updateInterval);
}
/**
* Calculates the position of the animation by time
* @param {number} startY Start position
* @param {number} targetY Target position
* @param {number} fraction Position of the animation (value between 0-1) 0 is start, 1 is end
* @returns {number}
* @private
*/
_lerp (startY, targetY, fraction) {
const diff = targetY - startY
const pos = diff * fraction
return startY + pos
}
}
module.exports = SmoothScrollTo