|
@@ -0,0 +1,1779 @@
|
|
|
|
+/*!
|
|
|
|
+ * skrollr core
|
|
|
|
+ *
|
|
|
|
+ * Alexander Prinzhorn - https://github.com/Prinzhorn/skrollr
|
|
|
|
+ *
|
|
|
|
+ * Free to use under terms of MIT license
|
|
|
|
+ */
|
|
|
|
+(function(window, document, undefined) {
|
|
|
|
+ 'use strict';
|
|
|
|
+
|
|
|
|
+ /*
|
|
|
|
+ * Global api.
|
|
|
|
+ */
|
|
|
|
+ var skrollr = {
|
|
|
|
+ get: function() {
|
|
|
|
+ return _instance;
|
|
|
|
+ },
|
|
|
|
+ //Main entry point.
|
|
|
|
+ init: function(options) {
|
|
|
|
+ return _instance || new Skrollr(options);
|
|
|
|
+ },
|
|
|
|
+ VERSION: '0.6.29'
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ //Minify optimization.
|
|
|
|
+ var hasProp = Object.prototype.hasOwnProperty;
|
|
|
|
+ var Math = window.Math;
|
|
|
|
+ var getStyle = window.getComputedStyle;
|
|
|
|
+
|
|
|
|
+ //They will be filled when skrollr gets initialized.
|
|
|
|
+ var documentElement;
|
|
|
|
+ var body;
|
|
|
|
+
|
|
|
|
+ var EVENT_TOUCHSTART = 'touchstart';
|
|
|
|
+ var EVENT_TOUCHMOVE = 'touchmove';
|
|
|
|
+ var EVENT_TOUCHCANCEL = 'touchcancel';
|
|
|
|
+ var EVENT_TOUCHEND = 'touchend';
|
|
|
|
+
|
|
|
|
+ var SKROLLABLE_CLASS = 'skrollable';
|
|
|
|
+ var SKROLLABLE_BEFORE_CLASS = SKROLLABLE_CLASS + '-before';
|
|
|
|
+ var SKROLLABLE_BETWEEN_CLASS = SKROLLABLE_CLASS + '-between';
|
|
|
|
+ var SKROLLABLE_AFTER_CLASS = SKROLLABLE_CLASS + '-after';
|
|
|
|
+
|
|
|
|
+ var SKROLLR_CLASS = 'skrollr';
|
|
|
|
+ var NO_SKROLLR_CLASS = 'no-' + SKROLLR_CLASS;
|
|
|
|
+ var SKROLLR_DESKTOP_CLASS = SKROLLR_CLASS + '-desktop';
|
|
|
|
+ var SKROLLR_MOBILE_CLASS = SKROLLR_CLASS + '-mobile';
|
|
|
|
+
|
|
|
|
+ var DEFAULT_EASING = 'linear';
|
|
|
|
+ var DEFAULT_DURATION = 1000;//ms
|
|
|
|
+ var DEFAULT_MOBILE_DECELERATION = 0.004;//pixel/ms²
|
|
|
|
+
|
|
|
|
+ var DEFAULT_SKROLLRBODY = 'skrollr-body';
|
|
|
|
+
|
|
|
|
+ var DEFAULT_SMOOTH_SCROLLING_DURATION = 200;//ms
|
|
|
|
+
|
|
|
|
+ var ANCHOR_START = 'start';
|
|
|
|
+ var ANCHOR_END = 'end';
|
|
|
|
+ var ANCHOR_CENTER = 'center';
|
|
|
|
+ var ANCHOR_BOTTOM = 'bottom';
|
|
|
|
+
|
|
|
|
+ //The property which will be added to the DOM element to hold the ID of the skrollable.
|
|
|
|
+ var SKROLLABLE_ID_DOM_PROPERTY = '___skrollable_id';
|
|
|
|
+
|
|
|
|
+ var rxTouchIgnoreTags = /^(?:input|textarea|button|select)$/i;
|
|
|
|
+
|
|
|
|
+ var rxTrim = /^\s+|\s+$/g;
|
|
|
|
+
|
|
|
|
+ //Find all data-attributes. data-[_constant]-[offset]-[anchor]-[anchor].
|
|
|
|
+ var rxKeyframeAttribute = /^data(?:-(_\w+))?(?:-?(-?\d*\.?\d+p?))?(?:-?(start|end|top|center|bottom))?(?:-?(top|center|bottom))?$/;
|
|
|
|
+
|
|
|
|
+ var rxPropValue = /\s*(@?[\w\-\[\]]+)\s*:\s*(.+?)\s*(?:;|$)/gi;
|
|
|
|
+
|
|
|
|
+ //Easing function names follow the property in square brackets.
|
|
|
|
+ var rxPropEasing = /^(@?[a-z\-]+)\[(\w+)\]$/;
|
|
|
|
+
|
|
|
|
+ var rxCamelCase = /-([a-z0-9_])/g;
|
|
|
|
+ var rxCamelCaseFn = function(str, letter) {
|
|
|
|
+ return letter.toUpperCase();
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ //Numeric values with optional sign.
|
|
|
|
+ var rxNumericValue = /[\-+]?[\d]*\.?[\d]+/g;
|
|
|
|
+
|
|
|
|
+ //Used to replace occurences of {?} with a number.
|
|
|
|
+ var rxInterpolateString = /\{\?\}/g;
|
|
|
|
+
|
|
|
|
+ //Finds rgb(a) colors, which don't use the percentage notation.
|
|
|
|
+ var rxRGBAIntegerColor = /rgba?\(\s*-?\d+\s*,\s*-?\d+\s*,\s*-?\d+/g;
|
|
|
|
+
|
|
|
|
+ //Finds all gradients.
|
|
|
|
+ var rxGradient = /[a-z\-]+-gradient/g;
|
|
|
|
+
|
|
|
|
+ //Vendor prefix. Will be set once skrollr gets initialized.
|
|
|
|
+ var theCSSPrefix = '';
|
|
|
|
+ var theDashedCSSPrefix = '';
|
|
|
|
+
|
|
|
|
+ //Will be called once (when skrollr gets initialized).
|
|
|
|
+ var detectCSSPrefix = function() {
|
|
|
|
+ //Only relevant prefixes. May be extended.
|
|
|
|
+ //Could be dangerous if there will ever be a CSS property which actually starts with "ms". Don't hope so.
|
|
|
|
+ var rxPrefixes = /^(?:O|Moz|webkit|ms)|(?:-(?:o|moz|webkit|ms)-)/;
|
|
|
|
+
|
|
|
|
+ //Detect prefix for current browser by finding the first property using a prefix.
|
|
|
|
+ if(!getStyle) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ var style = getStyle(body, null);
|
|
|
|
+
|
|
|
|
+ for(var k in style) {
|
|
|
|
+ //We check the key and if the key is a number, we check the value as well, because safari's getComputedStyle returns some weird array-like thingy.
|
|
|
|
+ theCSSPrefix = (k.match(rxPrefixes) || (+k == k && style[k].match(rxPrefixes)));
|
|
|
|
+
|
|
|
|
+ if(theCSSPrefix) {
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Did we even detect a prefix?
|
|
|
|
+ if(!theCSSPrefix) {
|
|
|
|
+ theCSSPrefix = theDashedCSSPrefix = '';
|
|
|
|
+
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ theCSSPrefix = theCSSPrefix[0];
|
|
|
|
+
|
|
|
|
+ //We could have detected either a dashed prefix or this camelCaseish-inconsistent stuff.
|
|
|
|
+ if(theCSSPrefix.slice(0,1) === '-') {
|
|
|
|
+ theDashedCSSPrefix = theCSSPrefix;
|
|
|
|
+
|
|
|
|
+ //There's no logic behind these. Need a look up.
|
|
|
|
+ theCSSPrefix = ({
|
|
|
|
+ '-webkit-': 'webkit',
|
|
|
|
+ '-moz-': 'Moz',
|
|
|
|
+ '-ms-': 'ms',
|
|
|
|
+ '-o-': 'O'
|
|
|
|
+ })[theCSSPrefix];
|
|
|
|
+ } else {
|
|
|
|
+ theDashedCSSPrefix = '-' + theCSSPrefix.toLowerCase() + '-';
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ var polyfillRAF = function() {
|
|
|
|
+ var requestAnimFrame = window.requestAnimationFrame || window[theCSSPrefix.toLowerCase() + 'RequestAnimationFrame'];
|
|
|
|
+
|
|
|
|
+ var lastTime = _now();
|
|
|
|
+
|
|
|
|
+ if(_isMobile || !requestAnimFrame) {
|
|
|
|
+ requestAnimFrame = function(callback) {
|
|
|
|
+ //How long did it take to render?
|
|
|
|
+ var deltaTime = _now() - lastTime;
|
|
|
|
+ var delay = Math.max(0, 1000 / 60 - deltaTime);
|
|
|
|
+
|
|
|
|
+ return window.setTimeout(function() {
|
|
|
|
+ lastTime = _now();
|
|
|
|
+ callback();
|
|
|
|
+ }, delay);
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return requestAnimFrame;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ var polyfillCAF = function() {
|
|
|
|
+ var cancelAnimFrame = window.cancelAnimationFrame || window[theCSSPrefix.toLowerCase() + 'CancelAnimationFrame'];
|
|
|
|
+
|
|
|
|
+ if(_isMobile || !cancelAnimFrame) {
|
|
|
|
+ cancelAnimFrame = function(timeout) {
|
|
|
|
+ return window.clearTimeout(timeout);
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return cancelAnimFrame;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ //Built-in easing functions.
|
|
|
|
+ var easings = {
|
|
|
|
+ begin: function() {
|
|
|
|
+ return 0;
|
|
|
|
+ },
|
|
|
|
+ end: function() {
|
|
|
|
+ return 1;
|
|
|
|
+ },
|
|
|
|
+ linear: function(p) {
|
|
|
|
+ return p;
|
|
|
|
+ },
|
|
|
|
+ quadratic: function(p) {
|
|
|
|
+ return p * p;
|
|
|
|
+ },
|
|
|
|
+ cubic: function(p) {
|
|
|
|
+ return p * p * p;
|
|
|
|
+ },
|
|
|
|
+ swing: function(p) {
|
|
|
|
+ return (-Math.cos(p * Math.PI) / 2) + 0.5;
|
|
|
|
+ },
|
|
|
|
+ sqrt: function(p) {
|
|
|
|
+ return Math.sqrt(p);
|
|
|
|
+ },
|
|
|
|
+ outCubic: function(p) {
|
|
|
|
+ return (Math.pow((p - 1), 3) + 1);
|
|
|
|
+ },
|
|
|
|
+ //see https://www.desmos.com/calculator/tbr20s8vd2 for how I did this
|
|
|
|
+ bounce: function(p) {
|
|
|
|
+ var a;
|
|
|
|
+
|
|
|
|
+ if(p <= 0.5083) {
|
|
|
|
+ a = 3;
|
|
|
|
+ } else if(p <= 0.8489) {
|
|
|
|
+ a = 9;
|
|
|
|
+ } else if(p <= 0.96208) {
|
|
|
|
+ a = 27;
|
|
|
|
+ } else if(p <= 0.99981) {
|
|
|
|
+ a = 91;
|
|
|
|
+ } else {
|
|
|
|
+ return 1;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return 1 - Math.abs(3 * Math.cos(p * a * 1.028) / a);
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Constructor.
|
|
|
|
+ */
|
|
|
|
+ function Skrollr(options) {
|
|
|
|
+ documentElement = document.documentElement;
|
|
|
|
+ body = document.body;
|
|
|
|
+
|
|
|
|
+ detectCSSPrefix();
|
|
|
|
+
|
|
|
|
+ _instance = this;
|
|
|
|
+
|
|
|
|
+ options = options || {};
|
|
|
|
+
|
|
|
|
+ _constants = options.constants || {};
|
|
|
|
+
|
|
|
|
+ //We allow defining custom easings or overwrite existing.
|
|
|
|
+ if(options.easing) {
|
|
|
|
+ for(var e in options.easing) {
|
|
|
|
+ easings[e] = options.easing[e];
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ _edgeStrategy = options.edgeStrategy || 'set';
|
|
|
|
+
|
|
|
|
+ _listeners = {
|
|
|
|
+ //Function to be called right before rendering.
|
|
|
|
+ beforerender: options.beforerender,
|
|
|
|
+
|
|
|
|
+ //Function to be called right after finishing rendering.
|
|
|
|
+ render: options.render,
|
|
|
|
+
|
|
|
|
+ //Function to be called whenever an element with the `data-emit-events` attribute passes a keyframe.
|
|
|
|
+ keyframe: options.keyframe
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ //forceHeight is true by default
|
|
|
|
+ _forceHeight = options.forceHeight !== false;
|
|
|
|
+
|
|
|
|
+ if(_forceHeight) {
|
|
|
|
+ _scale = options.scale || 1;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ _mobileDeceleration = options.mobileDeceleration || DEFAULT_MOBILE_DECELERATION;
|
|
|
|
+
|
|
|
|
+ _smoothScrollingEnabled = options.smoothScrolling !== false;
|
|
|
|
+ _smoothScrollingDuration = options.smoothScrollingDuration || DEFAULT_SMOOTH_SCROLLING_DURATION;
|
|
|
|
+
|
|
|
|
+ //Dummy object. Will be overwritten in the _render method when smooth scrolling is calculated.
|
|
|
|
+ _smoothScrolling = {
|
|
|
|
+ targetTop: _instance.getScrollTop()
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ //A custom check function may be passed.
|
|
|
|
+ _isMobile = ((options.mobileCheck || function() {
|
|
|
|
+ return (/Android|iPhone|iPad|iPod|BlackBerry/i).test(navigator.userAgent || navigator.vendor || window.opera);
|
|
|
|
+ })());
|
|
|
|
+
|
|
|
|
+ if(_isMobile) {
|
|
|
|
+ _skrollrBody = document.getElementById(options.skrollrBody || DEFAULT_SKROLLRBODY);
|
|
|
|
+
|
|
|
|
+ //Detect 3d transform if there's a skrollr-body (only needed for #skrollr-body).
|
|
|
|
+ if(_skrollrBody) {
|
|
|
|
+ _detect3DTransforms();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ _initMobile();
|
|
|
|
+ _updateClass(documentElement, [SKROLLR_CLASS, SKROLLR_MOBILE_CLASS], [NO_SKROLLR_CLASS]);
|
|
|
|
+ } else {
|
|
|
|
+ _updateClass(documentElement, [SKROLLR_CLASS, SKROLLR_DESKTOP_CLASS], [NO_SKROLLR_CLASS]);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Triggers parsing of elements and a first reflow.
|
|
|
|
+ _instance.refresh();
|
|
|
|
+
|
|
|
|
+ _addEvent(window, 'resize orientationchange', function() {
|
|
|
|
+ var width = documentElement.clientWidth;
|
|
|
|
+ var height = documentElement.clientHeight;
|
|
|
|
+
|
|
|
|
+ //Only reflow if the size actually changed (#271).
|
|
|
|
+ if(height !== _lastViewportHeight || width !== _lastViewportWidth) {
|
|
|
|
+ _lastViewportHeight = height;
|
|
|
|
+ _lastViewportWidth = width;
|
|
|
|
+
|
|
|
|
+ _requestReflow = true;
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ var requestAnimFrame = polyfillRAF();
|
|
|
|
+
|
|
|
|
+ //Let's go.
|
|
|
|
+ (function animloop(){
|
|
|
|
+ _render();
|
|
|
|
+ _animFrame = requestAnimFrame(animloop);
|
|
|
|
+ }());
|
|
|
|
+
|
|
|
|
+ return _instance;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * (Re)parses some or all elements.
|
|
|
|
+ */
|
|
|
|
+ Skrollr.prototype.refresh = function(elements) {
|
|
|
|
+ var elementIndex;
|
|
|
|
+ var elementsLength;
|
|
|
|
+ var ignoreID = false;
|
|
|
|
+
|
|
|
|
+ //Completely reparse anything without argument.
|
|
|
|
+ if(elements === undefined) {
|
|
|
|
+ //Ignore that some elements may already have a skrollable ID.
|
|
|
|
+ ignoreID = true;
|
|
|
|
+
|
|
|
|
+ _skrollables = [];
|
|
|
|
+ _skrollableIdCounter = 0;
|
|
|
|
+
|
|
|
|
+ elements = document.getElementsByTagName('*');
|
|
|
|
+ } else if(elements.length === undefined) {
|
|
|
|
+ //We also accept a single element as parameter.
|
|
|
|
+ elements = [elements];
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ elementIndex = 0;
|
|
|
|
+ elementsLength = elements.length;
|
|
|
|
+
|
|
|
|
+ for(; elementIndex < elementsLength; elementIndex++) {
|
|
|
|
+ var el = elements[elementIndex];
|
|
|
|
+ var anchorTarget = el;
|
|
|
|
+ var keyFrames = [];
|
|
|
|
+
|
|
|
|
+ //If this particular element should be smooth scrolled.
|
|
|
|
+ var smoothScrollThis = _smoothScrollingEnabled;
|
|
|
|
+
|
|
|
|
+ //The edge strategy for this particular element.
|
|
|
|
+ var edgeStrategy = _edgeStrategy;
|
|
|
|
+
|
|
|
|
+ //If this particular element should emit keyframe events.
|
|
|
|
+ var emitEvents = false;
|
|
|
|
+
|
|
|
|
+ //If we're reseting the counter, remove any old element ids that may be hanging around.
|
|
|
|
+ if(ignoreID && SKROLLABLE_ID_DOM_PROPERTY in el) {
|
|
|
|
+ delete el[SKROLLABLE_ID_DOM_PROPERTY];
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if(!el.attributes) {
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Iterate over all attributes and search for key frame attributes.
|
|
|
|
+ var attributeIndex = 0;
|
|
|
|
+ var attributesLength = el.attributes.length;
|
|
|
|
+
|
|
|
|
+ for (; attributeIndex < attributesLength; attributeIndex++) {
|
|
|
|
+ var attr = el.attributes[attributeIndex];
|
|
|
|
+
|
|
|
|
+ if(attr.name === 'data-anchor-target') {
|
|
|
|
+ anchorTarget = document.querySelector(attr.value);
|
|
|
|
+
|
|
|
|
+ if(anchorTarget === null) {
|
|
|
|
+ throw 'Unable to find anchor target "' + attr.value + '"';
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Global smooth scrolling can be overridden by the element attribute.
|
|
|
|
+ if(attr.name === 'data-smooth-scrolling') {
|
|
|
|
+ smoothScrollThis = attr.value !== 'off';
|
|
|
|
+
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Global edge strategy can be overridden by the element attribute.
|
|
|
|
+ if(attr.name === 'data-edge-strategy') {
|
|
|
|
+ edgeStrategy = attr.value;
|
|
|
|
+
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Is this element tagged with the `data-emit-events` attribute?
|
|
|
|
+ if(attr.name === 'data-emit-events') {
|
|
|
|
+ emitEvents = true;
|
|
|
|
+
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ var match = attr.name.match(rxKeyframeAttribute);
|
|
|
|
+
|
|
|
|
+ if(match === null) {
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ var kf = {
|
|
|
|
+ props: attr.value,
|
|
|
|
+ //Point back to the element as well.
|
|
|
|
+ element: el,
|
|
|
|
+ //The name of the event which this keyframe will fire, if emitEvents is
|
|
|
|
+ eventType: attr.name.replace(rxCamelCase, rxCamelCaseFn)
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ keyFrames.push(kf);
|
|
|
|
+
|
|
|
|
+ var constant = match[1];
|
|
|
|
+
|
|
|
|
+ if(constant) {
|
|
|
|
+ //Strip the underscore prefix.
|
|
|
|
+ kf.constant = constant.substr(1);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Get the key frame offset.
|
|
|
|
+ var offset = match[2];
|
|
|
|
+
|
|
|
|
+ //Is it a percentage offset?
|
|
|
|
+ if(/p$/.test(offset)) {
|
|
|
|
+ kf.isPercentage = true;
|
|
|
|
+ kf.offset = (offset.slice(0, -1) | 0) / 100;
|
|
|
|
+ } else {
|
|
|
|
+ kf.offset = (offset | 0);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ var anchor1 = match[3];
|
|
|
|
+
|
|
|
|
+ //If second anchor is not set, the first will be taken for both.
|
|
|
|
+ var anchor2 = match[4] || anchor1;
|
|
|
|
+
|
|
|
|
+ //"absolute" (or "classic") mode, where numbers mean absolute scroll offset.
|
|
|
|
+ if(!anchor1 || anchor1 === ANCHOR_START || anchor1 === ANCHOR_END) {
|
|
|
|
+ kf.mode = 'absolute';
|
|
|
|
+
|
|
|
|
+ //data-end needs to be calculated after all key frames are known.
|
|
|
|
+ if(anchor1 === ANCHOR_END) {
|
|
|
|
+ kf.isEnd = true;
|
|
|
|
+ } else if(!kf.isPercentage) {
|
|
|
|
+ //For data-start we can already set the key frame w/o calculations.
|
|
|
|
+ //#59: "scale" options should only affect absolute mode.
|
|
|
|
+ kf.offset = kf.offset * _scale;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ //"relative" mode, where numbers are relative to anchors.
|
|
|
|
+ else {
|
|
|
|
+ kf.mode = 'relative';
|
|
|
|
+ kf.anchors = [anchor1, anchor2];
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Does this element have key frames?
|
|
|
|
+ if(!keyFrames.length) {
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Will hold the original style and class attributes before we controlled the element (see #80).
|
|
|
|
+ var styleAttr, classAttr;
|
|
|
|
+
|
|
|
|
+ var id;
|
|
|
|
+
|
|
|
|
+ if(!ignoreID && SKROLLABLE_ID_DOM_PROPERTY in el) {
|
|
|
|
+ //We already have this element under control. Grab the corresponding skrollable id.
|
|
|
|
+ id = el[SKROLLABLE_ID_DOM_PROPERTY];
|
|
|
|
+ styleAttr = _skrollables[id].styleAttr;
|
|
|
|
+ classAttr = _skrollables[id].classAttr;
|
|
|
|
+ } else {
|
|
|
|
+ //It's an unknown element. Asign it a new skrollable id.
|
|
|
|
+ id = (el[SKROLLABLE_ID_DOM_PROPERTY] = _skrollableIdCounter++);
|
|
|
|
+ styleAttr = el.style.cssText;
|
|
|
|
+ classAttr = _getClass(el);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ _skrollables[id] = {
|
|
|
|
+ element: el,
|
|
|
|
+ styleAttr: styleAttr,
|
|
|
|
+ classAttr: classAttr,
|
|
|
|
+ anchorTarget: anchorTarget,
|
|
|
|
+ keyFrames: keyFrames,
|
|
|
|
+ smoothScrolling: smoothScrollThis,
|
|
|
|
+ edgeStrategy: edgeStrategy,
|
|
|
|
+ emitEvents: emitEvents,
|
|
|
|
+ lastFrameIndex: -1
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ _updateClass(el, [SKROLLABLE_CLASS], []);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Reflow for the first time.
|
|
|
|
+ _reflow();
|
|
|
|
+
|
|
|
|
+ //Now that we got all key frame numbers right, actually parse the properties.
|
|
|
|
+ elementIndex = 0;
|
|
|
|
+ elementsLength = elements.length;
|
|
|
|
+
|
|
|
|
+ for(; elementIndex < elementsLength; elementIndex++) {
|
|
|
|
+ var sk = _skrollables[elements[elementIndex][SKROLLABLE_ID_DOM_PROPERTY]];
|
|
|
|
+
|
|
|
|
+ if(sk === undefined) {
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Parse the property string to objects
|
|
|
|
+ _parseProps(sk);
|
|
|
|
+
|
|
|
|
+ //Fill key frames with missing properties from left and right
|
|
|
|
+ _fillProps(sk);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return _instance;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Transform "relative" mode to "absolute" mode.
|
|
|
|
+ * That is, calculate anchor position and offset of element.
|
|
|
|
+ */
|
|
|
|
+ Skrollr.prototype.relativeToAbsolute = function(element, viewportAnchor, elementAnchor) {
|
|
|
|
+ var viewportHeight = documentElement.clientHeight;
|
|
|
|
+ var box = element.getBoundingClientRect();
|
|
|
|
+ var absolute = box.top;
|
|
|
|
+
|
|
|
|
+ //#100: IE doesn't supply "height" with getBoundingClientRect.
|
|
|
|
+ var boxHeight = box.bottom - box.top;
|
|
|
|
+
|
|
|
|
+ if(viewportAnchor === ANCHOR_BOTTOM) {
|
|
|
|
+ absolute -= viewportHeight;
|
|
|
|
+ } else if(viewportAnchor === ANCHOR_CENTER) {
|
|
|
|
+ absolute -= viewportHeight / 2;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if(elementAnchor === ANCHOR_BOTTOM) {
|
|
|
|
+ absolute += boxHeight;
|
|
|
|
+ } else if(elementAnchor === ANCHOR_CENTER) {
|
|
|
|
+ absolute += boxHeight / 2;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Compensate scrolling since getBoundingClientRect is relative to viewport.
|
|
|
|
+ absolute += _instance.getScrollTop();
|
|
|
|
+
|
|
|
|
+ return (absolute + 0.5) | 0;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Animates scroll top to new position.
|
|
|
|
+ */
|
|
|
|
+ Skrollr.prototype.animateTo = function(top, options) {
|
|
|
|
+ options = options || {};
|
|
|
|
+
|
|
|
|
+ var now = _now();
|
|
|
|
+ var scrollTop = _instance.getScrollTop();
|
|
|
|
+
|
|
|
|
+ //Setting this to a new value will automatically cause the current animation to stop, if any.
|
|
|
|
+ _scrollAnimation = {
|
|
|
|
+ startTop: scrollTop,
|
|
|
|
+ topDiff: top - scrollTop,
|
|
|
|
+ targetTop: top,
|
|
|
|
+ duration: options.duration || DEFAULT_DURATION,
|
|
|
|
+ startTime: now,
|
|
|
|
+ endTime: now + (options.duration || DEFAULT_DURATION),
|
|
|
|
+ easing: easings[options.easing || DEFAULT_EASING],
|
|
|
|
+ done: options.done
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ //Don't queue the animation if there's nothing to animate.
|
|
|
|
+ if(!_scrollAnimation.topDiff) {
|
|
|
|
+ if(_scrollAnimation.done) {
|
|
|
|
+ _scrollAnimation.done.call(_instance, false);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ _scrollAnimation = undefined;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return _instance;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Stops animateTo animation.
|
|
|
|
+ */
|
|
|
|
+ Skrollr.prototype.stopAnimateTo = function() {
|
|
|
|
+ if(_scrollAnimation && _scrollAnimation.done) {
|
|
|
|
+ _scrollAnimation.done.call(_instance, true);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ _scrollAnimation = undefined;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Returns if an animation caused by animateTo is currently running.
|
|
|
|
+ */
|
|
|
|
+ Skrollr.prototype.isAnimatingTo = function() {
|
|
|
|
+ return !!_scrollAnimation;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ Skrollr.prototype.isMobile = function() {
|
|
|
|
+ return _isMobile;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ Skrollr.prototype.setScrollTop = function(top, force) {
|
|
|
|
+ _forceRender = (force === true);
|
|
|
|
+
|
|
|
|
+ if(_isMobile) {
|
|
|
|
+ _mobileOffset = Math.min(Math.max(top, 0), _maxKeyFrame);
|
|
|
|
+ } else {
|
|
|
|
+ window.scrollTo(0, top);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return _instance;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ Skrollr.prototype.getScrollTop = function() {
|
|
|
|
+ if(_isMobile) {
|
|
|
|
+ return _mobileOffset;
|
|
|
|
+ } else {
|
|
|
|
+ return window.pageYOffset || documentElement.scrollTop || body.scrollTop || 0;
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ Skrollr.prototype.getMaxScrollTop = function() {
|
|
|
|
+ return _maxKeyFrame;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ Skrollr.prototype.on = function(name, fn) {
|
|
|
|
+ _listeners[name] = fn;
|
|
|
|
+
|
|
|
|
+ return _instance;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ Skrollr.prototype.off = function(name) {
|
|
|
|
+ delete _listeners[name];
|
|
|
|
+
|
|
|
|
+ return _instance;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ Skrollr.prototype.destroy = function() {
|
|
|
|
+ var cancelAnimFrame = polyfillCAF();
|
|
|
|
+ cancelAnimFrame(_animFrame);
|
|
|
|
+ _removeAllEvents();
|
|
|
|
+
|
|
|
|
+ _updateClass(documentElement, [NO_SKROLLR_CLASS], [SKROLLR_CLASS, SKROLLR_DESKTOP_CLASS, SKROLLR_MOBILE_CLASS]);
|
|
|
|
+
|
|
|
|
+ var skrollableIndex = 0;
|
|
|
|
+ var skrollablesLength = _skrollables.length;
|
|
|
|
+
|
|
|
|
+ for(; skrollableIndex < skrollablesLength; skrollableIndex++) {
|
|
|
|
+ _reset(_skrollables[skrollableIndex].element);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ documentElement.style.overflow = body.style.overflow = '';
|
|
|
|
+ documentElement.style.height = body.style.height = '';
|
|
|
|
+
|
|
|
|
+ if(_skrollrBody) {
|
|
|
|
+ skrollr.setStyle(_skrollrBody, 'transform', 'none');
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ _instance = undefined;
|
|
|
|
+ _skrollrBody = undefined;
|
|
|
|
+ _listeners = undefined;
|
|
|
|
+ _forceHeight = undefined;
|
|
|
|
+ _maxKeyFrame = 0;
|
|
|
|
+ _scale = 1;
|
|
|
|
+ _constants = undefined;
|
|
|
|
+ _mobileDeceleration = undefined;
|
|
|
|
+ _direction = 'down';
|
|
|
|
+ _lastTop = -1;
|
|
|
|
+ _lastViewportWidth = 0;
|
|
|
|
+ _lastViewportHeight = 0;
|
|
|
|
+ _requestReflow = false;
|
|
|
|
+ _scrollAnimation = undefined;
|
|
|
|
+ _smoothScrollingEnabled = undefined;
|
|
|
|
+ _smoothScrollingDuration = undefined;
|
|
|
|
+ _smoothScrolling = undefined;
|
|
|
|
+ _forceRender = undefined;
|
|
|
|
+ _skrollableIdCounter = 0;
|
|
|
|
+ _edgeStrategy = undefined;
|
|
|
|
+ _isMobile = false;
|
|
|
|
+ _mobileOffset = 0;
|
|
|
|
+ _translateZ = undefined;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /*
|
|
|
|
+ Private methods.
|
|
|
|
+ */
|
|
|
|
+
|
|
|
|
+ var _initMobile = function() {
|
|
|
|
+ var initialElement;
|
|
|
|
+ var initialTouchY;
|
|
|
|
+ var initialTouchX;
|
|
|
|
+ var currentElement;
|
|
|
|
+ var currentTouchY;
|
|
|
|
+ var currentTouchX;
|
|
|
|
+ var lastTouchY;
|
|
|
|
+ var deltaY;
|
|
|
|
+
|
|
|
|
+ var initialTouchTime;
|
|
|
|
+ var currentTouchTime;
|
|
|
|
+ var lastTouchTime;
|
|
|
|
+ var deltaTime;
|
|
|
|
+
|
|
|
|
+ _addEvent(documentElement, [EVENT_TOUCHSTART, EVENT_TOUCHMOVE, EVENT_TOUCHCANCEL, EVENT_TOUCHEND].join(' '), function(e) {
|
|
|
|
+ var touch = e.changedTouches[0];
|
|
|
|
+
|
|
|
|
+ currentElement = e.target;
|
|
|
|
+
|
|
|
|
+ //We don't want text nodes.
|
|
|
|
+ while(currentElement.nodeType === 3) {
|
|
|
|
+ currentElement = currentElement.parentNode;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ currentTouchY = touch.clientY;
|
|
|
|
+ currentTouchX = touch.clientX;
|
|
|
|
+ currentTouchTime = e.timeStamp;
|
|
|
|
+
|
|
|
|
+ if(!rxTouchIgnoreTags.test(currentElement.tagName)) {
|
|
|
|
+ e.preventDefault();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ switch(e.type) {
|
|
|
|
+ case EVENT_TOUCHSTART:
|
|
|
|
+ //The last element we tapped on.
|
|
|
|
+ if(initialElement) {
|
|
|
|
+ initialElement.blur();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ _instance.stopAnimateTo();
|
|
|
|
+
|
|
|
|
+ initialElement = currentElement;
|
|
|
|
+
|
|
|
|
+ initialTouchY = lastTouchY = currentTouchY;
|
|
|
|
+ initialTouchX = currentTouchX;
|
|
|
|
+ initialTouchTime = currentTouchTime;
|
|
|
|
+
|
|
|
|
+ break;
|
|
|
|
+ case EVENT_TOUCHMOVE:
|
|
|
|
+ //Prevent default event on touchIgnore elements in case they don't have focus yet.
|
|
|
|
+ if(rxTouchIgnoreTags.test(currentElement.tagName) && document.activeElement !== currentElement) {
|
|
|
|
+ e.preventDefault();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ deltaY = currentTouchY - lastTouchY;
|
|
|
|
+ deltaTime = currentTouchTime - lastTouchTime;
|
|
|
|
+
|
|
|
|
+ _instance.setScrollTop(_mobileOffset - deltaY, true);
|
|
|
|
+
|
|
|
|
+ lastTouchY = currentTouchY;
|
|
|
|
+ lastTouchTime = currentTouchTime;
|
|
|
|
+ break;
|
|
|
|
+ default:
|
|
|
|
+ case EVENT_TOUCHCANCEL:
|
|
|
|
+ case EVENT_TOUCHEND:
|
|
|
|
+ var distanceY = initialTouchY - currentTouchY;
|
|
|
|
+ var distanceX = initialTouchX - currentTouchX;
|
|
|
|
+ var distance2 = distanceX * distanceX + distanceY * distanceY;
|
|
|
|
+
|
|
|
|
+ //Check if it was more like a tap (moved less than 7px).
|
|
|
|
+ if(distance2 < 49) {
|
|
|
|
+ if(!rxTouchIgnoreTags.test(initialElement.tagName)) {
|
|
|
|
+ initialElement.focus();
|
|
|
|
+
|
|
|
|
+ //It was a tap, click the element.
|
|
|
|
+ var clickEvent = document.createEvent('MouseEvents');
|
|
|
|
+ clickEvent.initMouseEvent('click', true, true, e.view, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, 0, null);
|
|
|
|
+ initialElement.dispatchEvent(clickEvent);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ initialElement = undefined;
|
|
|
|
+
|
|
|
|
+ var speed = deltaY / deltaTime;
|
|
|
|
+
|
|
|
|
+ //Cap speed at 3 pixel/ms.
|
|
|
|
+ speed = Math.max(Math.min(speed, 3), -3);
|
|
|
|
+
|
|
|
|
+ var duration = Math.abs(speed / _mobileDeceleration);
|
|
|
|
+ var targetOffset = speed * duration + 0.5 * _mobileDeceleration * duration * duration;
|
|
|
|
+ var targetTop = _instance.getScrollTop() - targetOffset;
|
|
|
|
+
|
|
|
|
+ //Relative duration change for when scrolling above bounds.
|
|
|
|
+ var targetRatio = 0;
|
|
|
|
+
|
|
|
|
+ //Change duration proportionally when scrolling would leave bounds.
|
|
|
|
+ if(targetTop > _maxKeyFrame) {
|
|
|
|
+ targetRatio = (_maxKeyFrame - targetTop) / targetOffset;
|
|
|
|
+
|
|
|
|
+ targetTop = _maxKeyFrame;
|
|
|
|
+ } else if(targetTop < 0) {
|
|
|
|
+ targetRatio = -targetTop / targetOffset;
|
|
|
|
+
|
|
|
|
+ targetTop = 0;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ duration = duration * (1 - targetRatio);
|
|
|
|
+
|
|
|
|
+ _instance.animateTo((targetTop + 0.5) | 0, {easing: 'outCubic', duration: duration});
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ //Just in case there has already been some native scrolling, reset it.
|
|
|
|
+ window.scrollTo(0, 0);
|
|
|
|
+ documentElement.style.overflow = body.style.overflow = 'hidden';
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Updates key frames which depend on others / need to be updated on resize.
|
|
|
|
+ * That is "end" in "absolute" mode and all key frames in "relative" mode.
|
|
|
|
+ * Also handles constants, because they may change on resize.
|
|
|
|
+ */
|
|
|
|
+ var _updateDependentKeyFrames = function() {
|
|
|
|
+ var viewportHeight = documentElement.clientHeight;
|
|
|
|
+ var processedConstants = _processConstants();
|
|
|
|
+ var skrollable;
|
|
|
|
+ var element;
|
|
|
|
+ var anchorTarget;
|
|
|
|
+ var keyFrames;
|
|
|
|
+ var keyFrameIndex;
|
|
|
|
+ var keyFramesLength;
|
|
|
|
+ var kf;
|
|
|
|
+ var skrollableIndex;
|
|
|
|
+ var skrollablesLength;
|
|
|
|
+ var offset;
|
|
|
|
+ var constantValue;
|
|
|
|
+
|
|
|
|
+ //First process all relative-mode elements and find the max key frame.
|
|
|
|
+ skrollableIndex = 0;
|
|
|
|
+ skrollablesLength = _skrollables.length;
|
|
|
|
+
|
|
|
|
+ for(; skrollableIndex < skrollablesLength; skrollableIndex++) {
|
|
|
|
+ skrollable = _skrollables[skrollableIndex];
|
|
|
|
+ element = skrollable.element;
|
|
|
|
+ anchorTarget = skrollable.anchorTarget;
|
|
|
|
+ keyFrames = skrollable.keyFrames;
|
|
|
|
+
|
|
|
|
+ keyFrameIndex = 0;
|
|
|
|
+ keyFramesLength = keyFrames.length;
|
|
|
|
+
|
|
|
|
+ for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) {
|
|
|
|
+ kf = keyFrames[keyFrameIndex];
|
|
|
|
+
|
|
|
|
+ offset = kf.offset;
|
|
|
|
+ constantValue = processedConstants[kf.constant] || 0;
|
|
|
|
+
|
|
|
|
+ kf.frame = offset;
|
|
|
|
+
|
|
|
|
+ if(kf.isPercentage) {
|
|
|
|
+ //Convert the offset to percentage of the viewport height.
|
|
|
|
+ offset = offset * viewportHeight;
|
|
|
|
+
|
|
|
|
+ //Absolute + percentage mode.
|
|
|
|
+ kf.frame = offset;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if(kf.mode === 'relative') {
|
|
|
|
+ _reset(element);
|
|
|
|
+
|
|
|
|
+ kf.frame = _instance.relativeToAbsolute(anchorTarget, kf.anchors[0], kf.anchors[1]) - offset;
|
|
|
|
+
|
|
|
|
+ _reset(element, true);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ kf.frame += constantValue;
|
|
|
|
+
|
|
|
|
+ //Only search for max key frame when forceHeight is enabled.
|
|
|
|
+ if(_forceHeight) {
|
|
|
|
+ //Find the max key frame, but don't use one of the data-end ones for comparison.
|
|
|
|
+ if(!kf.isEnd && kf.frame > _maxKeyFrame) {
|
|
|
|
+ _maxKeyFrame = kf.frame;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //#133: The document can be larger than the maxKeyFrame we found.
|
|
|
|
+ _maxKeyFrame = Math.max(_maxKeyFrame, _getDocumentHeight());
|
|
|
|
+
|
|
|
|
+ //Now process all data-end keyframes.
|
|
|
|
+ skrollableIndex = 0;
|
|
|
|
+ skrollablesLength = _skrollables.length;
|
|
|
|
+
|
|
|
|
+ for(; skrollableIndex < skrollablesLength; skrollableIndex++) {
|
|
|
|
+ skrollable = _skrollables[skrollableIndex];
|
|
|
|
+ keyFrames = skrollable.keyFrames;
|
|
|
|
+
|
|
|
|
+ keyFrameIndex = 0;
|
|
|
|
+ keyFramesLength = keyFrames.length;
|
|
|
|
+
|
|
|
|
+ for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) {
|
|
|
|
+ kf = keyFrames[keyFrameIndex];
|
|
|
|
+
|
|
|
|
+ constantValue = processedConstants[kf.constant] || 0;
|
|
|
|
+
|
|
|
|
+ if(kf.isEnd) {
|
|
|
|
+ kf.frame = _maxKeyFrame - kf.offset + constantValue;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ skrollable.keyFrames.sort(_keyFrameComparator);
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Calculates and sets the style properties for the element at the given frame.
|
|
|
|
+ * @param fakeFrame The frame to render at when smooth scrolling is enabled.
|
|
|
|
+ * @param actualFrame The actual frame we are at.
|
|
|
|
+ */
|
|
|
|
+ var _calcSteps = function(fakeFrame, actualFrame) {
|
|
|
|
+ //Iterate over all skrollables.
|
|
|
|
+ var skrollableIndex = 0;
|
|
|
|
+ var skrollablesLength = _skrollables.length;
|
|
|
|
+
|
|
|
|
+ for(; skrollableIndex < skrollablesLength; skrollableIndex++) {
|
|
|
|
+ var skrollable = _skrollables[skrollableIndex];
|
|
|
|
+ var element = skrollable.element;
|
|
|
|
+ var frame = skrollable.smoothScrolling ? fakeFrame : actualFrame;
|
|
|
|
+ var frames = skrollable.keyFrames;
|
|
|
|
+ var framesLength = frames.length;
|
|
|
|
+ var firstFrame = frames[0];
|
|
|
|
+ var lastFrame = frames[frames.length - 1];
|
|
|
|
+ var beforeFirst = frame < firstFrame.frame;
|
|
|
|
+ var afterLast = frame > lastFrame.frame;
|
|
|
|
+ var firstOrLastFrame = beforeFirst ? firstFrame : lastFrame;
|
|
|
|
+ var emitEvents = skrollable.emitEvents;
|
|
|
|
+ var lastFrameIndex = skrollable.lastFrameIndex;
|
|
|
|
+ var key;
|
|
|
|
+ var value;
|
|
|
|
+
|
|
|
|
+ //If we are before/after the first/last frame, set the styles according to the given edge strategy.
|
|
|
|
+ if(beforeFirst || afterLast) {
|
|
|
|
+ //Check if we already handled this edge case last time.
|
|
|
|
+ //Note: using setScrollTop it's possible that we jumped from one edge to the other.
|
|
|
|
+ if(beforeFirst && skrollable.edge === -1 || afterLast && skrollable.edge === 1) {
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Add the skrollr-before or -after class.
|
|
|
|
+ if(beforeFirst) {
|
|
|
|
+ _updateClass(element, [SKROLLABLE_BEFORE_CLASS], [SKROLLABLE_AFTER_CLASS, SKROLLABLE_BETWEEN_CLASS]);
|
|
|
|
+
|
|
|
|
+ //This handles the special case where we exit the first keyframe.
|
|
|
|
+ if(emitEvents && lastFrameIndex > -1) {
|
|
|
|
+ _emitEvent(element, firstFrame.eventType, _direction);
|
|
|
|
+ skrollable.lastFrameIndex = -1;
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ _updateClass(element, [SKROLLABLE_AFTER_CLASS], [SKROLLABLE_BEFORE_CLASS, SKROLLABLE_BETWEEN_CLASS]);
|
|
|
|
+
|
|
|
|
+ //This handles the special case where we exit the last keyframe.
|
|
|
|
+ if(emitEvents && lastFrameIndex < framesLength) {
|
|
|
|
+ _emitEvent(element, lastFrame.eventType, _direction);
|
|
|
|
+ skrollable.lastFrameIndex = framesLength;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Remember that we handled the edge case (before/after the first/last keyframe).
|
|
|
|
+ skrollable.edge = beforeFirst ? -1 : 1;
|
|
|
|
+
|
|
|
|
+ switch(skrollable.edgeStrategy) {
|
|
|
|
+ case 'reset':
|
|
|
|
+ _reset(element);
|
|
|
|
+ continue;
|
|
|
|
+ case 'ease':
|
|
|
|
+ //Handle this case like it would be exactly at first/last keyframe and just pass it on.
|
|
|
|
+ frame = firstOrLastFrame.frame;
|
|
|
|
+ break;
|
|
|
|
+ default:
|
|
|
|
+ case 'set':
|
|
|
|
+ var props = firstOrLastFrame.props;
|
|
|
|
+
|
|
|
|
+ for(key in props) {
|
|
|
|
+ if(hasProp.call(props, key)) {
|
|
|
|
+ value = _interpolateString(props[key].value);
|
|
|
|
+
|
|
|
|
+ //Set style or attribute.
|
|
|
|
+ if(key.indexOf('@') === 0) {
|
|
|
|
+ element.setAttribute(key.substr(1), value);
|
|
|
|
+ } else {
|
|
|
|
+ skrollr.setStyle(element, key, value);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ //Did we handle an edge last time?
|
|
|
|
+ if(skrollable.edge !== 0) {
|
|
|
|
+ _updateClass(element, [SKROLLABLE_CLASS, SKROLLABLE_BETWEEN_CLASS], [SKROLLABLE_BEFORE_CLASS, SKROLLABLE_AFTER_CLASS]);
|
|
|
|
+ skrollable.edge = 0;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Find out between which two key frames we are right now.
|
|
|
|
+ var keyFrameIndex = 0;
|
|
|
|
+
|
|
|
|
+ for(; keyFrameIndex < framesLength - 1; keyFrameIndex++) {
|
|
|
|
+ if(frame >= frames[keyFrameIndex].frame && frame <= frames[keyFrameIndex + 1].frame) {
|
|
|
|
+ var left = frames[keyFrameIndex];
|
|
|
|
+ var right = frames[keyFrameIndex + 1];
|
|
|
|
+
|
|
|
|
+ for(key in left.props) {
|
|
|
|
+ if(hasProp.call(left.props, key)) {
|
|
|
|
+ var progress = (frame - left.frame) / (right.frame - left.frame);
|
|
|
|
+
|
|
|
|
+ //Transform the current progress using the given easing function.
|
|
|
|
+ progress = left.props[key].easing(progress);
|
|
|
|
+
|
|
|
|
+ //Interpolate between the two values
|
|
|
|
+ value = _calcInterpolation(left.props[key].value, right.props[key].value, progress);
|
|
|
|
+
|
|
|
|
+ value = _interpolateString(value);
|
|
|
|
+
|
|
|
|
+ //Set style or attribute.
|
|
|
|
+ if(key.indexOf('@') === 0) {
|
|
|
|
+ element.setAttribute(key.substr(1), value);
|
|
|
|
+ } else {
|
|
|
|
+ skrollr.setStyle(element, key, value);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Are events enabled on this element?
|
|
|
|
+ //This code handles the usual cases of scrolling through different keyframes.
|
|
|
|
+ //The special cases of before first and after last keyframe are handled above.
|
|
|
|
+ if(emitEvents) {
|
|
|
|
+ //Did we pass a new keyframe?
|
|
|
|
+ if(lastFrameIndex !== keyFrameIndex) {
|
|
|
|
+ if(_direction === 'down') {
|
|
|
|
+ _emitEvent(element, left.eventType, _direction);
|
|
|
|
+ } else {
|
|
|
|
+ _emitEvent(element, right.eventType, _direction);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ skrollable.lastFrameIndex = keyFrameIndex;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Renders all elements.
|
|
|
|
+ */
|
|
|
|
+ var _render = function() {
|
|
|
|
+ if(_requestReflow) {
|
|
|
|
+ _requestReflow = false;
|
|
|
|
+ _reflow();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //We may render something else than the actual scrollbar position.
|
|
|
|
+ var renderTop = _instance.getScrollTop();
|
|
|
|
+
|
|
|
|
+ //If there's an animation, which ends in current render call, call the callback after rendering.
|
|
|
|
+ var afterAnimationCallback;
|
|
|
|
+ var now = _now();
|
|
|
|
+ var progress;
|
|
|
|
+
|
|
|
|
+ //Before actually rendering handle the scroll animation, if any.
|
|
|
|
+ if(_scrollAnimation) {
|
|
|
|
+ //It's over
|
|
|
|
+ if(now >= _scrollAnimation.endTime) {
|
|
|
|
+ renderTop = _scrollAnimation.targetTop;
|
|
|
|
+ afterAnimationCallback = _scrollAnimation.done;
|
|
|
|
+ _scrollAnimation = undefined;
|
|
|
|
+ } else {
|
|
|
|
+ //Map the current progress to the new progress using given easing function.
|
|
|
|
+ progress = _scrollAnimation.easing((now - _scrollAnimation.startTime) / _scrollAnimation.duration);
|
|
|
|
+
|
|
|
|
+ renderTop = (_scrollAnimation.startTop + progress * _scrollAnimation.topDiff) | 0;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ _instance.setScrollTop(renderTop, true);
|
|
|
|
+ }
|
|
|
|
+ //Smooth scrolling only if there's no animation running and if we're not forcing the rendering.
|
|
|
|
+ else if(!_forceRender) {
|
|
|
|
+ var smoothScrollingDiff = _smoothScrolling.targetTop - renderTop;
|
|
|
|
+
|
|
|
|
+ //The user scrolled, start new smooth scrolling.
|
|
|
|
+ if(smoothScrollingDiff) {
|
|
|
|
+ _smoothScrolling = {
|
|
|
|
+ startTop: _lastTop,
|
|
|
|
+ topDiff: renderTop - _lastTop,
|
|
|
|
+ targetTop: renderTop,
|
|
|
|
+ startTime: _lastRenderCall,
|
|
|
|
+ endTime: _lastRenderCall + _smoothScrollingDuration
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Interpolate the internal scroll position (not the actual scrollbar).
|
|
|
|
+ if(now <= _smoothScrolling.endTime) {
|
|
|
|
+ //Map the current progress to the new progress using easing function.
|
|
|
|
+ progress = easings.sqrt((now - _smoothScrolling.startTime) / _smoothScrollingDuration);
|
|
|
|
+
|
|
|
|
+ renderTop = (_smoothScrolling.startTop + progress * _smoothScrolling.topDiff) | 0;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //That's were we actually "scroll" on mobile.
|
|
|
|
+ if(_isMobile && _skrollrBody) {
|
|
|
|
+ //Set the transform ("scroll it").
|
|
|
|
+ skrollr.setStyle(_skrollrBody, 'transform', 'translate(0, ' + -(_mobileOffset) + 'px) ' + _translateZ);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Did the scroll position even change?
|
|
|
|
+ if(_forceRender || _lastTop !== renderTop) {
|
|
|
|
+ //Remember in which direction are we scrolling?
|
|
|
|
+ _direction = (renderTop > _lastTop) ? 'down' : (renderTop < _lastTop ? 'up' : _direction);
|
|
|
|
+
|
|
|
|
+ _forceRender = false;
|
|
|
|
+
|
|
|
|
+ var listenerParams = {
|
|
|
|
+ curTop: renderTop,
|
|
|
|
+ lastTop: _lastTop,
|
|
|
|
+ maxTop: _maxKeyFrame,
|
|
|
|
+ direction: _direction
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ //Tell the listener we are about to render.
|
|
|
|
+ var continueRendering = _listeners.beforerender && _listeners.beforerender.call(_instance, listenerParams);
|
|
|
|
+
|
|
|
|
+ //The beforerender listener function is able the cancel rendering.
|
|
|
|
+ if(continueRendering !== false) {
|
|
|
|
+ //Now actually interpolate all the styles.
|
|
|
|
+ _calcSteps(renderTop, _instance.getScrollTop());
|
|
|
|
+
|
|
|
|
+ //Remember when we last rendered.
|
|
|
|
+ _lastTop = renderTop;
|
|
|
|
+
|
|
|
|
+ if(_listeners.render) {
|
|
|
|
+ _listeners.render.call(_instance, listenerParams);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if(afterAnimationCallback) {
|
|
|
|
+ afterAnimationCallback.call(_instance, false);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ _lastRenderCall = now;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Parses the properties for each key frame of the given skrollable.
|
|
|
|
+ */
|
|
|
|
+ var _parseProps = function(skrollable) {
|
|
|
|
+ //Iterate over all key frames
|
|
|
|
+ var keyFrameIndex = 0;
|
|
|
|
+ var keyFramesLength = skrollable.keyFrames.length;
|
|
|
|
+
|
|
|
|
+ for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) {
|
|
|
|
+ var frame = skrollable.keyFrames[keyFrameIndex];
|
|
|
|
+ var easing;
|
|
|
|
+ var value;
|
|
|
|
+ var prop;
|
|
|
|
+ var props = {};
|
|
|
|
+
|
|
|
|
+ var match;
|
|
|
|
+
|
|
|
|
+ while((match = rxPropValue.exec(frame.props)) !== null) {
|
|
|
|
+ prop = match[1];
|
|
|
|
+ value = match[2];
|
|
|
|
+
|
|
|
|
+ easing = prop.match(rxPropEasing);
|
|
|
|
+
|
|
|
|
+ //Is there an easing specified for this prop?
|
|
|
|
+ if(easing !== null) {
|
|
|
|
+ prop = easing[1];
|
|
|
|
+ easing = easing[2];
|
|
|
|
+ } else {
|
|
|
|
+ easing = DEFAULT_EASING;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Exclamation point at first position forces the value to be taken literal.
|
|
|
|
+ value = value.indexOf('!') ? _parseProp(value) : [value.slice(1)];
|
|
|
|
+
|
|
|
|
+ //Save the prop for this key frame with his value and easing function
|
|
|
|
+ props[prop] = {
|
|
|
|
+ value: value,
|
|
|
|
+ easing: easings[easing]
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ frame.props = props;
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Parses a value extracting numeric values and generating a format string
|
|
|
|
+ * for later interpolation of the new values in old string.
|
|
|
|
+ *
|
|
|
|
+ * @param val The CSS value to be parsed.
|
|
|
|
+ * @return Something like ["rgba(?%,?%, ?%,?)", 100, 50, 0, .7]
|
|
|
|
+ * where the first element is the format string later used
|
|
|
|
+ * and all following elements are the numeric value.
|
|
|
|
+ */
|
|
|
|
+ var _parseProp = function(val) {
|
|
|
|
+ var numbers = [];
|
|
|
|
+
|
|
|
|
+ //One special case, where floats don't work.
|
|
|
|
+ //We replace all occurences of rgba colors
|
|
|
|
+ //which don't use percentage notation with the percentage notation.
|
|
|
|
+ rxRGBAIntegerColor.lastIndex = 0;
|
|
|
|
+ val = val.replace(rxRGBAIntegerColor, function(rgba) {
|
|
|
|
+ return rgba.replace(rxNumericValue, function(n) {
|
|
|
|
+ return n / 255 * 100 + '%';
|
|
|
|
+ });
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ //Handle prefixing of "gradient" values.
|
|
|
|
+ //For now only the prefixed value will be set. Unprefixed isn't supported anyway.
|
|
|
|
+ if(theDashedCSSPrefix) {
|
|
|
|
+ rxGradient.lastIndex = 0;
|
|
|
|
+ val = val.replace(rxGradient, function(s) {
|
|
|
|
+ return theDashedCSSPrefix + s;
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Now parse ANY number inside this string and create a format string.
|
|
|
|
+ val = val.replace(rxNumericValue, function(n) {
|
|
|
|
+ numbers.push(+n);
|
|
|
|
+ return '{?}';
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ //Add the formatstring as first value.
|
|
|
|
+ numbers.unshift(val);
|
|
|
|
+
|
|
|
|
+ return numbers;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Fills the key frames with missing left and right hand properties.
|
|
|
|
+ * If key frame 1 has property X and key frame 2 is missing X,
|
|
|
|
+ * but key frame 3 has X again, then we need to assign X to key frame 2 too.
|
|
|
|
+ *
|
|
|
|
+ * @param sk A skrollable.
|
|
|
|
+ */
|
|
|
|
+ var _fillProps = function(sk) {
|
|
|
|
+ //Will collect the properties key frame by key frame
|
|
|
|
+ var propList = {};
|
|
|
|
+ var keyFrameIndex;
|
|
|
|
+ var keyFramesLength;
|
|
|
|
+
|
|
|
|
+ //Iterate over all key frames from left to right
|
|
|
|
+ keyFrameIndex = 0;
|
|
|
|
+ keyFramesLength = sk.keyFrames.length;
|
|
|
|
+
|
|
|
|
+ for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) {
|
|
|
|
+ _fillPropForFrame(sk.keyFrames[keyFrameIndex], propList);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Now do the same from right to fill the last gaps
|
|
|
|
+
|
|
|
|
+ propList = {};
|
|
|
|
+
|
|
|
|
+ //Iterate over all key frames from right to left
|
|
|
|
+ keyFrameIndex = sk.keyFrames.length - 1;
|
|
|
|
+
|
|
|
|
+ for(; keyFrameIndex >= 0; keyFrameIndex--) {
|
|
|
|
+ _fillPropForFrame(sk.keyFrames[keyFrameIndex], propList);
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ var _fillPropForFrame = function(frame, propList) {
|
|
|
|
+ var key;
|
|
|
|
+
|
|
|
|
+ //For each key frame iterate over all right hand properties and assign them,
|
|
|
|
+ //but only if the current key frame doesn't have the property by itself
|
|
|
|
+ for(key in propList) {
|
|
|
|
+ //The current frame misses this property, so assign it.
|
|
|
|
+ if(!hasProp.call(frame.props, key)) {
|
|
|
|
+ frame.props[key] = propList[key];
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Iterate over all props of the current frame and collect them
|
|
|
|
+ for(key in frame.props) {
|
|
|
|
+ propList[key] = frame.props[key];
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Calculates the new values for two given values array.
|
|
|
|
+ */
|
|
|
|
+ var _calcInterpolation = function(val1, val2, progress) {
|
|
|
|
+ var valueIndex;
|
|
|
|
+ var val1Length = val1.length;
|
|
|
|
+
|
|
|
|
+ //They both need to have the same length
|
|
|
|
+ if(val1Length !== val2.length) {
|
|
|
|
+ throw 'Can\'t interpolate between "' + val1[0] + '" and "' + val2[0] + '"';
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Add the format string as first element.
|
|
|
|
+ var interpolated = [val1[0]];
|
|
|
|
+
|
|
|
|
+ valueIndex = 1;
|
|
|
|
+
|
|
|
|
+ for(; valueIndex < val1Length; valueIndex++) {
|
|
|
|
+ //That's the line where the two numbers are actually interpolated.
|
|
|
|
+ interpolated[valueIndex] = val1[valueIndex] + ((val2[valueIndex] - val1[valueIndex]) * progress);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return interpolated;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Interpolates the numeric values into the format string.
|
|
|
|
+ */
|
|
|
|
+ var _interpolateString = function(val) {
|
|
|
|
+ var valueIndex = 1;
|
|
|
|
+
|
|
|
|
+ rxInterpolateString.lastIndex = 0;
|
|
|
|
+
|
|
|
|
+ return val[0].replace(rxInterpolateString, function() {
|
|
|
|
+ return val[valueIndex++];
|
|
|
|
+ });
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Resets the class and style attribute to what it was before skrollr manipulated the element.
|
|
|
|
+ * Also remembers the values it had before reseting, in order to undo the reset.
|
|
|
|
+ */
|
|
|
|
+ var _reset = function(elements, undo) {
|
|
|
|
+ //We accept a single element or an array of elements.
|
|
|
|
+ elements = [].concat(elements);
|
|
|
|
+
|
|
|
|
+ var skrollable;
|
|
|
|
+ var element;
|
|
|
|
+ var elementsIndex = 0;
|
|
|
|
+ var elementsLength = elements.length;
|
|
|
|
+
|
|
|
|
+ for(; elementsIndex < elementsLength; elementsIndex++) {
|
|
|
|
+ element = elements[elementsIndex];
|
|
|
|
+ skrollable = _skrollables[element[SKROLLABLE_ID_DOM_PROPERTY]];
|
|
|
|
+
|
|
|
|
+ //Couldn't find the skrollable for this DOM element.
|
|
|
|
+ if(!skrollable) {
|
|
|
|
+ continue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if(undo) {
|
|
|
|
+ //Reset class and style to the "dirty" (set by skrollr) values.
|
|
|
|
+ element.style.cssText = skrollable.dirtyStyleAttr;
|
|
|
|
+ _updateClass(element, skrollable.dirtyClassAttr);
|
|
|
|
+ } else {
|
|
|
|
+ //Remember the "dirty" (set by skrollr) class and style.
|
|
|
|
+ skrollable.dirtyStyleAttr = element.style.cssText;
|
|
|
|
+ skrollable.dirtyClassAttr = _getClass(element);
|
|
|
|
+
|
|
|
|
+ //Reset class and style to what it originally was.
|
|
|
|
+ element.style.cssText = skrollable.styleAttr;
|
|
|
|
+ _updateClass(element, skrollable.classAttr);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Detects support for 3d transforms by applying it to the skrollr-body.
|
|
|
|
+ */
|
|
|
|
+ var _detect3DTransforms = function() {
|
|
|
|
+ _translateZ = 'translateZ(0)';
|
|
|
|
+ skrollr.setStyle(_skrollrBody, 'transform', _translateZ);
|
|
|
|
+
|
|
|
|
+ var computedStyle = getStyle(_skrollrBody);
|
|
|
|
+ var computedTransform = computedStyle.getPropertyValue('transform');
|
|
|
|
+ var computedTransformWithPrefix = computedStyle.getPropertyValue(theDashedCSSPrefix + 'transform');
|
|
|
|
+ var has3D = (computedTransform && computedTransform !== 'none') || (computedTransformWithPrefix && computedTransformWithPrefix !== 'none');
|
|
|
|
+
|
|
|
|
+ if(!has3D) {
|
|
|
|
+ _translateZ = '';
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Set the CSS property on the given element. Sets prefixed properties as well.
|
|
|
|
+ */
|
|
|
|
+ skrollr.setStyle = function(el, prop, val) {
|
|
|
|
+ var style = el.style;
|
|
|
|
+
|
|
|
|
+ //Camel case.
|
|
|
|
+ prop = prop.replace(rxCamelCase, rxCamelCaseFn).replace('-', '');
|
|
|
|
+
|
|
|
|
+ //Make sure z-index gets a <integer>.
|
|
|
|
+ //This is the only <integer> case we need to handle.
|
|
|
|
+ if(prop === 'zIndex') {
|
|
|
|
+ if(isNaN(val)) {
|
|
|
|
+ //If it's not a number, don't touch it.
|
|
|
|
+ //It could for example be "auto" (#351).
|
|
|
|
+ style[prop] = val;
|
|
|
|
+ } else {
|
|
|
|
+ //Floor the number.
|
|
|
|
+ style[prop] = '' + (val | 0);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ //#64: "float" can't be set across browsers. Needs to use "cssFloat" for all except IE.
|
|
|
|
+ else if(prop === 'float') {
|
|
|
|
+ style.styleFloat = style.cssFloat = val;
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ //Need try-catch for old IE.
|
|
|
|
+ try {
|
|
|
|
+ //Set prefixed property if there's a prefix.
|
|
|
|
+ if(theCSSPrefix) {
|
|
|
|
+ style[theCSSPrefix + prop.slice(0,1).toUpperCase() + prop.slice(1)] = val;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Set unprefixed.
|
|
|
|
+ style[prop] = val;
|
|
|
|
+ } catch(ignore) {}
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Cross browser event handling.
|
|
|
|
+ */
|
|
|
|
+ var _addEvent = skrollr.addEvent = function(element, names, callback) {
|
|
|
|
+ var intermediate = function(e) {
|
|
|
|
+ //Normalize IE event stuff.
|
|
|
|
+ e = e || window.event;
|
|
|
|
+
|
|
|
|
+ if(!e.target) {
|
|
|
|
+ e.target = e.srcElement;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if(!e.preventDefault) {
|
|
|
|
+ e.preventDefault = function() {
|
|
|
|
+ e.returnValue = false;
|
|
|
|
+ e.defaultPrevented = true;
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return callback.call(this, e);
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ names = names.split(' ');
|
|
|
|
+
|
|
|
|
+ var name;
|
|
|
|
+ var nameCounter = 0;
|
|
|
|
+ var namesLength = names.length;
|
|
|
|
+
|
|
|
|
+ for(; nameCounter < namesLength; nameCounter++) {
|
|
|
|
+ name = names[nameCounter];
|
|
|
|
+
|
|
|
|
+ if(element.addEventListener) {
|
|
|
|
+ element.addEventListener(name, callback, false);
|
|
|
|
+ } else {
|
|
|
|
+ element.attachEvent('on' + name, intermediate);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Remember the events to be able to flush them later.
|
|
|
|
+ _registeredEvents.push({
|
|
|
|
+ element: element,
|
|
|
|
+ name: name,
|
|
|
|
+ listener: callback
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ var _removeEvent = skrollr.removeEvent = function(element, names, callback) {
|
|
|
|
+ names = names.split(' ');
|
|
|
|
+
|
|
|
|
+ var nameCounter = 0;
|
|
|
|
+ var namesLength = names.length;
|
|
|
|
+
|
|
|
|
+ for(; nameCounter < namesLength; nameCounter++) {
|
|
|
|
+ if(element.removeEventListener) {
|
|
|
|
+ element.removeEventListener(names[nameCounter], callback, false);
|
|
|
|
+ } else {
|
|
|
|
+ element.detachEvent('on' + names[nameCounter], callback);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ var _removeAllEvents = function() {
|
|
|
|
+ var eventData;
|
|
|
|
+ var eventCounter = 0;
|
|
|
|
+ var eventsLength = _registeredEvents.length;
|
|
|
|
+
|
|
|
|
+ for(; eventCounter < eventsLength; eventCounter++) {
|
|
|
|
+ eventData = _registeredEvents[eventCounter];
|
|
|
|
+
|
|
|
|
+ _removeEvent(eventData.element, eventData.name, eventData.listener);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ _registeredEvents = [];
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ var _emitEvent = function(element, name, direction) {
|
|
|
|
+ if(_listeners.keyframe) {
|
|
|
|
+ _listeners.keyframe.call(_instance, element, name, direction);
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ var _reflow = function() {
|
|
|
|
+ var pos = _instance.getScrollTop();
|
|
|
|
+
|
|
|
|
+ //Will be recalculated by _updateDependentKeyFrames.
|
|
|
|
+ _maxKeyFrame = 0;
|
|
|
|
+
|
|
|
|
+ if(_forceHeight && !_isMobile) {
|
|
|
|
+ //un-"force" the height to not mess with the calculations in _updateDependentKeyFrames (#216).
|
|
|
|
+ body.style.height = '';
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ _updateDependentKeyFrames();
|
|
|
|
+
|
|
|
|
+ if(_forceHeight && !_isMobile) {
|
|
|
|
+ //"force" the height.
|
|
|
|
+ body.style.height = (_maxKeyFrame + documentElement.clientHeight) + 'px';
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //The scroll offset may now be larger than needed (on desktop the browser/os prevents scrolling farther than the bottom).
|
|
|
|
+ if(_isMobile) {
|
|
|
|
+ _instance.setScrollTop(Math.min(_instance.getScrollTop(), _maxKeyFrame));
|
|
|
|
+ } else {
|
|
|
|
+ //Remember and reset the scroll pos (#217).
|
|
|
|
+ _instance.setScrollTop(pos, true);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ _forceRender = true;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /*
|
|
|
|
+ * Returns a copy of the constants object where all functions and strings have been evaluated.
|
|
|
|
+ */
|
|
|
|
+ var _processConstants = function() {
|
|
|
|
+ var viewportHeight = documentElement.clientHeight;
|
|
|
|
+ var copy = {};
|
|
|
|
+ var prop;
|
|
|
|
+ var value;
|
|
|
|
+
|
|
|
|
+ for(prop in _constants) {
|
|
|
|
+ value = _constants[prop];
|
|
|
|
+
|
|
|
|
+ if(typeof value === 'function') {
|
|
|
|
+ value = value.call(_instance);
|
|
|
|
+ }
|
|
|
|
+ //Percentage offset.
|
|
|
|
+ else if((/p$/).test(value)) {
|
|
|
|
+ value = (value.slice(0, -1) / 100) * viewportHeight;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ copy[prop] = value;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return copy;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /*
|
|
|
|
+ * Returns the height of the document.
|
|
|
|
+ */
|
|
|
|
+ var _getDocumentHeight = function() {
|
|
|
|
+ var skrollrBodyHeight = 0;
|
|
|
|
+ var bodyHeight;
|
|
|
|
+
|
|
|
|
+ if(_skrollrBody) {
|
|
|
|
+ skrollrBodyHeight = Math.max(_skrollrBody.offsetHeight, _skrollrBody.scrollHeight);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ bodyHeight = Math.max(skrollrBodyHeight, body.scrollHeight, body.offsetHeight, documentElement.scrollHeight, documentElement.offsetHeight, documentElement.clientHeight);
|
|
|
|
+
|
|
|
|
+ return bodyHeight - documentElement.clientHeight;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Returns a string of space separated classnames for the current element.
|
|
|
|
+ * Works with SVG as well.
|
|
|
|
+ */
|
|
|
|
+ var _getClass = function(element) {
|
|
|
|
+ var prop = 'className';
|
|
|
|
+
|
|
|
|
+ //SVG support by using className.baseVal instead of just className.
|
|
|
|
+ if(window.SVGElement && element instanceof window.SVGElement) {
|
|
|
|
+ element = element[prop];
|
|
|
|
+ prop = 'baseVal';
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return element[prop];
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Adds and removes a CSS classes.
|
|
|
|
+ * Works with SVG as well.
|
|
|
|
+ * add and remove are arrays of strings,
|
|
|
|
+ * or if remove is ommited add is a string and overwrites all classes.
|
|
|
|
+ */
|
|
|
|
+ var _updateClass = function(element, add, remove) {
|
|
|
|
+ var prop = 'className';
|
|
|
|
+
|
|
|
|
+ //SVG support by using className.baseVal instead of just className.
|
|
|
|
+ if(window.SVGElement && element instanceof window.SVGElement) {
|
|
|
|
+ element = element[prop];
|
|
|
|
+ prop = 'baseVal';
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //When remove is ommited, we want to overwrite/set the classes.
|
|
|
|
+ if(remove === undefined) {
|
|
|
|
+ element[prop] = add;
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ //Cache current classes. We will work on a string before passing back to DOM.
|
|
|
|
+ var val = element[prop];
|
|
|
|
+
|
|
|
|
+ //All classes to be removed.
|
|
|
|
+ var classRemoveIndex = 0;
|
|
|
|
+ var removeLength = remove.length;
|
|
|
|
+
|
|
|
|
+ for(; classRemoveIndex < removeLength; classRemoveIndex++) {
|
|
|
|
+ val = _untrim(val).replace(_untrim(remove[classRemoveIndex]), ' ');
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ val = _trim(val);
|
|
|
|
+
|
|
|
|
+ //All classes to be added.
|
|
|
|
+ var classAddIndex = 0;
|
|
|
|
+ var addLength = add.length;
|
|
|
|
+
|
|
|
|
+ for(; classAddIndex < addLength; classAddIndex++) {
|
|
|
|
+ //Only add if el not already has class.
|
|
|
|
+ if(_untrim(val).indexOf(_untrim(add[classAddIndex])) === -1) {
|
|
|
|
+ val += ' ' + add[classAddIndex];
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ element[prop] = _trim(val);
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ var _trim = function(a) {
|
|
|
|
+ return a.replace(rxTrim, '');
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Adds a space before and after the string.
|
|
|
|
+ */
|
|
|
|
+ var _untrim = function(a) {
|
|
|
|
+ return ' ' + a + ' ';
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ var _now = Date.now || function() {
|
|
|
|
+ return +new Date();
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ var _keyFrameComparator = function(a, b) {
|
|
|
|
+ return a.frame - b.frame;
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ /*
|
|
|
|
+ * Private variables.
|
|
|
|
+ */
|
|
|
|
+
|
|
|
|
+ //Singleton
|
|
|
|
+ var _instance;
|
|
|
|
+
|
|
|
|
+ /*
|
|
|
|
+ A list of all elements which should be animated associated with their the metadata.
|
|
|
|
+ Exmaple skrollable with two key frames animating from 100px width to 20px:
|
|
|
|
+
|
|
|
|
+ skrollable = {
|
|
|
|
+ element: <the DOM element>,
|
|
|
|
+ styleAttr: <style attribute of the element before skrollr>,
|
|
|
|
+ classAttr: <class attribute of the element before skrollr>,
|
|
|
|
+ keyFrames: [
|
|
|
|
+ {
|
|
|
|
+ frame: 100,
|
|
|
|
+ props: {
|
|
|
|
+ width: {
|
|
|
|
+ value: ['{?}px', 100],
|
|
|
|
+ easing: <reference to easing function>
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ mode: "absolute"
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ frame: 200,
|
|
|
|
+ props: {
|
|
|
|
+ width: {
|
|
|
|
+ value: ['{?}px', 20],
|
|
|
|
+ easing: <reference to easing function>
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ mode: "absolute"
|
|
|
|
+ }
|
|
|
|
+ ]
|
|
|
|
+ };
|
|
|
|
+ */
|
|
|
|
+ var _skrollables;
|
|
|
|
+
|
|
|
|
+ var _skrollrBody;
|
|
|
|
+
|
|
|
|
+ var _listeners;
|
|
|
|
+ var _forceHeight;
|
|
|
|
+ var _maxKeyFrame = 0;
|
|
|
|
+
|
|
|
|
+ var _scale = 1;
|
|
|
|
+ var _constants;
|
|
|
|
+
|
|
|
|
+ var _mobileDeceleration;
|
|
|
|
+
|
|
|
|
+ //Current direction (up/down).
|
|
|
|
+ var _direction = 'down';
|
|
|
|
+
|
|
|
|
+ //The last top offset value. Needed to determine direction.
|
|
|
|
+ var _lastTop = -1;
|
|
|
|
+
|
|
|
|
+ //The last time we called the render method (doesn't mean we rendered!).
|
|
|
|
+ var _lastRenderCall = _now();
|
|
|
|
+
|
|
|
|
+ //For detecting if it actually resized (#271).
|
|
|
|
+ var _lastViewportWidth = 0;
|
|
|
|
+ var _lastViewportHeight = 0;
|
|
|
|
+
|
|
|
|
+ var _requestReflow = false;
|
|
|
|
+
|
|
|
|
+ //Will contain data about a running scrollbar animation, if any.
|
|
|
|
+ var _scrollAnimation;
|
|
|
|
+
|
|
|
|
+ var _smoothScrollingEnabled;
|
|
|
|
+
|
|
|
|
+ var _smoothScrollingDuration;
|
|
|
|
+
|
|
|
|
+ //Will contain settins for smooth scrolling if enabled.
|
|
|
|
+ var _smoothScrolling;
|
|
|
|
+
|
|
|
|
+ //Can be set by any operation/event to force rendering even if the scrollbar didn't move.
|
|
|
|
+ var _forceRender;
|
|
|
|
+
|
|
|
|
+ //Each skrollable gets an unique ID incremented for each skrollable.
|
|
|
|
+ //The ID is the index in the _skrollables array.
|
|
|
|
+ var _skrollableIdCounter = 0;
|
|
|
|
+
|
|
|
|
+ var _edgeStrategy;
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ //Mobile specific vars. Will be stripped by UglifyJS when not in use.
|
|
|
|
+ var _isMobile = false;
|
|
|
|
+
|
|
|
|
+ //The virtual scroll offset when using mobile scrolling.
|
|
|
|
+ var _mobileOffset = 0;
|
|
|
|
+
|
|
|
|
+ //If the browser supports 3d transforms, this will be filled with 'translateZ(0)' (empty string otherwise).
|
|
|
|
+ var _translateZ;
|
|
|
|
+
|
|
|
|
+ //Will contain data about registered events by skrollr.
|
|
|
|
+ var _registeredEvents = [];
|
|
|
|
+
|
|
|
|
+ //Animation frame id returned by RequestAnimationFrame (or timeout when RAF is not supported).
|
|
|
|
+ var _animFrame;
|
|
|
|
+
|
|
|
|
+ //Expose skrollr as either a global variable or a require.js module.
|
|
|
|
+ if(typeof define === 'function' && define.amd) {
|
|
|
|
+ define([], function () {
|
|
|
|
+ return skrollr;
|
|
|
|
+ });
|
|
|
|
+ } else if (typeof module !== 'undefined' && module.exports) {
|
|
|
|
+ module.exports = skrollr;
|
|
|
|
+ } else {
|
|
|
|
+ window.skrollr = skrollr;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+}(window, document));
|