-
-
Save gblazex/6477177 to your computer and use it in GitHub Desktop.
IMPORTANT | |
Please duplicate this radar for a Safari fix! | |
This will clean up a 50-line workaround. | |
rdar://22376037 (https://openradar.appspot.com/radar?id=4965070979203072) | |
////////////////////////////////////////////////////////////////////////////// | |
(Now available as a standalone repo.) | |
////////////////////////////////////////////////////////////////////////////// | |
UPDATE: THIS PROJECT MOVED TO A NEW ADDRESS: | |
https://github.com/galambalazs/smoothscroll-for-websites | |
////////////////////////////////////////////////////////////////////////////// | |
UPDATE: THIS PROJECT MOVED TO A NEW ADDRESS: | |
https://github.com/galambalazs/smoothscroll-for-websites | |
////////////////////////////////////////////////////////////////////////////// | |
UPDATE: THIS PROJECT MOVED TO A NEW ADDRESS: | |
https://github.com/galambalazs/smoothscroll-for-websites | |
////////////////////////////////////////////////////////////////////////////// | |
// | |
// I'll leave the old code below for historical purposes. | |
// But this gist will no longer be updated. | |
// For the up-to-date version, use the link above! | |
// | |
// | |
// SmoothScroll for websites v1.3.8 (Balazs Galambosi) | |
// http://www.smoothscroll.net/ | |
// | |
// Licensed under the terms of the MIT license. | |
// | |
// You may use it in your theme if you credit me. | |
// It is also free to use on any individual website. | |
// | |
// Exception: | |
// The only restriction would be not to publish any | |
// extension for browsers or native application | |
// without getting a written permission first. | |
// | |
(function () { | |
// Scroll Variables (tweakable) | |
var defaultOptions = { | |
// Scrolling Core | |
frameRate : 150, // [Hz] | |
animationTime : 400, // [ms] | |
stepSize : 120, // [px] | |
// Pulse (less tweakable) | |
// ratio of "tail" to "acceleration" | |
pulseAlgorithm : true, | |
pulseScale : 4, | |
pulseNormalize : 1, | |
// Acceleration | |
accelerationDelta : 20, // 20 | |
accelerationMax : 1, // 1 | |
// Keyboard Settings | |
keyboardSupport : true, // option | |
arrowScroll : 50, // [px] | |
// Other | |
touchpadSupport : true, | |
fixedBackground : true, | |
excluded : '' | |
}; | |
var options = defaultOptions; | |
// Other Variables | |
var isExcluded = false; | |
var isFrame = false; | |
var direction = { x: 0, y: 0 }; | |
var initDone = false; | |
var root = document.documentElement; | |
var activeElement; | |
var observer; | |
var deltaBuffer = []; | |
var isMac = /^Mac/.test(navigator.platform); | |
var key = { left: 37, up: 38, right: 39, down: 40, spacebar: 32, | |
pageup: 33, pagedown: 34, end: 35, home: 36 }; | |
/*********************************************** | |
* SETTINGS | |
***********************************************/ | |
var options = defaultOptions; | |
/*********************************************** | |
* INITIALIZE | |
***********************************************/ | |
/** | |
* Tests if smooth scrolling is allowed. Shuts down everything if not. | |
*/ | |
function initTest() { | |
if (options.keyboardSupport) { | |
addEvent('keydown', keydown); | |
} | |
} | |
/** | |
* Sets up scrolls array, determines if frames are involved. | |
*/ | |
function init() { | |
if (initDone || !document.body) return; | |
initDone = true; | |
var body = document.body; | |
var html = document.documentElement; | |
var windowHeight = window.innerHeight; | |
var scrollHeight = body.scrollHeight; | |
// check compat mode for root element | |
root = (document.compatMode.indexOf('CSS') >= 0) ? html : body; | |
activeElement = body; | |
initTest(); | |
// Checks if this script is running in a frame | |
if (top != self) { | |
isFrame = true; | |
} | |
/** | |
* This fixes a bug where the areas left and right to | |
* the content does not trigger the onmousewheel event | |
* on some pages. e.g.: html, body { height: 100% } | |
*/ | |
else if (scrollHeight > windowHeight && | |
(body.offsetHeight <= windowHeight || | |
html.offsetHeight <= windowHeight)) { | |
var fullPageElem = document.createElement('div'); | |
fullPageElem.style.cssText = 'position:absolute; z-index:-10000; ' + | |
'top:0; left:0; right:0; height:' + | |
root.scrollHeight + 'px'; | |
document.body.appendChild(fullPageElem); | |
// DOM changed (throttled) to fix height | |
var pendingRefresh; | |
var refresh = function () { | |
if (pendingRefresh) return; // could also be: clearTimeout(pendingRefresh); | |
pendingRefresh = setTimeout(function () { | |
if (isExcluded) return; // could be running after cleanup | |
fullPageElem.style.height = '0'; | |
fullPageElem.style.height = root.scrollHeight + 'px'; | |
pendingRefresh = null; | |
}, 500); // act rarely to stay fast | |
}; | |
setTimeout(refresh, 10); | |
// TODO: attributeFilter? | |
var config = { | |
attributes: true, | |
childList: true, | |
characterData: false | |
// subtree: true | |
}; | |
observer = new MutationObserver(refresh); | |
observer.observe(body, config); | |
if (root.offsetHeight <= windowHeight) { | |
var clearfix = document.createElement('div'); | |
clearfix.style.clear = 'both'; | |
body.appendChild(clearfix); | |
} | |
} | |
// disable fixed background | |
if (!options.fixedBackground && !isExcluded) { | |
body.style.backgroundAttachment = 'scroll'; | |
html.style.backgroundAttachment = 'scroll'; | |
} | |
} | |
/** | |
* Removes event listeners and other traces left on the page. | |
*/ | |
function cleanup() { | |
observer && observer.disconnect(); | |
removeEvent(wheelEvent, wheel); | |
removeEvent('mousedown', mousedown); | |
removeEvent('keydown', keydown); | |
} | |
/************************************************ | |
* SCROLLING | |
************************************************/ | |
var que = []; | |
var pending = false; | |
var lastScroll = Date.now(); | |
/** | |
* Pushes scroll actions to the scrolling queue. | |
*/ | |
function scrollArray(elem, left, top) { | |
directionCheck(left, top); | |
if (options.accelerationMax != 1) { | |
var now = Date.now(); | |
var elapsed = now - lastScroll; | |
if (elapsed < options.accelerationDelta) { | |
var factor = (1 + (50 / elapsed)) / 2; | |
if (factor > 1) { | |
factor = Math.min(factor, options.accelerationMax); | |
left *= factor; | |
top *= factor; | |
} | |
} | |
lastScroll = Date.now(); | |
} | |
// push a scroll command | |
que.push({ | |
x: left, | |
y: top, | |
lastX: (left < 0) ? 0.99 : -0.99, | |
lastY: (top < 0) ? 0.99 : -0.99, | |
start: Date.now() | |
}); | |
// don't act if there's a pending queue | |
if (pending) { | |
return; | |
} | |
var scrollWindow = (elem === document.body); | |
var step = function (time) { | |
var now = Date.now(); | |
var scrollX = 0; | |
var scrollY = 0; | |
for (var i = 0; i < que.length; i++) { | |
var item = que[i]; | |
var elapsed = now - item.start; | |
var finished = (elapsed >= options.animationTime); | |
// scroll position: [0, 1] | |
var position = (finished) ? 1 : elapsed / options.animationTime; | |
// easing [optional] | |
if (options.pulseAlgorithm) { | |
position = pulse(position); | |
} | |
// only need the difference | |
var x = (item.x * position - item.lastX) >> 0; | |
var y = (item.y * position - item.lastY) >> 0; | |
// add this to the total scrolling | |
scrollX += x; | |
scrollY += y; | |
// update last values | |
item.lastX += x; | |
item.lastY += y; | |
// delete and step back if it's over | |
if (finished) { | |
que.splice(i, 1); i--; | |
} | |
} | |
// scroll left and top | |
if (scrollWindow) { | |
window.scrollBy(scrollX, scrollY); | |
} | |
else { | |
if (scrollX) elem.scrollLeft += scrollX; | |
if (scrollY) elem.scrollTop += scrollY; | |
} | |
// clean up if there's nothing left to do | |
if (!left && !top) { | |
que = []; | |
} | |
if (que.length) { | |
requestFrame(step, elem, (1000 / options.frameRate + 1)); | |
} else { | |
pending = false; | |
} | |
}; | |
// start a new queue of actions | |
requestFrame(step, elem, 0); | |
pending = true; | |
} | |
/*********************************************** | |
* EVENTS | |
***********************************************/ | |
/** | |
* Mouse wheel handler. | |
* @param {Object} event | |
*/ | |
function wheel(event) { | |
if (!initDone) { | |
init(); | |
} | |
var target = event.target; | |
var overflowing = overflowingAncestor(target); | |
// use default if there's no overflowing | |
// element or default action is prevented | |
// or it's a zooming event with CTRL | |
if (!overflowing || event.defaultPrevented || event.ctrlKey) { | |
return true; | |
} | |
// leave embedded content alone (flash & pdf) | |
if (isNodeName(activeElement, 'embed') || | |
(isNodeName(target, 'embed') && /\.pdf/i.test(target.src)) || | |
isNodeName(activeElement, 'object')) { | |
return true; | |
} | |
var deltaX = -event.wheelDeltaX || event.deltaX || 0; | |
var deltaY = -event.wheelDeltaY || event.deltaY || 0; | |
if (isMac) { | |
if (event.wheelDeltaX && isDivisible(event.wheelDeltaX, 120)) { | |
deltaX = -120 * (event.wheelDeltaX / Math.abs(event.wheelDeltaX)); | |
} | |
if (event.wheelDeltaY && isDivisible(event.wheelDeltaY, 120)) { | |
deltaY = -120 * (event.wheelDeltaY / Math.abs(event.wheelDeltaY)); | |
} | |
} | |
// use wheelDelta if deltaX/Y is not available | |
if (!deltaX && !deltaY) { | |
deltaY = -event.wheelDelta || 0; | |
} | |
// line based scrolling (Firefox mostly) | |
if (event.deltaMode === 1) { | |
deltaX *= 40; | |
deltaY *= 40; | |
} | |
// check if it's a touchpad scroll that should be ignored | |
if (!options.touchpadSupport && isTouchpad(deltaY)) { | |
return true; | |
} | |
// scale by step size | |
// delta is 120 most of the time | |
// synaptics seems to send 1 sometimes | |
if (Math.abs(deltaX) > 1.2) { | |
deltaX *= options.stepSize / 120; | |
} | |
if (Math.abs(deltaY) > 1.2) { | |
deltaY *= options.stepSize / 120; | |
} | |
scrollArray(overflowing, deltaX, deltaY); | |
event.preventDefault(); | |
scheduleClearCache(); | |
} | |
/** | |
* Keydown event handler. | |
* @param {Object} event | |
*/ | |
function keydown(event) { | |
var target = event.target; | |
var modifier = event.ctrlKey || event.altKey || event.metaKey || | |
(event.shiftKey && event.keyCode !== key.spacebar); | |
// our own tracked active element could've been removed from the DOM | |
if (!document.contains(activeElement)) { | |
activeElement = document.activeElement; | |
} | |
// do nothing if user is editing text | |
// or using a modifier key (except shift) | |
// or in a dropdown | |
// or inside interactive elements | |
var inputNodeNames = /^(textarea|select|embed|object)$/i; | |
var buttonTypes = /^(button|submit|radio|checkbox|file|color|image)$/i; | |
if ( inputNodeNames.test(target.nodeName) || | |
isNodeName(target, 'input') && !buttonTypes.test(target.type) || | |
isNodeName(activeElement, 'video') || | |
isInsideYoutubeVideo(event) || | |
target.isContentEditable || | |
event.defaultPrevented || | |
modifier ) { | |
return true; | |
} | |
// spacebar should trigger button press | |
if ((isNodeName(target, 'button') || | |
isNodeName(target, 'input') && buttonTypes.test(target.type)) && | |
event.keyCode === key.spacebar) { | |
return true; | |
} | |
var shift, x = 0, y = 0; | |
var elem = overflowingAncestor(activeElement); | |
var clientHeight = elem.clientHeight; | |
if (elem == document.body) { | |
clientHeight = window.innerHeight; | |
} | |
switch (event.keyCode) { | |
case key.up: | |
y = -options.arrowScroll; | |
break; | |
case key.down: | |
y = options.arrowScroll; | |
break; | |
case key.spacebar: // (+ shift) | |
shift = event.shiftKey ? 1 : -1; | |
y = -shift * clientHeight * 0.9; | |
break; | |
case key.pageup: | |
y = -clientHeight * 0.9; | |
break; | |
case key.pagedown: | |
y = clientHeight * 0.9; | |
break; | |
case key.home: | |
y = -elem.scrollTop; | |
break; | |
case key.end: | |
var damt = elem.scrollHeight - elem.scrollTop - clientHeight; | |
y = (damt > 0) ? damt+10 : 0; | |
break; | |
case key.left: | |
x = -options.arrowScroll; | |
break; | |
case key.right: | |
x = options.arrowScroll; | |
break; | |
default: | |
return true; // a key we don't care about | |
} | |
scrollArray(elem, x, y); | |
event.preventDefault(); | |
scheduleClearCache(); | |
} | |
/** | |
* Mousedown event only for updating activeElement | |
*/ | |
function mousedown(event) { | |
activeElement = event.target; | |
} | |
/*********************************************** | |
* OVERFLOW | |
***********************************************/ | |
var uniqueID = (function () { | |
var i = 0; | |
return function (el) { | |
return el.uniqueID || (el.uniqueID = i++); | |
}; | |
})(); | |
var cache = {}; // cleared out after a scrolling session | |
var clearCacheTimer; | |
//setInterval(function () { cache = {}; }, 10 * 1000); | |
function scheduleClearCache() { | |
clearTimeout(clearCacheTimer); | |
clearCacheTimer = setInterval(function () { cache = {}; }, 1*1000); | |
} | |
function setCache(elems, overflowing) { | |
for (var i = elems.length; i--;) | |
cache[uniqueID(elems[i])] = overflowing; | |
return overflowing; | |
} | |
// (body) (root) | |
// | hidden | visible | scroll | auto | | |
// hidden | no | no | YES | YES | | |
// visible | no | YES | YES | YES | | |
// scroll | no | YES | YES | YES | | |
// auto | no | YES | YES | YES | | |
function overflowingAncestor(el) { | |
var elems = []; | |
var body = document.body; | |
var rootScrollHeight = root.scrollHeight; | |
do { | |
var cached = cache[uniqueID(el)]; | |
if (cached) { | |
return setCache(elems, cached); | |
} | |
elems.push(el); | |
if (rootScrollHeight === el.scrollHeight) { | |
var topOverflowsNotHidden = overflowNotHidden(root) && overflowNotHidden(body); | |
var isOverflowCSS = topOverflowsNotHidden || overflowAutoOrScroll(root); | |
if (isFrame && isContentOverflowing(root) || | |
!isFrame && isOverflowCSS) { | |
return setCache(elems, getScrollRoot()); | |
} | |
} else if (isContentOverflowing(el) && overflowAutoOrScroll(el)) { | |
return setCache(elems, el); | |
} | |
} while (el = el.parentElement); | |
} | |
function isContentOverflowing(el) { | |
return (el.clientHeight + 10 < el.scrollHeight); | |
} | |
// typically for <body> and <html> | |
function overflowNotHidden(el) { | |
var overflow = getComputedStyle(el, '').getPropertyValue('overflow-y'); | |
return (overflow !== 'hidden'); | |
} | |
// for all other elements | |
function overflowAutoOrScroll(el) { | |
var overflow = getComputedStyle(el, '').getPropertyValue('overflow-y'); | |
return (overflow === 'scroll' || overflow === 'auto'); | |
} | |
/*********************************************** | |
* HELPERS | |
***********************************************/ | |
function addEvent(type, fn) { | |
window.addEventListener(type, fn, false); | |
} | |
function removeEvent(type, fn) { | |
window.removeEventListener(type, fn, false); | |
} | |
function isNodeName(el, tag) { | |
return (el.nodeName||'').toLowerCase() === tag.toLowerCase(); | |
} | |
function directionCheck(x, y) { | |
x = (x > 0) ? 1 : -1; | |
y = (y > 0) ? 1 : -1; | |
if (direction.x !== x || direction.y !== y) { | |
direction.x = x; | |
direction.y = y; | |
que = []; | |
lastScroll = 0; | |
} | |
} | |
var deltaBufferTimer; | |
if (window.localStorage && localStorage.SS_deltaBuffer) { | |
deltaBuffer = localStorage.SS_deltaBuffer.split(','); | |
} | |
function isTouchpad(deltaY) { | |
if (!deltaY) return; | |
if (!deltaBuffer.length) { | |
deltaBuffer = [deltaY, deltaY, deltaY]; | |
} | |
deltaY = Math.abs(deltaY) | |
deltaBuffer.push(deltaY); | |
deltaBuffer.shift(); | |
clearTimeout(deltaBufferTimer); | |
deltaBufferTimer = setTimeout(function () { | |
if (window.localStorage) { | |
localStorage.SS_deltaBuffer = deltaBuffer.join(','); | |
} | |
}, 1000); | |
return !allDeltasDivisableBy(120) && !allDeltasDivisableBy(100); | |
} | |
function isDivisible(n, divisor) { | |
return (Math.floor(n / divisor) == n / divisor); | |
} | |
function allDeltasDivisableBy(divisor) { | |
return (isDivisible(deltaBuffer[0], divisor) && | |
isDivisible(deltaBuffer[1], divisor) && | |
isDivisible(deltaBuffer[2], divisor)); | |
} | |
function isInsideYoutubeVideo(event) { | |
var elem = event.target; | |
var isControl = false; | |
if (document.URL.indexOf ('www.youtube.com/watch') != -1) { | |
do { | |
isControl = (elem.classList && | |
elem.classList.contains('html5-video-controls')); | |
if (isControl) break; | |
} while (elem = elem.parentNode); | |
} | |
return isControl; | |
} | |
var requestFrame = (function () { | |
return (window.requestAnimationFrame || | |
window.webkitRequestAnimationFrame || | |
window.mozRequestAnimationFrame || | |
function (callback, element, delay) { | |
window.setTimeout(callback, delay || (1000/60)); | |
}); | |
})(); | |
var MutationObserver = (window.MutationObserver || | |
window.WebKitMutationObserver || | |
window.MozMutationObserver); | |
var getScrollRoot = (function() { | |
var SCROLL_ROOT; | |
return function() { | |
if (!SCROLL_ROOT) { | |
var dummy = document.createElement('div'); | |
dummy.style.cssText = 'height:10000px;width:1px;'; | |
document.body.appendChild(dummy); | |
var bodyScrollTop = document.body.scrollTop; | |
var docElScrollTop = document.documentElement.scrollTop; | |
window.scrollBy(0, 1); | |
if (document.body.scrollTop != bodyScrollTop) | |
(SCROLL_ROOT = document.body); | |
else | |
(SCROLL_ROOT = document.documentElement); | |
window.scrollBy(0, -1); | |
document.body.removeChild(dummy); | |
} | |
return SCROLL_ROOT; | |
}; | |
})(); | |
/*********************************************** | |
* PULSE (by Michael Herf) | |
***********************************************/ | |
/** | |
* Viscous fluid with a pulse for part and decay for the rest. | |
* - Applies a fixed force over an interval (a damped acceleration), and | |
* - Lets the exponential bleed away the velocity over a longer interval | |
* - Michael Herf, http://stereopsis.com/stopping/ | |
*/ | |
function pulse_(x) { | |
var val, start, expx; | |
// test | |
x = x * options.pulseScale; | |
if (x < 1) { // acceleartion | |
val = x - (1 - Math.exp(-x)); | |
} else { // tail | |
// the previous animation ended here: | |
start = Math.exp(-1); | |
// simple viscous drag | |
x -= 1; | |
expx = 1 - Math.exp(-x); | |
val = start + (expx * (1 - start)); | |
} | |
return val * options.pulseNormalize; | |
} | |
function pulse(x) { | |
if (x >= 1) return 1; | |
if (x <= 0) return 0; | |
if (options.pulseNormalize == 1) { | |
options.pulseNormalize /= pulse_(1); | |
} | |
return pulse_(x); | |
} | |
var wheelEvent; | |
if ('onwheel' in document.createElement('div')) | |
wheelEvent = 'wheel'; | |
else if ('onmousewheel' in document.createElement('div')) | |
wheelEvent = 'mousewheel'; | |
if (wheelEvent) { | |
addEvent(wheelEvent, wheel); | |
addEvent('mousedown', mousedown); | |
addEvent('load', init); | |
} | |
})(); | |
Thank you! Feels very natural and doesn't require any libs. :)
I'm using it only for chrome, all other browsers have smoothscrool enabled by default. It would be nice to add option to enable it only for chrome.
v1.3.8:
- Duplicate declaration on line 44 and 66.
- Unused function "cleanup" on line 164.
- Unterminated statement on line 561.
thanks, helped a lot.
Good news: with the help of the community (800 stars!) we just got the Chrome team to fix a 5 year old bug which will allow me to eliminate a huge block of workaround code: https://gist.github.com/galambalazs/6477177#file-smoothscroll-js-L107-L145
Also thanks to the Chromium team who were so fast to respond.
Thank you so for this,
It helped lot to me ,
will it works for smart phones or small screen devices ?
Hey, this works great! Just one small detail, this
animationTime: 400, // [px]
should be
animationTime: 400, // [ms]
Right? Sorry, SI units are the nº1 pet peeve of engineers! =P
yep, thank you. Probably leftover after a refactor :-)
@praveenkumarkansara This supports touchpads and smartphones in a sense that it detects them and leaves them alone. They provide continuous events so they don't need SmoothScroll to ease out the ticks (like a classic mouse wheel).
Hi,
Great plugin! I have one issue that when applied your plugin into website, I can't scroll by mouse. Here is my js
$('a[href*=#]:not([href=#])').on('click', function() {
if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) {
var target = $(this.hash);
target = target.length ? target : $('[name=' + this.hash.slice(1) +']');
if (target.length) {
$('html,body').animate({
scrollTop: target.offset().top
}, 1000);
return false;
}
}
});
This is the best smooth scroller i've found yet, and I would like to see it available on cdnjs if thats possible?
Also, I'm not 100% savvy with js, how do I take advantage of changing the options?
Hi, great plugin. Here are couple of my request
- Update the version number whenever you update the script.
- Make a repository instead of gist. Cause gists don't give update notification.
- Minified version
- Can you make a jQuery based version? I guess that will be even smaller.
Thanks.
Thanks for sharing! Please create a repository for this! 😄
Please don't use this. I know a lot of work went into this script, but scrolljacking is a horrible web design malpractice and causes inusability on some devices. Just... don't.
+1 Create a repo.
We need this as a Bower component.
Create a repo and i add this to bower as a pull request.
Not scrolling works when you increase or decrease the page ( Ctrl + +/- ) Google Chrome/ Fix please, this creates problems navigating some of my visitors.
@RROrg this code is mostly for Chrome which doesn't support smooth scrolling. It simply matches what other browsers (e.g. Firefox) are doing out of the box. Shouldn't cause usability issues.
But if you have a specific issue to report, please do.
I'm going to create a repo. :)
Awesome effects...
I have doubt. It will be effect to all browser or specified?
@galambalazs i'll fork and add to bower.
Thanks for create the repo.
@webcaetano I already submitted it to bower and will ask the existing repos to be removed so we converge into 1 main repo that gets updates.
@galambalazs Smoothscroll + MacBook Pro Touchpad does not work together, since the Touchpad is already smoothing the input.
@RROrg it's supposed to ignore touchpad.
I just found that the default option here was ON for touchpad for some reason. I'll update the repo, thanks:
PROJECT MOVED TO NEW ADDRESS: https://github.com/galambalazs/smoothscroll-for-websites
please post your issues here: https://github.com/galambalazs/smoothscroll-for-websites/issues
Hey,
Great Job.
Is there a way to do ease in out?
Thanks