| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779 |
- /*!
- Skrollr
- Plugin URL: https://github.com/Prinzhorn/skrollr
- License: Copyright Alexander Prinzhorn | 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.30'
- };
- //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();
- var duration = options.duration === undefined ? DEFAULT_DURATION : options.duration;
- //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: duration,
- startTime: now,
- endTime: now + 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;
- }
- }
- //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());
- //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);
- }
- //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));
|