// // smoothscroll for websites v1.4.6 (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 is to not 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 : 550, // [ms] stepsize : 100, // [px] // pulse (less tweakable) // ratio of "tail" to "acceleration" pulsealgorithm : true, pulsescale : 4, pulsenormalize : 1, // acceleration accelerationdelta : 50, // 50 accelerationmax : 3, // 3 // keyboard settings keyboardsupport : true, // option arrowscroll : 50, // [px] // other 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 refreshsize; 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 }; var arrowkeys = { 37: 1, 38: 1, 39: 1, 40: 1 }; /*********************************************** * 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; } /** * safari 10 fixed it, chrome fixed it in v45: * 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 (isoldsafari && 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; refreshsize = 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(refreshsize, 10); addevent('resize', refreshsize); // todo: attributefilter? var config = { attributes: true, childlist: true, characterdata: false // subtree: true }; observer = new mutationobserver(refreshsize); 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); removeevent('resize', refreshsize); removeevent('load', init); } /************************************************ * 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; // leave early if default action is prevented // or it's a zooming event with ctrl if (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') || target.shadowroot) { 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; } var overflowing = overflowingancestor(target); // nothing to do if there's no element that's scrollable if (!overflowing) { // except chrome iframes seem to eat wheel events, which we need to // propagate up, if the iframe has nothing overflowing to scroll if (isframe && ischrome) { // change target to iframe element itself for the parent frame object.defineproperty(event, "target", {value: window.frameelement}); return parent.wheel(event); } return true; } // check if it's a touchpad scroll that should be ignored if (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.body.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 ( event.defaultprevented || inputnodenames.test(target.nodename) || isnodename(target, 'input') && !buttontypes.test(target.type) || isnodename(activeelement, 'video') || isinsideyoutubevideo(event) || target.iscontenteditable || modifier ) { return true; } // [spacebar] should trigger button press, leave it alone if ((isnodename(target, 'button') || isnodename(target, 'input') && buttontypes.test(target.type)) && event.keycode === key.spacebar) { return true; } // [arrwow keys] on radio buttons should be left alone if (isnodename(target, 'input') && target.type == 'radio' && arrowkeys[event.keycode]) { return true; } var shift, x = 0, y = 0; var overflowing = overflowingancestor(activeelement); if (!overflowing) { // chrome iframes seem to eat key events, which we need to // propagate up, if the iframe has nothing overflowing to scroll return (isframe && ischrome) ? parent.keydown(event) : true; } var clientheight = overflowing.clientheight; if (overflowing == 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 = -overflowing.scrolltop; break; case key.end: var scroll = overflowing.scrollheight - overflowing.scrolltop; var scrollremaining = scroll - clientheight; y = (scrollremaining > 0) ? scrollremaining + 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(overflowing, 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 and 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) { try { // #46 safari throws in private browsing for localstorage deltabuffer = localstorage.ss_deltabuffer.split(','); } catch (e) { } } 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 () { try { // #46 safari throws in private browsing for localstorage localstorage.ss_deltabuffer = deltabuffer.join(','); } catch (e) { } }, 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, 3); if (document.body.scrolltop != bodyscrolltop) (scroll_root = document.body); else (scroll_root = document.documentelement); window.scrollby(0, -3); 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); } /*********************************************** * first run ***********************************************/ var useragent = window.navigator.useragent; var isedge = /edge/.test(useragent); // thank you ms var ischrome = /chrome/i.test(useragent) && !isedge; var issafari = /safari/i.test(useragent) && !isedge; var ismobile = /mobile/i.test(useragent); var isiewin7 = /windows nt 6.1/i.test(useragent) && /rv:11/i.test(useragent); var isoldsafari = issafari && (/version\/8/i.test(useragent) || /version\/9/i.test(useragent)); var isenabledforbrowser = (ischrome || issafari || isiewin7) && !ismobile; var wheelevent; if ('onwheel' in document.createelement('div')) wheelevent = 'wheel'; else if ('onmousewheel' in document.createelement('div')) wheelevent = 'mousewheel'; if (wheelevent && isenabledforbrowser) { addevent(wheelevent, wheel); addevent('mousedown', mousedown); addevent('load', init); } /*********************************************** * public interface ***********************************************/ function smoothscroll(optionstoset) { for (var key in optionstoset) if (defaultoptions.hasownproperty(key)) options[key] = optionstoset[key]; } smoothscroll.destroy = cleanup; if (window.smoothscrolloptions) // async api smoothscroll(window.smoothscrolloptions); if (typeof define === 'function' && define.amd) define(function() { return smoothscroll; }); else if ('object' == typeof exports) module.exports = smoothscroll; else window.smoothscroll = smoothscroll; })();