Team:Tokyo Tech/mobirise/smoothscroll/smooth-scroll-js

// // SmoothScroll for websites v1.4.5 (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
   touchpadSupport   : false, // ignore touchpad by default
   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});
           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 (!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.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 <body> 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; })();