diff --git a/client/smoothscroll.js b/client/smoothscroll.js new file mode 100644 index 0000000..a5f34c0 --- /dev/null +++ b/client/smoothscroll.js @@ -0,0 +1,789 @@ +// +// SmoothScroll for websites v1.4.9 (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 : 400, // [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 deltaBufferTimer; + 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 scrollRoot = getScrollRoot(); + var isWindowScroll = (elem === scrollRoot || elem === document.body); + + // if we haven't already fixed the behavior, + // and it needs fixing for this sesh + if (elem.$scrollBehavior == null && isScrollBehaviorSmooth(elem)) { + elem.$scrollBehavior = elem.style.scrollBehavior; + elem.style.scrollBehavior = 'auto'; + } + + 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 (isWindowScroll) { + 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; + // restore default behavior at the end of scrolling sesh + if (elem.$scrollBehavior != null) { + elem.style.scrollBehavior = elem.$scrollBehavior; + elem.$scrollBehavior = null; + } + } + }; + + // 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}); + event = new event.constructor(event.type, event); // redefine event because already dispatched + return parent.dispatchEvent(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: + if (overflowing == document.body && document.scrollingElement) + overflowing = document.scrollingElement; + 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 cacheX = {}; // cleared out after a scrolling session + var cacheY = {}; // cleared out after a scrolling session + var clearCacheTimer; + var smoothBehaviorForElement = {}; + +//setInterval(function () { cache = {}; }, 10 * 1000); + + function scheduleClearCache() { + clearTimeout(clearCacheTimer); + clearCacheTimer = setInterval(function () { + cacheX = cacheY = smoothBehaviorForElement = {}; + }, 1*1000); + } + + function setCache(elems, overflowing, x) { + var cache = x ? cacheX: cacheY; + for (var i = elems.length; i--;) + cache[uniqueID(elems[i])] = overflowing; + return overflowing; + } + + function getCache(el, x) { + return (x ? cacheX: cacheY)[uniqueID(el)]; + } + +// (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 = getCache(el, false); + 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'); + } + +// for all other elements + function isScrollBehaviorSmooth(el) { + var id = uniqueID(el); + if (smoothBehaviorForElement[id] == null) { + var scrollBehavior = getComputedStyle(el, '')['scroll-behavior']; + smoothBehaviorForElement[id] = ('smooth' == scrollBehavior); + } + return smoothBehaviorForElement[id]; + } + + + /*********************************************** + * HELPERS + ***********************************************/ + + function addEvent(type, fn, arg) { + window.addEventListener(type, fn, arg || false); + } + + function removeEvent(type, fn, arg) { + window.removeEventListener(type, fn, arg || false); + } + + function isNodeName(el, tag) { + return el && (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; + } + } + + 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); + var dpiScaledWheelDelta = deltaY > 120 && allDeltasDivisableBy(deltaY); // win64 + return !allDeltasDivisableBy(120) && !allDeltasDivisableBy(100) && !dpiScaledWheelDelta; + } + + 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 = document.scrollingElement; + 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 supportsPassive = false; + try { + window.addEventListener("test", null, Object.defineProperty({}, 'passive', { + get: function () { + supportsPassive = true; + } + })); + } catch(e) {} + + var wheelOpt = supportsPassive ? { passive: false }: false; + var wheelEvent = 'onwheel' in document.createElement('div') ? 'wheel': 'mousewheel'; + + if (wheelEvent && isEnabledForBrowser) { + addEvent(wheelEvent, wheel, wheelOpt); + 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; + +})();