UNPKG

css-scroll-snap-polyfill

Version:

Polyfill for CSS scroll snapping draft.

710 lines (616 loc) 32.7 kB
/*! * Polyfill.js - v0.1.0 * * Copyright (c) 2015 Philip Walton <http://philipwalton.com> * Released under the MIT license * * Date: 2015-06-21 */ !function(a,b,c){"use strict";function d(a){return a.replace(/^\s+|\s+$/g,"")}function e(a,b){var c,d=0;if(!a||!b)return!1;for(;c=b[d++];)if(a===c)return!0;return!1}function f(a){return j.test(a)}function g(a){var b,c=0;for(this._rules=[];b=a[c++];)this._rules.push(new h(b));}function h(a){this._rule=a;}function i(a){return this instanceof i?(this._options=a, a.keywords||(this._options={keywords:a}), this._promise=[], this._getStylesheets(), this._downloadStylesheets(), this._parseStylesheets(), this._filterCSSByKeywords(), this._buildMediaQueryMap(), this._reportInitialMatches(), void this._addMediaListeners()):new i(a)}var j=RegExp("^"+String({}.valueOf).replace(/[.*+?\^${}()|\[\]\\]/g,"\\$&").replace(/valueOf|for [^\]]+/g,".+?")+"$"),k=function(){var a=b.getElementsByTagName("base")[0],c=/^([a-zA-Z:]*\/\/)/;return function(b){var d=!c.test(b)&&!a||b.replace(RegExp.$1,"").split("/")[0]===location.host;return d}}(),l={matchMedia:a.matchMedia&&a.matchMedia("only all").matches,nativeMatchMedia:f(a.matchMedia)},m=function(){function b(a){for(var b,c=0;b=a[c++];)i[b]||e(b,j)||j.push(b);}function c(){if(0===m.readyState||4===m.readyState){var a;(a=j[0])&&d(a), a||g();}}function d(a){l++, m.open("GET",a,!0), m.onreadystatechange=function(){4!=m.readyState||200!=m.status&&304!=m.status||(i[a]=m.responseText,j.shift(),c())}, m.send(null);}function f(a){for(var b,c=0,d=0;b=a[c++];)i[b]&&d++;return d===a.length}function g(){for(var a;a=k.shift();)h(a.urls,a.fn);}function h(a,b){for(var c,d=[],e=0;c=a[e++];)d.push(i[c]);b.call(null,d);}var i={},j=[],k=[],l=0,m=function(){var b;try{b=new a.XMLHttpRequest;}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP");}return b}();return{request:function(a,d){k.push({urls:a,fn:d}), f(a)?g():(b(a),c());},clearCache:function(){i={};},_getRequestCount:function(){return l}}}(),n={_cache:{},clearCache:function(){n._cache={};},parse:function(a,b){function c(){return g(/^\{\s*/)}function e(){return g(/^\}\s*/)}function f(){var b,c=[];for(h(), i(c);"}"!=a.charAt(0)&&(b=y()||z());)c.push(b), i(c);return c}function g(b){var c=b.exec(a);if(c)return a=a.slice(c[0].length), c}function h(){g(/^\s*/);}function i(a){a=a||[];for(var b;b=j();)a.push(b);return a}function j(){if("/"==a[0]&&"*"==a[1]){for(var b=2;"*"!=a[b]||"/"!=a[b+1];)++b;b+=2;var c=a.slice(2,b-2);return a=a.slice(b), h(), {comment:c}}}function k(){var a=g(/^([^{]+)/);if(a)return d(a[0]).split(/\s*,\s*/)}function l(){var a=g(/^(\*?[\-\w]+)\s*/);if(a&&(a=a[0], g(/^:\s*/))){var b=g(/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^\)]*?\)|[^};])+)\s*/);if(b)return b=d(b[0]), g(/^[;\s]*/), {property:a,value:b}}}function m(){for(var a,b=[];a=g(/^(from|to|\d+%|\.\d+%|\d+\.\d+%)\s*/);)b.push(a[1]), g(/^,\s*/);return b.length?{values:b,declarations:x()}:void 0}function o(){var a=g(/^@([\-\w]+)?keyframes */);if(a){var b=a[1],a=g(/^([\-\w]+)\s*/);if(a){var d=a[1];if(c()){i();for(var f,h=[];f=m();)h.push(f), i();if(e()){var j={name:d,keyframes:h};return b&&(j.vendor=b), j}}}}}function p(){var a=g(/^@supports *([^{]+)/);if(a){var b=d(a[1]);if(c()){i();var h=f();if(e())return{supports:b,rules:h}}}}function q(){var a=g(/^@media *([^{]+)/);if(a){var b=d(a[1]);if(c()){i();var h=f();if(e())return{media:b,rules:h}}}}function r(){var a=g(/^@page */);if(a){var b=k()||[],d=[];if(c()){i();for(var f;f=l()||s();)d.push(f), i();if(e())return{type:"page",selectors:b,declarations:d}}}}function s(){var a=g(/^@([a-z\-]+) */);if(a){var b=a[1];return{type:b,declarations:x()}}}function t(){return w("import")}function u(){return w("charset")}function v(){return w("namespace")}function w(a){var b=g(new RegExp("^@"+a+" *([^;\\n]+);\\s*"));if(b){var c={};return c[a]=d(b[1]), c}}function x(){var a=[];if(c()){i();for(var b;b=l();)a.push(b), i();if(e())return a}}function y(){return o()||q()||p()||t()||u()||v()||r()}function z(){var a=k();if(a)return i(), {selectors:a,declarations:x()}}return b&&n._cache[b]?n._cache[b]:(a=a.replace(/\/\*[\s\S]*?\*\//g,""), n._cache[b]=f())},filter:function(a,b){function c(a,b){return a||b?a?a.concat(b):[b]:void 0}function e(a){null==a.media&&delete a.media, null==a.supports&&delete a.supports, k.push(a);}function f(a,b){if(b)for(var c=b.length;c--;)if(a.indexOf(b[c])>=0)return!0}function g(a,b){for(var c,e,f,g,h=/\*/,i=0;c=b[i++];)if(e=c.split(":"), f=new RegExp("^"+d(e[0]).replace(h,".*")+"$"), g=new RegExp("^"+d(e[1]).replace(h,".*")+"$"), f.test(a.property)&&g.test(a.value))return!0}function h(a,c,d){return b.selectors&&f(a.selectors.join(","),b.selectors)?(e({media:c,supports:d,selectors:a.selectors,declarations:a.declarations}), !0):void 0}function i(a,c,d){if(b.declarations)for(var f,h=0;f=a.declarations[h++];)if(g(f,b.declarations))return e({media:c,supports:d,selectors:a.selectors,declarations:a.declarations}), !0}function j(a,b,d){for(var e,f=0;e=a[f++];)e.declarations?h(e,b,d)||i(e,b,d):e.rules&&e.media?j(e.rules,c(b,e.media),d):e.rules&&e.supports&&j(e.rules,b,c(d,e.supports));}var k=[];return j(a), k}},o=function(){function c(){if(f)return f;var a=b.documentElement,c=b.body,d=a.style.fontSize,e=c.style.fontSize,g=b.createElement("div");return a.style.fontSize="1em", c.style.fontSize="1em", c.appendChild(g), g.style.width="1em", g.style.position="absolute", f=g.offsetWidth, c.removeChild(g), c.style.fontSize=e, a.style.fontSize=d, f}function d(b){return a.matchMedia(b)}function e(a){var d,e,f=!1;return g=b.documentElement.clientWidth, h.test(a)&&(d="em"===RegExp.$2?parseFloat(RegExp.$1)*c():parseFloat(RegExp.$1)), i.test(a)&&(e="em"===RegExp.$2?parseFloat(RegExp.$1)*c():parseFloat(RegExp.$1)), d&&e?f=g>=d&&e>=g:(d&&g>=d&&(f=!0),e&&e>=g&&(f=!0)), {matches:f,media:a}}var f,g,h=/\(min\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,i=/\(max\-width:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,j={};return{matchMedia:function(a){return l.matchMedia?d(a):e(a)},clearCache:function(){l.nativeMatchMedia||(g=null, j={});}}}(),p=function(){function b(a,b){var c;return function(){clearTimeout(c), c=setTimeout(a,b);}}var c=function(){var a=[];return{add:function(b,c,d){for(var e,f=0;e=a[f++];)if(e.polyfill==b&&e.mql===c&&e.fn===d)return!1;c.addListener(d), a.push({polyfill:b,mql:c,fn:d});},remove:function(b){for(var c,d=0;c=a[d++];)c.polyfill===b&&(c.mql.removeListener(c.fn), a.splice(--d,1));}}}(),d=function(b){function c(){for(var a,c=0;a=b[c++];)a.fn();}return{add:function(d,e){b.length||(a.addEventListener?a.addEventListener("resize",c,!1):a.attachEvent("onresize",c)), b.push({polyfill:d,fn:e});},remove:function(d){for(var e,f=0;e=b[f++];)e.polyfill===d&&b.splice(--f,1);b.length||(a.removeEventListener?a.removeEventListener("resize",c,!1):a.detachEvent&&a.detachEvent("onresize",c));}}}([]);return{removeListeners:function(a){l.nativeMatchMedia?c.remove(a):d.remove(a);},addListeners:function(a,e){function f(){if(l.nativeMatchMedia)for(var f in h)h.hasOwnProperty(f)&&!function(b,d){c.add(a,b,function(){e.call(a,d,b.matches);});}(h[f],f);else{var i=b(function(a,b){return function(){g(a,b);}}(a,h),a._options.debounceTimeout||100);d.add(a,i);}}function g(a,b){var c,d={};o.clearCache();for(c in b)b.hasOwnProperty(c)&&(d[c]=o.matchMedia(c).matches, d[c]!=i[c]&&e.call(a,c,o.matchMedia(c).matches));i=d;}var h=a._mediaQueryMap,i={};!function(){for(var a in h)h.hasOwnProperty(a)&&(i[a]=o.matchMedia(a).matches)}(), f();}}}();g.prototype.each=function(a,b){var c,d=0;for(b||(b=this);c=this._rules[d++];)a.call(b,c)}, g.prototype.size=function(){return this._rules.length}, g.prototype.at=function(a){return this._rules[a]}, h.prototype.getDeclaration=function(){for(var a,b={},c=0,d=this._rule.declarations;a=d[c++];)b[a.property]=a.value;return b}, h.prototype.getSelectors=function(){return this._rule.selectors.join(", ")}, h.prototype.getMedia=function(){return this._rule.media.join(" and ")}, i.prototype.doMatched=function(a){return this._doMatched=a,this._resolve(),this}, i.prototype.undoUnmatched=function(a){return this._undoUnmatched=a,this._resolve(),this}, i.prototype.getCurrentMatches=function(){for(var a,b,c=0,d=[];a=this._filteredRules[c++];)b=a.media&&a.media.join(" and "),(!b||o.matchMedia(b).matches)&&d.push(a);return new g(d)}, i.prototype.destroy=function(){this._undoUnmatched&&(this._undoUnmatched(this.getCurrentMatches()),p.removeListeners(this))}, i.prototype._defer=function(a,b){a.call(this)?b.call(this):this._promise.push({condition:a,callback:b})}, i.prototype._resolve=function(){for(var a,b=0;a=this._promise[b];)a.condition.call(this)?(this._promise.splice(b,1),a.callback.call(this)):b++}, i.prototype._getStylesheets=function(){var a,c,d,f,g,h,i,j=0,l=[];if(this._options.include){for(c=this._options.include;a=c[j++];)if(d=b.getElementById(a)){if("STYLE"===d.nodeName){i={text:d.textContent},l.push(i);continue}if(d.media&&"print"==d.media)continue;if(!k(d.href))continue;i={href:d.href},d.media&&(i.media=d.media),l.push(i)}}else{for(c=this._options.exclude,f=b.getElementsByTagName("link");d=f[j++];)d.rel&&"stylesheet"==d.rel&&"print"!=d.media&&k(d.href)&&!e(d.id,c)&&(i={href:d.href},d.media&&(i.media=d.media),l.push(i));for(h=b.getElementsByTagName("style"),j=0;g=h[j++];)i={text:g.textContent},l.push(i)}return this._stylesheets=l}, i.prototype._downloadStylesheets=function(){for(var a,b=this,c=[],d=0;a=this._stylesheets[d++];)c.push(a.href);m.request(c,function(a){for(var c,d=0;c=a[d];)b._stylesheets[d++].text=c;b._resolve()})}, i.prototype._parseStylesheets=function(){this._defer(function(){return this._stylesheets&&this._stylesheets.length&&this._stylesheets[0].text},function(){for(var a,b=0;a=this._stylesheets[b++];)a.rules=n.parse(a.text,a.url)})}, i.prototype._filterCSSByKeywords=function(){this._defer(function(){return this._stylesheets&&this._stylesheets.length&&this._stylesheets[0].rules},function(){for(var a,b,c=[],d=0;a=this._stylesheets[d++];)b=a.media,b&&"all"!=b&&"screen"!=b?c.push({rules:a.rules,media:a.media}):c=c.concat(a.rules);this._filteredRules=n.filter(c,this._options.keywords)})}, i.prototype._buildMediaQueryMap=function(){this._defer(function(){return this._filteredRules},function(){var a,b,c=0;for(this._mediaQueryMap={};b=this._filteredRules[c++];)b.media&&(a=b.media.join(" and "),this._mediaQueryMap[a]=o.matchMedia(a))})}, i.prototype._reportInitialMatches=function(){this._defer(function(){return this._filteredRules&&this._doMatched},function(){this._doMatched(this.getCurrentMatches())})}, i.prototype._addMediaListeners=function(){this._defer(function(){return this._filteredRules&&this._doMatched&&this._undoUnmatched},function(){p.addListeners(this,function(a,b){for(var c,d=0,e=[],f=[];c=this._filteredRules[d++];)c.media&&c.media.join(" and ")==a&&(b?e:f).push(c);e.length&&this._doMatched(new g(e)),f.length&&this._undoUnmatched(new g(f))})})}, i.modules={DownloadManager:m,StyleManager:n,MediaManager:o,EventManager:p}, i.constructors={Ruleset:g,Rule:h}, a.Polyfill=i;}(window,document); const NONE = 'none'; const START = 'start'; const END = 'end'; const CENTER = 'center'; const LENGTH_PERCENTAGE_REGEX = /(\d+)(px|vh|vw|%)/g; /** * constraint to jumping to the next snap-point. * when scrolling further than SNAP_CONSTRAINT snap-points, * but the current distance is less than 1-0.18 (read: 18 percent), * the snap-will go back to the closer snap-point. */ const CONSTRAINT_DECIMAL = 0.18; /** * time in ms after which scrolling is considered finished. * the scroll timeouts are timed with this. * whenever a new scroll event is triggered, the previous timeout is deleted. * @type {Number} */ const SCROLL_TIMEOUT = 45; /** * time for the smooth scrolling * @type {Number} */ const SCROLL_TIME = 350; /** * doMatched is a callback for Polyfill to fill in the desired behaviour. * @param {array} rules rules found for the polyfill */ function doMatched(rules) { // iterate over rules rules.each((rule) => { const elements = document.querySelectorAll(rule.getSelectors()); const declaration = rule.getDeclaration(); // iterate over elements [].forEach.call(elements, (el) => { // set up the behaviour setUpElement(el, declaration); }); }); } /** * unDomatched is a callback for polyfill to undo any polyfilled behaviour * @param {Object} rules */ function undoUnmatched(rules) { // iterate over rules rules.each((rule) => { const elements = document.querySelectorAll(rule.getSelectors()); // iterate over elements [].forEach.call(elements, (el) => { // tear down the behaviour tearDownElement(el); }); }); } /** * set up an element for scroll-snap behaviour * @param {Object} el HTML element * @param {Object} declaration CSS declarations */ function setUpElement(el, declaration) { // if this is a scroll-snap element in a scroll snap container, attach to the container only. if (typeof declaration['scroll-snap-align'] !== 'undefined') { // save declaration el.scrollSnapAlignment = parseScrollSnapAlignment(declaration); return attachToScrollParent(el) } // if the scroll snap attributes are applied on the body/html tag, use the doc for scroll events. const tag = el.tagName; if (tag.toLowerCase() == "body" || tag.toLowerCase() == "html") { el = document; } // add the event listener el.addEventListener('scroll', handler, false); // set up scroll padding el.scrollPadding = parseScrollPadding(declaration); // save declaration // if (typeof declaration['scroll-snap-destination'] !== 'undefined') { // el.snapLengthUnit = parseSnapCoordValue(declaration); // } else { // el.snapLengthUnit = parseSnapPointValue(declaration); // } // init possible elements el.snapElements = []; } /** * tear down an element. remove all added behaviour. * @param {Object} el DomElement */ function tearDownElement(el) { // if the scroll snap attributes are applied on the body/html tag, use the doc for scroll events. const tag = el.tagName; if (tag.toLowerCase() == "body" || tag.toLowerCase() == "html") { el = document; } document.removeEventListener('scroll', handler, false); el.removeEventListener('scroll', handler, false); el.snapLengthUnit = null; el.snapElements = []; } /** * parse snap alignment values. * @param {Object} declaration * @return {Object} */ function parseScrollSnapAlignment(declaration) { const { 'scroll-snap-align': snapAlign } = declaration; let xAlign = NONE; let yAlign = NONE; if (typeof snapAlign !== 'undefined') { // calculate scroll snap align const parts = snapAlign.split(' '); xAlign = parts[0]; yAlign = parts.length > 1 ? parts[1] : xAlign; } return { x: xAlign, y: yAlign } } function parseLengthPercentage(strValue) { // regex to parse lengths const result = LENGTH_PERCENTAGE_REGEX.exec(strValue); // if result is null return default values if (result === null) return { value: 0, unit: 'px' } const [_, value, unit] = result; return { value: parseInt(value, 10), unit } } /** * parse scroll padding values. * @param {Object} declaration * @return {Object} */ function parseScrollPadding(declaration) { const { 'scroll-padding': scrollPadding, 'scroll-padding-top': scrollPaddingTop, 'scroll-padding-right': scrollPaddingRight, 'scroll-padding-bottom': scrollPaddingBottom, 'scroll-padding-left': scrollPaddingLeft } = declaration; let paddingTop = { value: 0, unit: 'px' }; let paddingRight = { value: 0, unit: 'px' }; let paddingBottom = { value: 0, unit: 'px' }; let paddingLeft = { value: 0, unit: 'px' }; if (typeof scrollPadding !== 'undefined') { // regex to parse lengths const parts = scrollPadding.split(' '); parts.forEach((part, i) => { const value = parseLengthPercentage(part); switch (i) { case 0: paddingTop = value; paddingRight = value; paddingBottom = value; paddingLeft = value; break; case 1: paddingRight = value; paddingLeft = value; break; case 2: paddingBottom = value; break; case 3: paddingLeft = value; break; default: } }); } if (typeof scrollPaddingTop !== 'undefined') { paddingTop = parseLengthPercentage(scrollPaddingTop); } if (typeof scrollPaddingRight !== 'undefined') { paddingRight = parseLengthPercentage(scrollPaddingRight); } if (typeof scrollPaddingBottom !== 'undefined') { paddingBottom = parseLengthPercentage(scrollPaddingBottom); } if (typeof scrollPaddingLeft !== 'undefined') { paddingLeft = parseLengthPercentage(scrollPaddingLeft); } return { top: paddingTop, right: paddingRight, bottom: paddingBottom, left: paddingLeft, } } /** * attach a child-element onto a scroll-container * @param {Object} el */ function attachToScrollParent(el) { var attach = el; // iterate over parent elements for ( ; el && el !== document; el = el.parentNode ) { if (typeof el.snapElements !== 'undefined') { el.snapElements.push(attach); } } } /** * the last created timeOutId for scroll event timeouts. * @type int */ let timeOutId = null; /** * starting point for current scroll * @type length */ let scrollStart = null; /** * the last object receiving a scroll event */ let lastObj; let lastScrollObj; /** * scroll handler * this is the callback for scroll events. */ let handler = function(evt) { // use evt.target as target-element lastObj = evt.target; lastScrollObj = getScrollObj(lastObj); // if currently animating, stop it. this prevents flickering. if (animationFrame) { // cross browser if (!cancelAnimationFrame(animationFrame)) { clearTimeout(animationFrame); } } // if a previous timeout exists, clear it. if (timeOutId) { // we only want to call a timeout once after scrolling.. clearTimeout(timeOutId); } else { // save new scroll start scrollStart = { y: lastScrollObj.scrollTop, x: lastScrollObj.scrollLeft }; } /* set a timeout for every scroll event. * if we have new scroll events in that time, the previous timeouts are cleared. * thus we can be sure that the timeout will be called 50ms after the last scroll event. * this means a huge improvement in speed, as we just assign a timeout in the scroll event, which will be called only once (after scrolling is finished) */ timeOutId = setTimeout(handlerDelayed, SCROLL_TIMEOUT); }; /** * a delayed handler for scrolling. * this will be called by setTimeout once, after scrolling is finished. */ let handlerDelayed = function() { // if we don't move a thing, we can ignore the timeout: if we did, there'd be another timeout added for scrollStart+1. if (scrollStart.y == lastScrollObj.scrollTop && scrollStart.x == lastScrollObj.scrollLeft) { // ignore timeout return; } // detect direction of scroll. negative is up, positive is down. let direction = { y: (scrollStart.y - lastScrollObj.scrollTop > 0) ? -1 : 1, x: (scrollStart.x - lastScrollObj.scrollLeft > 0) ? -1 : 1 }; let snapPoint; if (typeof lastScrollObj.snapElements !== 'undefined' && lastScrollObj.snapElements.length > 0) { snapPoint = getNextElementSnapPoint(lastScrollObj, lastObj, direction); } // before doing the move, unbind the event handler (otherwise it calls itself kinda) lastObj.removeEventListener('scroll', handler, false); // smoothly move to the snap point smoothScroll(lastScrollObj, snapPoint, function() { // after moving to the snap point, rebind the scroll event handler lastObj.addEventListener('scroll', handler, false); }); // we just jumped to the snapPoint, so this will be our next scrollStart if (!isNaN(snapPoint.x) || !isNaN(snapPoint.y)) { scrollStart = snapPoint; } }; var currentIteratedObj = null; var currentIteration = 0; function toPx(value, unit, containerEl) { if (unit && unit.toLowerCase() === 'vw') { return getWidth(document.documentElement) * (value / 100); } if (unit && unit.toLowerCase() === 'vh') { return getHeight(document.documentElement) * (value / 100); } if (unit && unit === '%') { return getWidth(containerEl) * (value / 100); } return value; } function getNextElementSnapPoint(scrollObj, obj, direction) { var l = obj.snapElements.length, top = scrollObj.scrollTop, left = scrollObj.scrollLeft, // decide upon an iteration direction (favor -1, as 1 is default and will be applied when there is no direction on an axis) primaryDirection = Math.min(direction.y, direction.x), snapCoords = {y: 0, x: 0}; const { top: paddingTop, right: paddingRight, bottom: paddingBottom, left: paddingLeft } = scrollObj.scrollPadding; const pTop = roundByDirection(direction, toPx(paddingTop.value, paddingTop.unit, scrollObj)); const pRight = roundByDirection(direction, toPx(paddingRight.value, paddingRight.unit, scrollObj)); const pBottom = roundByDirection(direction, toPx(paddingBottom.value, paddingBottom.unit, scrollObj)); const pLeft = roundByDirection(direction, toPx(paddingLeft.value, paddingLeft.unit, scrollObj)); function adjustForPadding(value, adjustment) { if (currentIteration === 0 || currentIteration === l - 1) { return value; } return value - adjustment; } // handle use-case where scrolling to end if ((left > 0 && (left + getWidth(scrollObj)) === getScrollWidth(scrollObj)) || (top > 0 && (top + getHeight(scrollObj)) === getScrollHeight(scrollObj))) { currentIteration = l-1; const lastSnapElement = obj.snapElements[currentIteration]; const lastSnapCoords = { x: (getLeft(lastSnapElement) - getLeft(scrollObj)) + getXSnapLength(lastSnapElement, lastSnapElement.scrollSnapAlignment.x, direction), y: (getTop(lastSnapElement) - getTop(scrollObj)) + getYSnapLength(lastSnapElement, lastSnapElement.scrollSnapAlignment.y, direction) }; lastSnapElement.snapCoords = lastSnapCoords; // the for loop stopped at the last element return {y: stayInBounds(0, getScrollHeight(scrollObj), lastSnapCoords.y), x: stayInBounds(0, getScrollWidth(scrollObj), lastSnapCoords.x)}; } const currentSnapElement = obj.snapElements[currentIteration]; const currentSnapCoords = { x: currentIteration === 0 ? 0 : (getLeft(currentSnapElement) - getLeft(scrollObj)) + getXSnapLength(currentSnapElement, currentSnapElement.scrollSnapAlignment.x, direction) - getXSnapLength(scrollObj, currentSnapElement.scrollSnapAlignment.x, direction), y: currentIteration === 0 ? 0 : (getTop(currentSnapElement) - getTop(scrollObj)) + getYSnapLength(currentSnapElement, currentSnapElement.scrollSnapAlignment.y, direction) - getYSnapLength(scrollObj, currentSnapElement.scrollSnapAlignment.y, direction) }; currentSnapElement.snapCoords = currentSnapCoords; const xThreshold = currentSnapCoords.x + (direction.x * getWidth(currentSnapElement) * CONSTRAINT_DECIMAL); const yThreshold = currentSnapCoords.y + (direction.y * getHeight(currentSnapElement) * CONSTRAINT_DECIMAL); for(var i = currentIteration + primaryDirection; i<l && i >= 0; i = i+primaryDirection) { currentIteratedObj = obj.snapElements[i]; // get objects snap coords by adding obj.top + obj.snaplength.y snapCoords = { y: i === 0 ? 0 : (getTop(currentIteratedObj) - getTop(scrollObj)) + getYSnapLength(currentIteratedObj, currentIteratedObj.scrollSnapAlignment.y, direction) - getYSnapLength(scrollObj, currentIteratedObj.scrollSnapAlignment.y, direction), x: i === 0 ? 0 : (getLeft(currentIteratedObj) - getLeft(scrollObj)) + getXSnapLength(currentIteratedObj, currentIteratedObj.scrollSnapAlignment.x, direction) - getXSnapLength(scrollObj, currentIteratedObj.scrollSnapAlignment.x, direction) }; currentIteratedObj.snapCoords = snapCoords; // check if object snappoint is "close" enough to scrollable snappoint // check if not beyond scroll threshold if ((direction.x === 1 ? left < xThreshold : left > xThreshold) && (direction.y === 1 ? top < yThreshold : top > yThreshold)) { break; } const elementXThreshold = snapCoords.x + (direction.x * getWidth(currentIteratedObj) * CONSTRAINT_DECIMAL); const elementYThreshold = snapCoords.y + (direction.y * getHeight(currentIteratedObj) * CONSTRAINT_DECIMAL); // check if not scrolled past element snap point if ((direction.x === 1 ? left > elementXThreshold : left < elementXThreshold) || (direction.y === 1 ? top > elementYThreshold : top < elementYThreshold)) { continue; } // ok, we found a snap point. currentIteration = i; // stay in bounds (minimum: 0, maxmimum: absolute height) return {y: stayInBounds(0, getScrollHeight(scrollObj), adjustForPadding(snapCoords.y, pTop)), x: stayInBounds(0, getScrollWidth(scrollObj), adjustForPadding(snapCoords.x, pLeft))}; } // no snap found, use first or last? if (primaryDirection == 1 && i === l-1) { currentIteration = l-1; // the for loop stopped at the last element return {y: stayInBounds(0, getScrollHeight(scrollObj), snapCoords.y), x: stayInBounds(0, getScrollWidth(scrollObj), snapCoords.x)}; } else if (primaryDirection == -1 && i === 0) { currentIteration = 0; // the for loop stopped at the first element return {y: stayInBounds(0, getScrollHeight(scrollObj), snapCoords.y), x: stayInBounds(0, getScrollWidth(scrollObj), snapCoords.x)}; } // stay in the same place return {y: stayInBounds(0, getScrollHeight(scrollObj), adjustForPadding(obj.snapElements[currentIteration].snapCoords.y, pTop)), x: stayInBounds(0, getScrollWidth(scrollObj), adjustForPadding(obj.snapElements[currentIteration].snapCoords.x, pLeft))}; } /** * ceil or floor a number based on direction * @param {Number} direction * @param {Number} currentPoint * @return {Number} */ function roundByDirection(direction, currentPoint) { if (direction === -1) { // when we go up, we floor the number to jump to the next snap-point in scroll direction return Math.floor(currentPoint); } // go down, we ceil the number to jump to the next in view. return Math.ceil(currentPoint); } /** * keep scrolling in bounds * @param {Number} min * @param {Number} max * @param {Number} destined * @return {Number} */ function stayInBounds(min, max, destined) { return Math.max(Math.min(destined, max), min); } /** * calc length of one snap on y-axis * @param {Object} declaration the parsed declaration * @return {Number} */ function getYSnapLength(obj, alignment, direction) { if (alignment === START) { return 0; } else if (alignment === END) { return getHeight(obj); } else if (alignment === CENTER) { return roundByDirection(direction, getHeight(obj) / 2); } return 0; } /** * calc length of one snap on x-axis * @param {Object} declaration the parsed declaration * @return {Number} */ function getXSnapLength(obj, alignment, direction) { if (alignment === START) { return 0; } else if (alignment === END) { return getWidth(obj); } else if (alignment === CENTER) { return roundByDirection(direction, getWidth(obj) / 2); } return 0; } /** * get an elements scrollable height * @param {Object} obj * @return {Number} */ function getScrollHeight(obj) { return obj.scrollHeight; } /** * get an elements scrollable width * @param {Object} obj * @return {Number} */ function getScrollWidth(obj) { return obj.scrollWidth; } /** * get an elements height * @param {Object} obj * @return {Number} */ function getHeight(obj) { return obj.offsetHeight; } /** * get an elements width * @param {Object} obj * @return {Number} */ function getWidth(obj) { return obj.offsetWidth; } /** * get an elements height * @param {Object} obj * @return {Number} */ function getLeft(obj) { return obj.offsetLeft + obj.clientLeft; } /** * get an elements width * @param {Object} obj * @return {Number} */ function getTop(obj) { return obj.offsetTop + obj.clientTop; } /** * return the element scrolling values are applied to. * when receiving window.onscroll events, the actual scrolling is on the body. * @param {Object} obj * @return {Object} */ function getScrollObj(obj) { // if the scroll container is body, the scrolling is invoked on window/doc. if (obj == document || obj == window) { // firefox scrolls on doc.documentElement if (document.documentElement.scrollTop > 0 || document.documentElement.scrollLeft > 0) { return document.documentElement; } // chrome scrolls on body return document.querySelector('body'); } return obj; } /** * calc the duration of the animation proportional to the distance travelled * @param {Number} start * @param {Number} end * @return {Number} scroll time in ms */ function getDuration(start, end) { var distance = Math.abs(start - end), procDist = 100 / Math.max(document.documentElement.clientHeight, window.innerHeight || 1) * distance, duration = 100 / SCROLL_TIME * procDist; if (isNaN(duration)) { return 0; } return Math.max(SCROLL_TIME / 1.5, Math.min(duration, SCROLL_TIME)); } /** * ease in out function thanks to: * http://blog.greweb.fr/2012/02/bezier-curve-based-easing-functions-from-concept-to-implementation/ * @param {Number} t timing * @return {Number} easing factor */ var easeInCubic = function(t) { return t*t*t; }; /** * calculate the scroll position we should be in * @param {Number} start the start point of the scroll * @param {Number} end the end point of the scroll * @param {Number} elapsed the time elapsed from the beginning of the scroll * @param {Number} duration the total duration of the scroll (default 500ms) * @return {Number} the next position */ var position = function(start, end, elapsed, duration) { if (elapsed > duration) { return end; } return start + (end - start) * easeInCubic(elapsed / duration); }; // a current animation frame var animationFrame = null; /** * smoothScroll function by Alice Lietieur. * @see https://github.com/alicelieutier/smoothScroll * we use requestAnimationFrame to be called by the browser before every repaint * @param {Object} obj the scroll context * @param {Number} end where to scroll to * @param {Number} duration scroll duration * @param {Function} callback called when the scrolling is finished */ var smoothScroll = function(obj, end, callback) { var start = {y: obj.scrollTop, x: obj.scrollLeft}, clock = Date.now(), // get animation frame or a fallback requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || function(fn){window.setTimeout(fn, 15);}, duration = Math.max(getDuration(start.y, end.y), getDuration(start.x, end.x)); // setup the stepping function var step = function() { // calculate timings var elapsed = Date.now() - clock; // change position on y-axis if result is a number. if (!isNaN(end.y)) { obj.scrollTop = position(start.y, end.y, elapsed, duration); } // change position on x-axis if result is a number. if (!isNaN(end.x)) { obj.scrollLeft = position(start.x, end.x, elapsed, duration); } // check if we are over due if (elapsed > duration) { // is there a callback? if (typeof callback === 'function') { // stop execution and run the callback return callback(end); } // stop execution return; } // use a new animation frame animationFrame = requestAnimationFrame(step); }; // start the first step step(); }; var index = () => { /** * Feature detect scroll-snap-type, if it exists then do nothing (return) */ if ('scrollSnapAlign' in document.documentElement.style || 'webkitScrollSnapAlign' in document.documentElement.style || 'msScrollSnapAlign' in document.documentElement.style) { // just return void to stop executing the polyfill. return } Polyfill({ declarations: [ 'scroll-snap-type:*', 'scroll-snap-align:*', 'scroll-snap-padding:*' ] }) .doMatched(doMatched) .undoUnmatched(undoUnmatched); }; export default index;