skrollr.js 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779
  1. /*!
  2. Skrollr
  3. Plugin URL: https://github.com/Prinzhorn/skrollr
  4. License: Copyright Alexander Prinzhorn | Free to use under terms of MIT license
  5. !*/
  6. (function(window, document, undefined) {
  7. 'use strict';
  8. /*
  9. * Global api.
  10. */
  11. var skrollr = {
  12. get: function() {
  13. return _instance;
  14. },
  15. //Main entry point.
  16. init: function(options) {
  17. return _instance || new Skrollr(options);
  18. },
  19. VERSION: '0.6.30'
  20. };
  21. //Minify optimization.
  22. var hasProp = Object.prototype.hasOwnProperty;
  23. var Math = window.Math;
  24. var getStyle = window.getComputedStyle;
  25. //They will be filled when skrollr gets initialized.
  26. var documentElement;
  27. var body;
  28. var EVENT_TOUCHSTART = 'touchstart';
  29. var EVENT_TOUCHMOVE = 'touchmove';
  30. var EVENT_TOUCHCANCEL = 'touchcancel';
  31. var EVENT_TOUCHEND = 'touchend';
  32. var SKROLLABLE_CLASS = 'skrollable';
  33. var SKROLLABLE_BEFORE_CLASS = SKROLLABLE_CLASS + '-before';
  34. var SKROLLABLE_BETWEEN_CLASS = SKROLLABLE_CLASS + '-between';
  35. var SKROLLABLE_AFTER_CLASS = SKROLLABLE_CLASS + '-after';
  36. var SKROLLR_CLASS = 'skrollr';
  37. var NO_SKROLLR_CLASS = 'no-' + SKROLLR_CLASS;
  38. var SKROLLR_DESKTOP_CLASS = SKROLLR_CLASS + '-desktop';
  39. var SKROLLR_MOBILE_CLASS = SKROLLR_CLASS + '-mobile';
  40. var DEFAULT_EASING = 'linear';
  41. var DEFAULT_DURATION = 1000;//ms
  42. var DEFAULT_MOBILE_DECELERATION = 0.004;//pixel/ms²
  43. var DEFAULT_SKROLLRBODY = 'skrollr-body';
  44. var DEFAULT_SMOOTH_SCROLLING_DURATION = 200;//ms
  45. var ANCHOR_START = 'start';
  46. var ANCHOR_END = 'end';
  47. var ANCHOR_CENTER = 'center';
  48. var ANCHOR_BOTTOM = 'bottom';
  49. //The property which will be added to the DOM element to hold the ID of the skrollable.
  50. var SKROLLABLE_ID_DOM_PROPERTY = '___skrollable_id';
  51. var rxTouchIgnoreTags = /^(?:input|textarea|button|select)$/i;
  52. var rxTrim = /^\s+|\s+$/g;
  53. //Find all data-attributes. data-[_constant]-[offset]-[anchor]-[anchor].
  54. var rxKeyframeAttribute = /^data(?:-(_\w+))?(?:-?(-?\d*\.?\d+p?))?(?:-?(start|end|top|center|bottom))?(?:-?(top|center|bottom))?$/;
  55. var rxPropValue = /\s*(@?[\w\-\[\]]+)\s*:\s*(.+?)\s*(?:;|$)/gi;
  56. //Easing function names follow the property in square brackets.
  57. var rxPropEasing = /^(@?[a-z\-]+)\[(\w+)\]$/;
  58. var rxCamelCase = /-([a-z0-9_])/g;
  59. var rxCamelCaseFn = function(str, letter) {
  60. return letter.toUpperCase();
  61. };
  62. //Numeric values with optional sign.
  63. var rxNumericValue = /[\-+]?[\d]*\.?[\d]+/g;
  64. //Used to replace occurences of {?} with a number.
  65. var rxInterpolateString = /\{\?\}/g;
  66. //Finds rgb(a) colors, which don't use the percentage notation.
  67. var rxRGBAIntegerColor = /rgba?\(\s*-?\d+\s*,\s*-?\d+\s*,\s*-?\d+/g;
  68. //Finds all gradients.
  69. var rxGradient = /[a-z\-]+-gradient/g;
  70. //Vendor prefix. Will be set once skrollr gets initialized.
  71. var theCSSPrefix = '';
  72. var theDashedCSSPrefix = '';
  73. //Will be called once (when skrollr gets initialized).
  74. var detectCSSPrefix = function() {
  75. //Only relevant prefixes. May be extended.
  76. //Could be dangerous if there will ever be a CSS property which actually starts with "ms". Don't hope so.
  77. var rxPrefixes = /^(?:O|Moz|webkit|ms)|(?:-(?:o|moz|webkit|ms)-)/;
  78. //Detect prefix for current browser by finding the first property using a prefix.
  79. if(!getStyle) {
  80. return;
  81. }
  82. var style = getStyle(body, null);
  83. for(var k in style) {
  84. //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.
  85. theCSSPrefix = (k.match(rxPrefixes) || (+k == k && style[k].match(rxPrefixes)));
  86. if(theCSSPrefix) {
  87. break;
  88. }
  89. }
  90. //Did we even detect a prefix?
  91. if(!theCSSPrefix) {
  92. theCSSPrefix = theDashedCSSPrefix = '';
  93. return;
  94. }
  95. theCSSPrefix = theCSSPrefix[0];
  96. //We could have detected either a dashed prefix or this camelCaseish-inconsistent stuff.
  97. if(theCSSPrefix.slice(0,1) === '-') {
  98. theDashedCSSPrefix = theCSSPrefix;
  99. //There's no logic behind these. Need a look up.
  100. theCSSPrefix = ({
  101. '-webkit-': 'webkit',
  102. '-moz-': 'Moz',
  103. '-ms-': 'ms',
  104. '-o-': 'O'
  105. })[theCSSPrefix];
  106. } else {
  107. theDashedCSSPrefix = '-' + theCSSPrefix.toLowerCase() + '-';
  108. }
  109. };
  110. var polyfillRAF = function() {
  111. var requestAnimFrame = window.requestAnimationFrame || window[theCSSPrefix.toLowerCase() + 'RequestAnimationFrame'];
  112. var lastTime = _now();
  113. if(_isMobile || !requestAnimFrame) {
  114. requestAnimFrame = function(callback) {
  115. //How long did it take to render?
  116. var deltaTime = _now() - lastTime;
  117. var delay = Math.max(0, 1000 / 60 - deltaTime);
  118. return window.setTimeout(function() {
  119. lastTime = _now();
  120. callback();
  121. }, delay);
  122. };
  123. }
  124. return requestAnimFrame;
  125. };
  126. var polyfillCAF = function() {
  127. var cancelAnimFrame = window.cancelAnimationFrame || window[theCSSPrefix.toLowerCase() + 'CancelAnimationFrame'];
  128. if(_isMobile || !cancelAnimFrame) {
  129. cancelAnimFrame = function(timeout) {
  130. return window.clearTimeout(timeout);
  131. };
  132. }
  133. return cancelAnimFrame;
  134. };
  135. //Built-in easing functions.
  136. var easings = {
  137. begin: function() {
  138. return 0;
  139. },
  140. end: function() {
  141. return 1;
  142. },
  143. linear: function(p) {
  144. return p;
  145. },
  146. quadratic: function(p) {
  147. return p * p;
  148. },
  149. cubic: function(p) {
  150. return p * p * p;
  151. },
  152. swing: function(p) {
  153. return (-Math.cos(p * Math.PI) / 2) + 0.5;
  154. },
  155. sqrt: function(p) {
  156. return Math.sqrt(p);
  157. },
  158. outCubic: function(p) {
  159. return (Math.pow((p - 1), 3) + 1);
  160. },
  161. //see https://www.desmos.com/calculator/tbr20s8vd2 for how I did this
  162. bounce: function(p) {
  163. var a;
  164. if(p <= 0.5083) {
  165. a = 3;
  166. } else if(p <= 0.8489) {
  167. a = 9;
  168. } else if(p <= 0.96208) {
  169. a = 27;
  170. } else if(p <= 0.99981) {
  171. a = 91;
  172. } else {
  173. return 1;
  174. }
  175. return 1 - Math.abs(3 * Math.cos(p * a * 1.028) / a);
  176. }
  177. };
  178. /**
  179. * Constructor.
  180. */
  181. function Skrollr(options) {
  182. documentElement = document.documentElement;
  183. body = document.body;
  184. detectCSSPrefix();
  185. _instance = this;
  186. options = options || {};
  187. _constants = options.constants || {};
  188. //We allow defining custom easings or overwrite existing.
  189. if(options.easing) {
  190. for(var e in options.easing) {
  191. easings[e] = options.easing[e];
  192. }
  193. }
  194. _edgeStrategy = options.edgeStrategy || 'set';
  195. _listeners = {
  196. //Function to be called right before rendering.
  197. beforerender: options.beforerender,
  198. //Function to be called right after finishing rendering.
  199. render: options.render,
  200. //Function to be called whenever an element with the `data-emit-events` attribute passes a keyframe.
  201. keyframe: options.keyframe
  202. };
  203. //forceHeight is true by default
  204. _forceHeight = options.forceHeight !== false;
  205. if(_forceHeight) {
  206. _scale = options.scale || 1;
  207. }
  208. _mobileDeceleration = options.mobileDeceleration || DEFAULT_MOBILE_DECELERATION;
  209. _smoothScrollingEnabled = options.smoothScrolling !== false;
  210. _smoothScrollingDuration = options.smoothScrollingDuration || DEFAULT_SMOOTH_SCROLLING_DURATION;
  211. //Dummy object. Will be overwritten in the _render method when smooth scrolling is calculated.
  212. _smoothScrolling = {
  213. targetTop: _instance.getScrollTop()
  214. };
  215. //A custom check function may be passed.
  216. _isMobile = ((options.mobileCheck || function() {
  217. return (/Android|iPhone|iPad|iPod|BlackBerry/i).test(navigator.userAgent || navigator.vendor || window.opera);
  218. })());
  219. if(_isMobile) {
  220. _skrollrBody = document.getElementById(options.skrollrBody || DEFAULT_SKROLLRBODY);
  221. //Detect 3d transform if there's a skrollr-body (only needed for #skrollr-body).
  222. if(_skrollrBody) {
  223. _detect3DTransforms();
  224. }
  225. _initMobile();
  226. _updateClass(documentElement, [SKROLLR_CLASS, SKROLLR_MOBILE_CLASS], [NO_SKROLLR_CLASS]);
  227. } else {
  228. _updateClass(documentElement, [SKROLLR_CLASS, SKROLLR_DESKTOP_CLASS], [NO_SKROLLR_CLASS]);
  229. }
  230. //Triggers parsing of elements and a first reflow.
  231. _instance.refresh();
  232. _addEvent(window, 'resize orientationchange', function() {
  233. var width = documentElement.clientWidth;
  234. var height = documentElement.clientHeight;
  235. //Only reflow if the size actually changed (#271).
  236. if(height !== _lastViewportHeight || width !== _lastViewportWidth) {
  237. _lastViewportHeight = height;
  238. _lastViewportWidth = width;
  239. _requestReflow = true;
  240. }
  241. });
  242. var requestAnimFrame = polyfillRAF();
  243. //Let's go.
  244. (function animloop(){
  245. _render();
  246. _animFrame = requestAnimFrame(animloop);
  247. }());
  248. return _instance;
  249. }
  250. /**
  251. * (Re)parses some or all elements.
  252. */
  253. Skrollr.prototype.refresh = function(elements) {
  254. var elementIndex;
  255. var elementsLength;
  256. var ignoreID = false;
  257. //Completely reparse anything without argument.
  258. if(elements === undefined) {
  259. //Ignore that some elements may already have a skrollable ID.
  260. ignoreID = true;
  261. _skrollables = [];
  262. _skrollableIdCounter = 0;
  263. elements = document.getElementsByTagName('*');
  264. } else if(elements.length === undefined) {
  265. //We also accept a single element as parameter.
  266. elements = [elements];
  267. }
  268. elementIndex = 0;
  269. elementsLength = elements.length;
  270. for(; elementIndex < elementsLength; elementIndex++) {
  271. var el = elements[elementIndex];
  272. var anchorTarget = el;
  273. var keyFrames = [];
  274. //If this particular element should be smooth scrolled.
  275. var smoothScrollThis = _smoothScrollingEnabled;
  276. //The edge strategy for this particular element.
  277. var edgeStrategy = _edgeStrategy;
  278. //If this particular element should emit keyframe events.
  279. var emitEvents = false;
  280. //If we're reseting the counter, remove any old element ids that may be hanging around.
  281. if(ignoreID && SKROLLABLE_ID_DOM_PROPERTY in el) {
  282. delete el[SKROLLABLE_ID_DOM_PROPERTY];
  283. }
  284. if(!el.attributes) {
  285. continue;
  286. }
  287. //Iterate over all attributes and search for key frame attributes.
  288. var attributeIndex = 0;
  289. var attributesLength = el.attributes.length;
  290. for (; attributeIndex < attributesLength; attributeIndex++) {
  291. var attr = el.attributes[attributeIndex];
  292. if(attr.name === 'data-anchor-target') {
  293. anchorTarget = document.querySelector(attr.value);
  294. if(anchorTarget === null) {
  295. throw 'Unable to find anchor target "' + attr.value + '"';
  296. }
  297. continue;
  298. }
  299. //Global smooth scrolling can be overridden by the element attribute.
  300. if(attr.name === 'data-smooth-scrolling') {
  301. smoothScrollThis = attr.value !== 'off';
  302. continue;
  303. }
  304. //Global edge strategy can be overridden by the element attribute.
  305. if(attr.name === 'data-edge-strategy') {
  306. edgeStrategy = attr.value;
  307. continue;
  308. }
  309. //Is this element tagged with the `data-emit-events` attribute?
  310. if(attr.name === 'data-emit-events') {
  311. emitEvents = true;
  312. continue;
  313. }
  314. var match = attr.name.match(rxKeyframeAttribute);
  315. if(match === null) {
  316. continue;
  317. }
  318. var kf = {
  319. props: attr.value,
  320. //Point back to the element as well.
  321. element: el,
  322. //The name of the event which this keyframe will fire, if emitEvents is
  323. eventType: attr.name.replace(rxCamelCase, rxCamelCaseFn)
  324. };
  325. keyFrames.push(kf);
  326. var constant = match[1];
  327. if(constant) {
  328. //Strip the underscore prefix.
  329. kf.constant = constant.substr(1);
  330. }
  331. //Get the key frame offset.
  332. var offset = match[2];
  333. //Is it a percentage offset?
  334. if(/p$/.test(offset)) {
  335. kf.isPercentage = true;
  336. kf.offset = (offset.slice(0, -1) | 0) / 100;
  337. } else {
  338. kf.offset = (offset | 0);
  339. }
  340. var anchor1 = match[3];
  341. //If second anchor is not set, the first will be taken for both.
  342. var anchor2 = match[4] || anchor1;
  343. //"absolute" (or "classic") mode, where numbers mean absolute scroll offset.
  344. if(!anchor1 || anchor1 === ANCHOR_START || anchor1 === ANCHOR_END) {
  345. kf.mode = 'absolute';
  346. //data-end needs to be calculated after all key frames are known.
  347. if(anchor1 === ANCHOR_END) {
  348. kf.isEnd = true;
  349. } else if(!kf.isPercentage) {
  350. //For data-start we can already set the key frame w/o calculations.
  351. //#59: "scale" options should only affect absolute mode.
  352. kf.offset = kf.offset * _scale;
  353. }
  354. }
  355. //"relative" mode, where numbers are relative to anchors.
  356. else {
  357. kf.mode = 'relative';
  358. kf.anchors = [anchor1, anchor2];
  359. }
  360. }
  361. //Does this element have key frames?
  362. if(!keyFrames.length) {
  363. continue;
  364. }
  365. //Will hold the original style and class attributes before we controlled the element (see #80).
  366. var styleAttr, classAttr;
  367. var id;
  368. if(!ignoreID && SKROLLABLE_ID_DOM_PROPERTY in el) {
  369. //We already have this element under control. Grab the corresponding skrollable id.
  370. id = el[SKROLLABLE_ID_DOM_PROPERTY];
  371. styleAttr = _skrollables[id].styleAttr;
  372. classAttr = _skrollables[id].classAttr;
  373. } else {
  374. //It's an unknown element. Asign it a new skrollable id.
  375. id = (el[SKROLLABLE_ID_DOM_PROPERTY] = _skrollableIdCounter++);
  376. styleAttr = el.style.cssText;
  377. classAttr = _getClass(el);
  378. }
  379. _skrollables[id] = {
  380. element: el,
  381. styleAttr: styleAttr,
  382. classAttr: classAttr,
  383. anchorTarget: anchorTarget,
  384. keyFrames: keyFrames,
  385. smoothScrolling: smoothScrollThis,
  386. edgeStrategy: edgeStrategy,
  387. emitEvents: emitEvents,
  388. lastFrameIndex: -1
  389. };
  390. _updateClass(el, [SKROLLABLE_CLASS], []);
  391. }
  392. //Reflow for the first time.
  393. _reflow();
  394. //Now that we got all key frame numbers right, actually parse the properties.
  395. elementIndex = 0;
  396. elementsLength = elements.length;
  397. for(; elementIndex < elementsLength; elementIndex++) {
  398. var sk = _skrollables[elements[elementIndex][SKROLLABLE_ID_DOM_PROPERTY]];
  399. if(sk === undefined) {
  400. continue;
  401. }
  402. //Parse the property string to objects
  403. _parseProps(sk);
  404. //Fill key frames with missing properties from left and right
  405. _fillProps(sk);
  406. }
  407. return _instance;
  408. };
  409. /**
  410. * Transform "relative" mode to "absolute" mode.
  411. * That is, calculate anchor position and offset of element.
  412. */
  413. Skrollr.prototype.relativeToAbsolute = function(element, viewportAnchor, elementAnchor) {
  414. var viewportHeight = documentElement.clientHeight;
  415. var box = element.getBoundingClientRect();
  416. var absolute = box.top;
  417. //#100: IE doesn't supply "height" with getBoundingClientRect.
  418. var boxHeight = box.bottom - box.top;
  419. if(viewportAnchor === ANCHOR_BOTTOM) {
  420. absolute -= viewportHeight;
  421. } else if(viewportAnchor === ANCHOR_CENTER) {
  422. absolute -= viewportHeight / 2;
  423. }
  424. if(elementAnchor === ANCHOR_BOTTOM) {
  425. absolute += boxHeight;
  426. } else if(elementAnchor === ANCHOR_CENTER) {
  427. absolute += boxHeight / 2;
  428. }
  429. //Compensate scrolling since getBoundingClientRect is relative to viewport.
  430. absolute += _instance.getScrollTop();
  431. return (absolute + 0.5) | 0;
  432. };
  433. /**
  434. * Animates scroll top to new position.
  435. */
  436. Skrollr.prototype.animateTo = function(top, options) {
  437. options = options || {};
  438. var now = _now();
  439. var scrollTop = _instance.getScrollTop();
  440. var duration = options.duration === undefined ? DEFAULT_DURATION : options.duration;
  441. //Setting this to a new value will automatically cause the current animation to stop, if any.
  442. _scrollAnimation = {
  443. startTop: scrollTop,
  444. topDiff: top - scrollTop,
  445. targetTop: top,
  446. duration: duration,
  447. startTime: now,
  448. endTime: now + duration,
  449. easing: easings[options.easing || DEFAULT_EASING],
  450. done: options.done
  451. };
  452. //Don't queue the animation if there's nothing to animate.
  453. if(!_scrollAnimation.topDiff) {
  454. if(_scrollAnimation.done) {
  455. _scrollAnimation.done.call(_instance, false);
  456. }
  457. _scrollAnimation = undefined;
  458. }
  459. return _instance;
  460. };
  461. /**
  462. * Stops animateTo animation.
  463. */
  464. Skrollr.prototype.stopAnimateTo = function() {
  465. if(_scrollAnimation && _scrollAnimation.done) {
  466. _scrollAnimation.done.call(_instance, true);
  467. }
  468. _scrollAnimation = undefined;
  469. };
  470. /**
  471. * Returns if an animation caused by animateTo is currently running.
  472. */
  473. Skrollr.prototype.isAnimatingTo = function() {
  474. return !!_scrollAnimation;
  475. };
  476. Skrollr.prototype.isMobile = function() {
  477. return _isMobile;
  478. };
  479. Skrollr.prototype.setScrollTop = function(top, force) {
  480. _forceRender = (force === true);
  481. if(_isMobile) {
  482. _mobileOffset = Math.min(Math.max(top, 0), _maxKeyFrame);
  483. } else {
  484. window.scrollTo(0, top);
  485. }
  486. return _instance;
  487. };
  488. Skrollr.prototype.getScrollTop = function() {
  489. if(_isMobile) {
  490. return _mobileOffset;
  491. } else {
  492. return window.pageYOffset || documentElement.scrollTop || body.scrollTop || 0;
  493. }
  494. };
  495. Skrollr.prototype.getMaxScrollTop = function() {
  496. return _maxKeyFrame;
  497. };
  498. Skrollr.prototype.on = function(name, fn) {
  499. _listeners[name] = fn;
  500. return _instance;
  501. };
  502. Skrollr.prototype.off = function(name) {
  503. delete _listeners[name];
  504. return _instance;
  505. };
  506. Skrollr.prototype.destroy = function() {
  507. var cancelAnimFrame = polyfillCAF();
  508. cancelAnimFrame(_animFrame);
  509. _removeAllEvents();
  510. _updateClass(documentElement, [NO_SKROLLR_CLASS], [SKROLLR_CLASS, SKROLLR_DESKTOP_CLASS, SKROLLR_MOBILE_CLASS]);
  511. var skrollableIndex = 0;
  512. var skrollablesLength = _skrollables.length;
  513. for(; skrollableIndex < skrollablesLength; skrollableIndex++) {
  514. _reset(_skrollables[skrollableIndex].element);
  515. }
  516. documentElement.style.overflow = body.style.overflow = '';
  517. documentElement.style.height = body.style.height = '';
  518. if(_skrollrBody) {
  519. skrollr.setStyle(_skrollrBody, 'transform', 'none');
  520. }
  521. _instance = undefined;
  522. _skrollrBody = undefined;
  523. _listeners = undefined;
  524. _forceHeight = undefined;
  525. _maxKeyFrame = 0;
  526. _scale = 1;
  527. _constants = undefined;
  528. _mobileDeceleration = undefined;
  529. _direction = 'down';
  530. _lastTop = -1;
  531. _lastViewportWidth = 0;
  532. _lastViewportHeight = 0;
  533. _requestReflow = false;
  534. _scrollAnimation = undefined;
  535. _smoothScrollingEnabled = undefined;
  536. _smoothScrollingDuration = undefined;
  537. _smoothScrolling = undefined;
  538. _forceRender = undefined;
  539. _skrollableIdCounter = 0;
  540. _edgeStrategy = undefined;
  541. _isMobile = false;
  542. _mobileOffset = 0;
  543. _translateZ = undefined;
  544. };
  545. /*
  546. Private methods.
  547. */
  548. var _initMobile = function() {
  549. var initialElement;
  550. var initialTouchY;
  551. var initialTouchX;
  552. var currentElement;
  553. var currentTouchY;
  554. var currentTouchX;
  555. var lastTouchY;
  556. var deltaY;
  557. var initialTouchTime;
  558. var currentTouchTime;
  559. var lastTouchTime;
  560. var deltaTime;
  561. _addEvent(documentElement, [EVENT_TOUCHSTART, EVENT_TOUCHMOVE, EVENT_TOUCHCANCEL, EVENT_TOUCHEND].join(' '), function(e) {
  562. var touch = e.changedTouches[0];
  563. currentElement = e.target;
  564. //We don't want text nodes.
  565. while(currentElement.nodeType === 3) {
  566. currentElement = currentElement.parentNode;
  567. }
  568. currentTouchY = touch.clientY;
  569. currentTouchX = touch.clientX;
  570. currentTouchTime = e.timeStamp;
  571. if(!rxTouchIgnoreTags.test(currentElement.tagName)) {
  572. e.preventDefault();
  573. }
  574. switch(e.type) {
  575. case EVENT_TOUCHSTART:
  576. //The last element we tapped on.
  577. if(initialElement) {
  578. initialElement.blur();
  579. }
  580. _instance.stopAnimateTo();
  581. initialElement = currentElement;
  582. initialTouchY = lastTouchY = currentTouchY;
  583. initialTouchX = currentTouchX;
  584. initialTouchTime = currentTouchTime;
  585. break;
  586. case EVENT_TOUCHMOVE:
  587. //Prevent default event on touchIgnore elements in case they don't have focus yet.
  588. if(rxTouchIgnoreTags.test(currentElement.tagName) && document.activeElement !== currentElement) {
  589. e.preventDefault();
  590. }
  591. deltaY = currentTouchY - lastTouchY;
  592. deltaTime = currentTouchTime - lastTouchTime;
  593. _instance.setScrollTop(_mobileOffset - deltaY, true);
  594. lastTouchY = currentTouchY;
  595. lastTouchTime = currentTouchTime;
  596. break;
  597. default:
  598. case EVENT_TOUCHCANCEL:
  599. case EVENT_TOUCHEND:
  600. var distanceY = initialTouchY - currentTouchY;
  601. var distanceX = initialTouchX - currentTouchX;
  602. var distance2 = distanceX * distanceX + distanceY * distanceY;
  603. //Check if it was more like a tap (moved less than 7px).
  604. if(distance2 < 49) {
  605. if(!rxTouchIgnoreTags.test(initialElement.tagName)) {
  606. initialElement.focus();
  607. //It was a tap, click the element.
  608. var clickEvent = document.createEvent('MouseEvents');
  609. 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);
  610. initialElement.dispatchEvent(clickEvent);
  611. }
  612. return;
  613. }
  614. initialElement = undefined;
  615. var speed = deltaY / deltaTime;
  616. //Cap speed at 3 pixel/ms.
  617. speed = Math.max(Math.min(speed, 3), -3);
  618. var duration = Math.abs(speed / _mobileDeceleration);
  619. var targetOffset = speed * duration + 0.5 * _mobileDeceleration * duration * duration;
  620. var targetTop = _instance.getScrollTop() - targetOffset;
  621. //Relative duration change for when scrolling above bounds.
  622. var targetRatio = 0;
  623. //Change duration proportionally when scrolling would leave bounds.
  624. if(targetTop > _maxKeyFrame) {
  625. targetRatio = (_maxKeyFrame - targetTop) / targetOffset;
  626. targetTop = _maxKeyFrame;
  627. } else if(targetTop < 0) {
  628. targetRatio = -targetTop / targetOffset;
  629. targetTop = 0;
  630. }
  631. duration = duration * (1 - targetRatio);
  632. _instance.animateTo((targetTop + 0.5) | 0, {easing: 'outCubic', duration: duration});
  633. break;
  634. }
  635. });
  636. //Just in case there has already been some native scrolling, reset it.
  637. window.scrollTo(0, 0);
  638. documentElement.style.overflow = body.style.overflow = 'hidden';
  639. };
  640. /**
  641. * Updates key frames which depend on others / need to be updated on resize.
  642. * That is "end" in "absolute" mode and all key frames in "relative" mode.
  643. * Also handles constants, because they may change on resize.
  644. */
  645. var _updateDependentKeyFrames = function() {
  646. var viewportHeight = documentElement.clientHeight;
  647. var processedConstants = _processConstants();
  648. var skrollable;
  649. var element;
  650. var anchorTarget;
  651. var keyFrames;
  652. var keyFrameIndex;
  653. var keyFramesLength;
  654. var kf;
  655. var skrollableIndex;
  656. var skrollablesLength;
  657. var offset;
  658. var constantValue;
  659. //First process all relative-mode elements and find the max key frame.
  660. skrollableIndex = 0;
  661. skrollablesLength = _skrollables.length;
  662. for(; skrollableIndex < skrollablesLength; skrollableIndex++) {
  663. skrollable = _skrollables[skrollableIndex];
  664. element = skrollable.element;
  665. anchorTarget = skrollable.anchorTarget;
  666. keyFrames = skrollable.keyFrames;
  667. keyFrameIndex = 0;
  668. keyFramesLength = keyFrames.length;
  669. for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) {
  670. kf = keyFrames[keyFrameIndex];
  671. offset = kf.offset;
  672. constantValue = processedConstants[kf.constant] || 0;
  673. kf.frame = offset;
  674. if(kf.isPercentage) {
  675. //Convert the offset to percentage of the viewport height.
  676. offset = offset * viewportHeight;
  677. //Absolute + percentage mode.
  678. kf.frame = offset;
  679. }
  680. if(kf.mode === 'relative') {
  681. _reset(element);
  682. kf.frame = _instance.relativeToAbsolute(anchorTarget, kf.anchors[0], kf.anchors[1]) - offset;
  683. _reset(element, true);
  684. }
  685. kf.frame += constantValue;
  686. //Only search for max key frame when forceHeight is enabled.
  687. if(_forceHeight) {
  688. //Find the max key frame, but don't use one of the data-end ones for comparison.
  689. if(!kf.isEnd && kf.frame > _maxKeyFrame) {
  690. _maxKeyFrame = kf.frame;
  691. }
  692. }
  693. }
  694. }
  695. //#133: The document can be larger than the maxKeyFrame we found.
  696. _maxKeyFrame = Math.max(_maxKeyFrame, _getDocumentHeight());
  697. //Now process all data-end keyframes.
  698. skrollableIndex = 0;
  699. skrollablesLength = _skrollables.length;
  700. for(; skrollableIndex < skrollablesLength; skrollableIndex++) {
  701. skrollable = _skrollables[skrollableIndex];
  702. keyFrames = skrollable.keyFrames;
  703. keyFrameIndex = 0;
  704. keyFramesLength = keyFrames.length;
  705. for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) {
  706. kf = keyFrames[keyFrameIndex];
  707. constantValue = processedConstants[kf.constant] || 0;
  708. if(kf.isEnd) {
  709. kf.frame = _maxKeyFrame - kf.offset + constantValue;
  710. }
  711. }
  712. skrollable.keyFrames.sort(_keyFrameComparator);
  713. }
  714. };
  715. /**
  716. * Calculates and sets the style properties for the element at the given frame.
  717. * @param fakeFrame The frame to render at when smooth scrolling is enabled.
  718. * @param actualFrame The actual frame we are at.
  719. */
  720. var _calcSteps = function(fakeFrame, actualFrame) {
  721. //Iterate over all skrollables.
  722. var skrollableIndex = 0;
  723. var skrollablesLength = _skrollables.length;
  724. for(; skrollableIndex < skrollablesLength; skrollableIndex++) {
  725. var skrollable = _skrollables[skrollableIndex];
  726. var element = skrollable.element;
  727. var frame = skrollable.smoothScrolling ? fakeFrame : actualFrame;
  728. var frames = skrollable.keyFrames;
  729. var framesLength = frames.length;
  730. var firstFrame = frames[0];
  731. var lastFrame = frames[frames.length - 1];
  732. var beforeFirst = frame < firstFrame.frame;
  733. var afterLast = frame > lastFrame.frame;
  734. var firstOrLastFrame = beforeFirst ? firstFrame : lastFrame;
  735. var emitEvents = skrollable.emitEvents;
  736. var lastFrameIndex = skrollable.lastFrameIndex;
  737. var key;
  738. var value;
  739. //If we are before/after the first/last frame, set the styles according to the given edge strategy.
  740. if(beforeFirst || afterLast) {
  741. //Check if we already handled this edge case last time.
  742. //Note: using setScrollTop it's possible that we jumped from one edge to the other.
  743. if(beforeFirst && skrollable.edge === -1 || afterLast && skrollable.edge === 1) {
  744. continue;
  745. }
  746. //Add the skrollr-before or -after class.
  747. if(beforeFirst) {
  748. _updateClass(element, [SKROLLABLE_BEFORE_CLASS], [SKROLLABLE_AFTER_CLASS, SKROLLABLE_BETWEEN_CLASS]);
  749. //This handles the special case where we exit the first keyframe.
  750. if(emitEvents && lastFrameIndex > -1) {
  751. _emitEvent(element, firstFrame.eventType, _direction);
  752. skrollable.lastFrameIndex = -1;
  753. }
  754. } else {
  755. _updateClass(element, [SKROLLABLE_AFTER_CLASS], [SKROLLABLE_BEFORE_CLASS, SKROLLABLE_BETWEEN_CLASS]);
  756. //This handles the special case where we exit the last keyframe.
  757. if(emitEvents && lastFrameIndex < framesLength) {
  758. _emitEvent(element, lastFrame.eventType, _direction);
  759. skrollable.lastFrameIndex = framesLength;
  760. }
  761. }
  762. //Remember that we handled the edge case (before/after the first/last keyframe).
  763. skrollable.edge = beforeFirst ? -1 : 1;
  764. switch(skrollable.edgeStrategy) {
  765. case 'reset':
  766. _reset(element);
  767. continue;
  768. case 'ease':
  769. //Handle this case like it would be exactly at first/last keyframe and just pass it on.
  770. frame = firstOrLastFrame.frame;
  771. break;
  772. default:
  773. case 'set':
  774. var props = firstOrLastFrame.props;
  775. for(key in props) {
  776. if(hasProp.call(props, key)) {
  777. value = _interpolateString(props[key].value);
  778. //Set style or attribute.
  779. if(key.indexOf('@') === 0) {
  780. element.setAttribute(key.substr(1), value);
  781. } else {
  782. skrollr.setStyle(element, key, value);
  783. }
  784. }
  785. }
  786. continue;
  787. }
  788. } else {
  789. //Did we handle an edge last time?
  790. if(skrollable.edge !== 0) {
  791. _updateClass(element, [SKROLLABLE_CLASS, SKROLLABLE_BETWEEN_CLASS], [SKROLLABLE_BEFORE_CLASS, SKROLLABLE_AFTER_CLASS]);
  792. skrollable.edge = 0;
  793. }
  794. }
  795. //Find out between which two key frames we are right now.
  796. var keyFrameIndex = 0;
  797. for(; keyFrameIndex < framesLength - 1; keyFrameIndex++) {
  798. if(frame >= frames[keyFrameIndex].frame && frame <= frames[keyFrameIndex + 1].frame) {
  799. var left = frames[keyFrameIndex];
  800. var right = frames[keyFrameIndex + 1];
  801. for(key in left.props) {
  802. if(hasProp.call(left.props, key)) {
  803. var progress = (frame - left.frame) / (right.frame - left.frame);
  804. //Transform the current progress using the given easing function.
  805. progress = left.props[key].easing(progress);
  806. //Interpolate between the two values
  807. value = _calcInterpolation(left.props[key].value, right.props[key].value, progress);
  808. value = _interpolateString(value);
  809. //Set style or attribute.
  810. if(key.indexOf('@') === 0) {
  811. element.setAttribute(key.substr(1), value);
  812. } else {
  813. skrollr.setStyle(element, key, value);
  814. }
  815. }
  816. }
  817. //Are events enabled on this element?
  818. //This code handles the usual cases of scrolling through different keyframes.
  819. //The special cases of before first and after last keyframe are handled above.
  820. if(emitEvents) {
  821. //Did we pass a new keyframe?
  822. if(lastFrameIndex !== keyFrameIndex) {
  823. if(_direction === 'down') {
  824. _emitEvent(element, left.eventType, _direction);
  825. } else {
  826. _emitEvent(element, right.eventType, _direction);
  827. }
  828. skrollable.lastFrameIndex = keyFrameIndex;
  829. }
  830. }
  831. break;
  832. }
  833. }
  834. }
  835. };
  836. /**
  837. * Renders all elements.
  838. */
  839. var _render = function() {
  840. if(_requestReflow) {
  841. _requestReflow = false;
  842. _reflow();
  843. }
  844. //We may render something else than the actual scrollbar position.
  845. var renderTop = _instance.getScrollTop();
  846. //If there's an animation, which ends in current render call, call the callback after rendering.
  847. var afterAnimationCallback;
  848. var now = _now();
  849. var progress;
  850. //Before actually rendering handle the scroll animation, if any.
  851. if(_scrollAnimation) {
  852. //It's over
  853. if(now >= _scrollAnimation.endTime) {
  854. renderTop = _scrollAnimation.targetTop;
  855. afterAnimationCallback = _scrollAnimation.done;
  856. _scrollAnimation = undefined;
  857. } else {
  858. //Map the current progress to the new progress using given easing function.
  859. progress = _scrollAnimation.easing((now - _scrollAnimation.startTime) / _scrollAnimation.duration);
  860. renderTop = (_scrollAnimation.startTop + progress * _scrollAnimation.topDiff) | 0;
  861. }
  862. _instance.setScrollTop(renderTop, true);
  863. }
  864. //Smooth scrolling only if there's no animation running and if we're not forcing the rendering.
  865. else if(!_forceRender) {
  866. var smoothScrollingDiff = _smoothScrolling.targetTop - renderTop;
  867. //The user scrolled, start new smooth scrolling.
  868. if(smoothScrollingDiff) {
  869. _smoothScrolling = {
  870. startTop: _lastTop,
  871. topDiff: renderTop - _lastTop,
  872. targetTop: renderTop,
  873. startTime: _lastRenderCall,
  874. endTime: _lastRenderCall + _smoothScrollingDuration
  875. };
  876. }
  877. //Interpolate the internal scroll position (not the actual scrollbar).
  878. if(now <= _smoothScrolling.endTime) {
  879. //Map the current progress to the new progress using easing function.
  880. progress = easings.sqrt((now - _smoothScrolling.startTime) / _smoothScrollingDuration);
  881. renderTop = (_smoothScrolling.startTop + progress * _smoothScrolling.topDiff) | 0;
  882. }
  883. }
  884. //Did the scroll position even change?
  885. if(_forceRender || _lastTop !== renderTop) {
  886. //Remember in which direction are we scrolling?
  887. _direction = (renderTop > _lastTop) ? 'down' : (renderTop < _lastTop ? 'up' : _direction);
  888. _forceRender = false;
  889. var listenerParams = {
  890. curTop: renderTop,
  891. lastTop: _lastTop,
  892. maxTop: _maxKeyFrame,
  893. direction: _direction
  894. };
  895. //Tell the listener we are about to render.
  896. var continueRendering = _listeners.beforerender && _listeners.beforerender.call(_instance, listenerParams);
  897. //The beforerender listener function is able the cancel rendering.
  898. if(continueRendering !== false) {
  899. //Now actually interpolate all the styles.
  900. _calcSteps(renderTop, _instance.getScrollTop());
  901. //That's were we actually "scroll" on mobile.
  902. if(_isMobile && _skrollrBody) {
  903. //Set the transform ("scroll it").
  904. skrollr.setStyle(_skrollrBody, 'transform', 'translate(0, ' + -(_mobileOffset) + 'px) ' + _translateZ);
  905. }
  906. //Remember when we last rendered.
  907. _lastTop = renderTop;
  908. if(_listeners.render) {
  909. _listeners.render.call(_instance, listenerParams);
  910. }
  911. }
  912. if(afterAnimationCallback) {
  913. afterAnimationCallback.call(_instance, false);
  914. }
  915. }
  916. _lastRenderCall = now;
  917. };
  918. /**
  919. * Parses the properties for each key frame of the given skrollable.
  920. */
  921. var _parseProps = function(skrollable) {
  922. //Iterate over all key frames
  923. var keyFrameIndex = 0;
  924. var keyFramesLength = skrollable.keyFrames.length;
  925. for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) {
  926. var frame = skrollable.keyFrames[keyFrameIndex];
  927. var easing;
  928. var value;
  929. var prop;
  930. var props = {};
  931. var match;
  932. while((match = rxPropValue.exec(frame.props)) !== null) {
  933. prop = match[1];
  934. value = match[2];
  935. easing = prop.match(rxPropEasing);
  936. //Is there an easing specified for this prop?
  937. if(easing !== null) {
  938. prop = easing[1];
  939. easing = easing[2];
  940. } else {
  941. easing = DEFAULT_EASING;
  942. }
  943. //Exclamation point at first position forces the value to be taken literal.
  944. value = value.indexOf('!') ? _parseProp(value) : [value.slice(1)];
  945. //Save the prop for this key frame with his value and easing function
  946. props[prop] = {
  947. value: value,
  948. easing: easings[easing]
  949. };
  950. }
  951. frame.props = props;
  952. }
  953. };
  954. /**
  955. * Parses a value extracting numeric values and generating a format string
  956. * for later interpolation of the new values in old string.
  957. *
  958. * @param val The CSS value to be parsed.
  959. * @return Something like ["rgba(?%,?%, ?%,?)", 100, 50, 0, .7]
  960. * where the first element is the format string later used
  961. * and all following elements are the numeric value.
  962. */
  963. var _parseProp = function(val) {
  964. var numbers = [];
  965. //One special case, where floats don't work.
  966. //We replace all occurences of rgba colors
  967. //which don't use percentage notation with the percentage notation.
  968. rxRGBAIntegerColor.lastIndex = 0;
  969. val = val.replace(rxRGBAIntegerColor, function(rgba) {
  970. return rgba.replace(rxNumericValue, function(n) {
  971. return n / 255 * 100 + '%';
  972. });
  973. });
  974. //Handle prefixing of "gradient" values.
  975. //For now only the prefixed value will be set. Unprefixed isn't supported anyway.
  976. if(theDashedCSSPrefix) {
  977. rxGradient.lastIndex = 0;
  978. val = val.replace(rxGradient, function(s) {
  979. return theDashedCSSPrefix + s;
  980. });
  981. }
  982. //Now parse ANY number inside this string and create a format string.
  983. val = val.replace(rxNumericValue, function(n) {
  984. numbers.push(+n);
  985. return '{?}';
  986. });
  987. //Add the formatstring as first value.
  988. numbers.unshift(val);
  989. return numbers;
  990. };
  991. /**
  992. * Fills the key frames with missing left and right hand properties.
  993. * If key frame 1 has property X and key frame 2 is missing X,
  994. * but key frame 3 has X again, then we need to assign X to key frame 2 too.
  995. *
  996. * @param sk A skrollable.
  997. */
  998. var _fillProps = function(sk) {
  999. //Will collect the properties key frame by key frame
  1000. var propList = {};
  1001. var keyFrameIndex;
  1002. var keyFramesLength;
  1003. //Iterate over all key frames from left to right
  1004. keyFrameIndex = 0;
  1005. keyFramesLength = sk.keyFrames.length;
  1006. for(; keyFrameIndex < keyFramesLength; keyFrameIndex++) {
  1007. _fillPropForFrame(sk.keyFrames[keyFrameIndex], propList);
  1008. }
  1009. //Now do the same from right to fill the last gaps
  1010. propList = {};
  1011. //Iterate over all key frames from right to left
  1012. keyFrameIndex = sk.keyFrames.length - 1;
  1013. for(; keyFrameIndex >= 0; keyFrameIndex--) {
  1014. _fillPropForFrame(sk.keyFrames[keyFrameIndex], propList);
  1015. }
  1016. };
  1017. var _fillPropForFrame = function(frame, propList) {
  1018. var key;
  1019. //For each key frame iterate over all right hand properties and assign them,
  1020. //but only if the current key frame doesn't have the property by itself
  1021. for(key in propList) {
  1022. //The current frame misses this property, so assign it.
  1023. if(!hasProp.call(frame.props, key)) {
  1024. frame.props[key] = propList[key];
  1025. }
  1026. }
  1027. //Iterate over all props of the current frame and collect them
  1028. for(key in frame.props) {
  1029. propList[key] = frame.props[key];
  1030. }
  1031. };
  1032. /**
  1033. * Calculates the new values for two given values array.
  1034. */
  1035. var _calcInterpolation = function(val1, val2, progress) {
  1036. var valueIndex;
  1037. var val1Length = val1.length;
  1038. //They both need to have the same length
  1039. if(val1Length !== val2.length) {
  1040. throw 'Can\'t interpolate between "' + val1[0] + '" and "' + val2[0] + '"';
  1041. }
  1042. //Add the format string as first element.
  1043. var interpolated = [val1[0]];
  1044. valueIndex = 1;
  1045. for(; valueIndex < val1Length; valueIndex++) {
  1046. //That's the line where the two numbers are actually interpolated.
  1047. interpolated[valueIndex] = val1[valueIndex] + ((val2[valueIndex] - val1[valueIndex]) * progress);
  1048. }
  1049. return interpolated;
  1050. };
  1051. /**
  1052. * Interpolates the numeric values into the format string.
  1053. */
  1054. var _interpolateString = function(val) {
  1055. var valueIndex = 1;
  1056. rxInterpolateString.lastIndex = 0;
  1057. return val[0].replace(rxInterpolateString, function() {
  1058. return val[valueIndex++];
  1059. });
  1060. };
  1061. /**
  1062. * Resets the class and style attribute to what it was before skrollr manipulated the element.
  1063. * Also remembers the values it had before reseting, in order to undo the reset.
  1064. */
  1065. var _reset = function(elements, undo) {
  1066. //We accept a single element or an array of elements.
  1067. elements = [].concat(elements);
  1068. var skrollable;
  1069. var element;
  1070. var elementsIndex = 0;
  1071. var elementsLength = elements.length;
  1072. for(; elementsIndex < elementsLength; elementsIndex++) {
  1073. element = elements[elementsIndex];
  1074. skrollable = _skrollables[element[SKROLLABLE_ID_DOM_PROPERTY]];
  1075. //Couldn't find the skrollable for this DOM element.
  1076. if(!skrollable) {
  1077. continue;
  1078. }
  1079. if(undo) {
  1080. //Reset class and style to the "dirty" (set by skrollr) values.
  1081. element.style.cssText = skrollable.dirtyStyleAttr;
  1082. _updateClass(element, skrollable.dirtyClassAttr);
  1083. } else {
  1084. //Remember the "dirty" (set by skrollr) class and style.
  1085. skrollable.dirtyStyleAttr = element.style.cssText;
  1086. skrollable.dirtyClassAttr = _getClass(element);
  1087. //Reset class and style to what it originally was.
  1088. element.style.cssText = skrollable.styleAttr;
  1089. _updateClass(element, skrollable.classAttr);
  1090. }
  1091. }
  1092. };
  1093. /**
  1094. * Detects support for 3d transforms by applying it to the skrollr-body.
  1095. */
  1096. var _detect3DTransforms = function() {
  1097. _translateZ = 'translateZ(0)';
  1098. skrollr.setStyle(_skrollrBody, 'transform', _translateZ);
  1099. var computedStyle = getStyle(_skrollrBody);
  1100. var computedTransform = computedStyle.getPropertyValue('transform');
  1101. var computedTransformWithPrefix = computedStyle.getPropertyValue(theDashedCSSPrefix + 'transform');
  1102. var has3D = (computedTransform && computedTransform !== 'none') || (computedTransformWithPrefix && computedTransformWithPrefix !== 'none');
  1103. if(!has3D) {
  1104. _translateZ = '';
  1105. }
  1106. };
  1107. /**
  1108. * Set the CSS property on the given element. Sets prefixed properties as well.
  1109. */
  1110. skrollr.setStyle = function(el, prop, val) {
  1111. var style = el.style;
  1112. //Camel case.
  1113. prop = prop.replace(rxCamelCase, rxCamelCaseFn).replace('-', '');
  1114. //Make sure z-index gets a <integer>.
  1115. //This is the only <integer> case we need to handle.
  1116. if(prop === 'zIndex') {
  1117. if(isNaN(val)) {
  1118. //If it's not a number, don't touch it.
  1119. //It could for example be "auto" (#351).
  1120. style[prop] = val;
  1121. } else {
  1122. //Floor the number.
  1123. style[prop] = '' + (val | 0);
  1124. }
  1125. }
  1126. //#64: "float" can't be set across browsers. Needs to use "cssFloat" for all except IE.
  1127. else if(prop === 'float') {
  1128. style.styleFloat = style.cssFloat = val;
  1129. }
  1130. else {
  1131. //Need try-catch for old IE.
  1132. try {
  1133. //Set prefixed property if there's a prefix.
  1134. if(theCSSPrefix) {
  1135. style[theCSSPrefix + prop.slice(0,1).toUpperCase() + prop.slice(1)] = val;
  1136. }
  1137. //Set unprefixed.
  1138. style[prop] = val;
  1139. } catch(ignore) {}
  1140. }
  1141. };
  1142. /**
  1143. * Cross browser event handling.
  1144. */
  1145. var _addEvent = skrollr.addEvent = function(element, names, callback) {
  1146. var intermediate = function(e) {
  1147. //Normalize IE event stuff.
  1148. e = e || window.event;
  1149. if(!e.target) {
  1150. e.target = e.srcElement;
  1151. }
  1152. if(!e.preventDefault) {
  1153. e.preventDefault = function() {
  1154. e.returnValue = false;
  1155. e.defaultPrevented = true;
  1156. };
  1157. }
  1158. return callback.call(this, e);
  1159. };
  1160. names = names.split(' ');
  1161. var name;
  1162. var nameCounter = 0;
  1163. var namesLength = names.length;
  1164. for(; nameCounter < namesLength; nameCounter++) {
  1165. name = names[nameCounter];
  1166. if(element.addEventListener) {
  1167. element.addEventListener(name, callback, false);
  1168. } else {
  1169. element.attachEvent('on' + name, intermediate);
  1170. }
  1171. //Remember the events to be able to flush them later.
  1172. _registeredEvents.push({
  1173. element: element,
  1174. name: name,
  1175. listener: callback
  1176. });
  1177. }
  1178. };
  1179. var _removeEvent = skrollr.removeEvent = function(element, names, callback) {
  1180. names = names.split(' ');
  1181. var nameCounter = 0;
  1182. var namesLength = names.length;
  1183. for(; nameCounter < namesLength; nameCounter++) {
  1184. if(element.removeEventListener) {
  1185. element.removeEventListener(names[nameCounter], callback, false);
  1186. } else {
  1187. element.detachEvent('on' + names[nameCounter], callback);
  1188. }
  1189. }
  1190. };
  1191. var _removeAllEvents = function() {
  1192. var eventData;
  1193. var eventCounter = 0;
  1194. var eventsLength = _registeredEvents.length;
  1195. for(; eventCounter < eventsLength; eventCounter++) {
  1196. eventData = _registeredEvents[eventCounter];
  1197. _removeEvent(eventData.element, eventData.name, eventData.listener);
  1198. }
  1199. _registeredEvents = [];
  1200. };
  1201. var _emitEvent = function(element, name, direction) {
  1202. if(_listeners.keyframe) {
  1203. _listeners.keyframe.call(_instance, element, name, direction);
  1204. }
  1205. };
  1206. var _reflow = function() {
  1207. var pos = _instance.getScrollTop();
  1208. //Will be recalculated by _updateDependentKeyFrames.
  1209. _maxKeyFrame = 0;
  1210. if(_forceHeight && !_isMobile) {
  1211. //un-"force" the height to not mess with the calculations in _updateDependentKeyFrames (#216).
  1212. body.style.height = '';
  1213. }
  1214. _updateDependentKeyFrames();
  1215. if(_forceHeight && !_isMobile) {
  1216. //"force" the height.
  1217. body.style.height = (_maxKeyFrame + documentElement.clientHeight) + 'px';
  1218. }
  1219. //The scroll offset may now be larger than needed (on desktop the browser/os prevents scrolling farther than the bottom).
  1220. if(_isMobile) {
  1221. _instance.setScrollTop(Math.min(_instance.getScrollTop(), _maxKeyFrame));
  1222. } else {
  1223. //Remember and reset the scroll pos (#217).
  1224. _instance.setScrollTop(pos, true);
  1225. }
  1226. _forceRender = true;
  1227. };
  1228. /*
  1229. * Returns a copy of the constants object where all functions and strings have been evaluated.
  1230. */
  1231. var _processConstants = function() {
  1232. var viewportHeight = documentElement.clientHeight;
  1233. var copy = {};
  1234. var prop;
  1235. var value;
  1236. for(prop in _constants) {
  1237. value = _constants[prop];
  1238. if(typeof value === 'function') {
  1239. value = value.call(_instance);
  1240. }
  1241. //Percentage offset.
  1242. else if((/p$/).test(value)) {
  1243. value = (value.slice(0, -1) / 100) * viewportHeight;
  1244. }
  1245. copy[prop] = value;
  1246. }
  1247. return copy;
  1248. };
  1249. /*
  1250. * Returns the height of the document.
  1251. */
  1252. var _getDocumentHeight = function() {
  1253. var skrollrBodyHeight = 0;
  1254. var bodyHeight;
  1255. if(_skrollrBody) {
  1256. skrollrBodyHeight = Math.max(_skrollrBody.offsetHeight, _skrollrBody.scrollHeight);
  1257. }
  1258. bodyHeight = Math.max(skrollrBodyHeight, body.scrollHeight, body.offsetHeight, documentElement.scrollHeight, documentElement.offsetHeight, documentElement.clientHeight);
  1259. return bodyHeight - documentElement.clientHeight;
  1260. };
  1261. /**
  1262. * Returns a string of space separated classnames for the current element.
  1263. * Works with SVG as well.
  1264. */
  1265. var _getClass = function(element) {
  1266. var prop = 'className';
  1267. //SVG support by using className.baseVal instead of just className.
  1268. if(window.SVGElement && element instanceof window.SVGElement) {
  1269. element = element[prop];
  1270. prop = 'baseVal';
  1271. }
  1272. return element[prop];
  1273. };
  1274. /**
  1275. * Adds and removes a CSS classes.
  1276. * Works with SVG as well.
  1277. * add and remove are arrays of strings,
  1278. * or if remove is ommited add is a string and overwrites all classes.
  1279. */
  1280. var _updateClass = function(element, add, remove) {
  1281. var prop = 'className';
  1282. //SVG support by using className.baseVal instead of just className.
  1283. if(window.SVGElement && element instanceof window.SVGElement) {
  1284. element = element[prop];
  1285. prop = 'baseVal';
  1286. }
  1287. //When remove is ommited, we want to overwrite/set the classes.
  1288. if(remove === undefined) {
  1289. element[prop] = add;
  1290. return;
  1291. }
  1292. //Cache current classes. We will work on a string before passing back to DOM.
  1293. var val = element[prop];
  1294. //All classes to be removed.
  1295. var classRemoveIndex = 0;
  1296. var removeLength = remove.length;
  1297. for(; classRemoveIndex < removeLength; classRemoveIndex++) {
  1298. val = _untrim(val).replace(_untrim(remove[classRemoveIndex]), ' ');
  1299. }
  1300. val = _trim(val);
  1301. //All classes to be added.
  1302. var classAddIndex = 0;
  1303. var addLength = add.length;
  1304. for(; classAddIndex < addLength; classAddIndex++) {
  1305. //Only add if el not already has class.
  1306. if(_untrim(val).indexOf(_untrim(add[classAddIndex])) === -1) {
  1307. val += ' ' + add[classAddIndex];
  1308. }
  1309. }
  1310. element[prop] = _trim(val);
  1311. };
  1312. var _trim = function(a) {
  1313. return a.replace(rxTrim, '');
  1314. };
  1315. /**
  1316. * Adds a space before and after the string.
  1317. */
  1318. var _untrim = function(a) {
  1319. return ' ' + a + ' ';
  1320. };
  1321. var _now = Date.now || function() {
  1322. return +new Date();
  1323. };
  1324. var _keyFrameComparator = function(a, b) {
  1325. return a.frame - b.frame;
  1326. };
  1327. /*
  1328. * Private variables.
  1329. */
  1330. //Singleton
  1331. var _instance;
  1332. /*
  1333. A list of all elements which should be animated associated with their the metadata.
  1334. Exmaple skrollable with two key frames animating from 100px width to 20px:
  1335. skrollable = {
  1336. element: <the DOM element>,
  1337. styleAttr: <style attribute of the element before skrollr>,
  1338. classAttr: <class attribute of the element before skrollr>,
  1339. keyFrames: [
  1340. {
  1341. frame: 100,
  1342. props: {
  1343. width: {
  1344. value: ['{?}px', 100],
  1345. easing: <reference to easing function>
  1346. }
  1347. },
  1348. mode: "absolute"
  1349. },
  1350. {
  1351. frame: 200,
  1352. props: {
  1353. width: {
  1354. value: ['{?}px', 20],
  1355. easing: <reference to easing function>
  1356. }
  1357. },
  1358. mode: "absolute"
  1359. }
  1360. ]
  1361. };
  1362. */
  1363. var _skrollables;
  1364. var _skrollrBody;
  1365. var _listeners;
  1366. var _forceHeight;
  1367. var _maxKeyFrame = 0;
  1368. var _scale = 1;
  1369. var _constants;
  1370. var _mobileDeceleration;
  1371. //Current direction (up/down).
  1372. var _direction = 'down';
  1373. //The last top offset value. Needed to determine direction.
  1374. var _lastTop = -1;
  1375. //The last time we called the render method (doesn't mean we rendered!).
  1376. var _lastRenderCall = _now();
  1377. //For detecting if it actually resized (#271).
  1378. var _lastViewportWidth = 0;
  1379. var _lastViewportHeight = 0;
  1380. var _requestReflow = false;
  1381. //Will contain data about a running scrollbar animation, if any.
  1382. var _scrollAnimation;
  1383. var _smoothScrollingEnabled;
  1384. var _smoothScrollingDuration;
  1385. //Will contain settins for smooth scrolling if enabled.
  1386. var _smoothScrolling;
  1387. //Can be set by any operation/event to force rendering even if the scrollbar didn't move.
  1388. var _forceRender;
  1389. //Each skrollable gets an unique ID incremented for each skrollable.
  1390. //The ID is the index in the _skrollables array.
  1391. var _skrollableIdCounter = 0;
  1392. var _edgeStrategy;
  1393. //Mobile specific vars. Will be stripped by UglifyJS when not in use.
  1394. var _isMobile = false;
  1395. //The virtual scroll offset when using mobile scrolling.
  1396. var _mobileOffset = 0;
  1397. //If the browser supports 3d transforms, this will be filled with 'translateZ(0)' (empty string otherwise).
  1398. var _translateZ;
  1399. //Will contain data about registered events by skrollr.
  1400. var _registeredEvents = [];
  1401. //Animation frame id returned by RequestAnimationFrame (or timeout when RAF is not supported).
  1402. var _animFrame;
  1403. //Expose skrollr as either a global variable or a require.js module.
  1404. if(typeof define === 'function' && define.amd) {
  1405. define([], function () {
  1406. return skrollr;
  1407. });
  1408. } else if (typeof module !== 'undefined' && module.exports) {
  1409. module.exports = skrollr;
  1410. } else {
  1411. window.skrollr = skrollr;
  1412. }
  1413. }(window, document));