skrollr.js 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771
  1. /*!
  2. * skrollr core
  3. *
  4. * Alexander Prinzhorn - https://github.com/Prinzhorn/skrollr
  5. *
  6. * Free to use under terms of MIT license
  7. */
  8. (function(window, document, undefined) {
  9. 'use strict';
  10. /*
  11. * Global api.
  12. */
  13. var skrollr = {
  14. get: function() {
  15. return _instance;
  16. },
  17. //Main entry point.
  18. init: function(options) {
  19. return _instance || new Skrollr(options);
  20. },
  21. VERSION: '0.6.26'
  22. };
  23. //Minify optimization.
  24. var hasProp = Object.prototype.hasOwnProperty;
  25. var Math = window.Math;
  26. var getStyle = window.getComputedStyle;
  27. //They will be filled when skrollr gets initialized.
  28. var documentElement;
  29. var body;
  30. var EVENT_TOUCHSTART = 'touchstart';
  31. var EVENT_TOUCHMOVE = 'touchmove';
  32. var EVENT_TOUCHCANCEL = 'touchcancel';
  33. var EVENT_TOUCHEND = 'touchend';
  34. var SKROLLABLE_CLASS = 'skrollable';
  35. var SKROLLABLE_BEFORE_CLASS = SKROLLABLE_CLASS + '-before';
  36. var SKROLLABLE_BETWEEN_CLASS = SKROLLABLE_CLASS + '-between';
  37. var SKROLLABLE_AFTER_CLASS = SKROLLABLE_CLASS + '-after';
  38. var SKROLLR_CLASS = 'skrollr';
  39. var NO_SKROLLR_CLASS = 'no-' + SKROLLR_CLASS;
  40. var SKROLLR_DESKTOP_CLASS = SKROLLR_CLASS + '-desktop';
  41. var SKROLLR_MOBILE_CLASS = SKROLLR_CLASS + '-mobile';
  42. var DEFAULT_EASING = 'linear';
  43. var DEFAULT_DURATION = 1000;//ms
  44. var DEFAULT_MOBILE_DECELERATION = 0.004;//pixel/ms²
  45. var DEFAULT_SMOOTH_SCROLLING_DURATION = 200;//ms
  46. var ANCHOR_START = 'start';
  47. var ANCHOR_END = 'end';
  48. var ANCHOR_CENTER = 'center';
  49. var ANCHOR_BOTTOM = 'bottom';
  50. //The property which will be added to the DOM element to hold the ID of the skrollable.
  51. var SKROLLABLE_ID_DOM_PROPERTY = '___skrollable_id';
  52. var rxTouchIgnoreTags = /^(?:input|textarea|button|select)$/i;
  53. var rxTrim = /^\s+|\s+$/g;
  54. //Find all data-attributes. data-[_constant]-[offset]-[anchor]-[anchor].
  55. var rxKeyframeAttribute = /^data(?:-(_\w+))?(?:-?(-?\d*\.?\d+p?))?(?:-?(start|end|top|center|bottom))?(?:-?(top|center|bottom))?$/;
  56. var rxPropValue = /\s*(@?[\w\-\[\]]+)\s*:\s*(.+?)\s*(?:;|$)/gi;
  57. //Easing function names follow the property in square brackets.
  58. var rxPropEasing = /^(@?[a-z\-]+)\[(\w+)\]$/;
  59. var rxCamelCase = /-([a-z0-9_])/g;
  60. var rxCamelCaseFn = function(str, letter) {
  61. return letter.toUpperCase();
  62. };
  63. //Numeric values with optional sign.
  64. var rxNumericValue = /[\-+]?[\d]*\.?[\d]+/g;
  65. //Used to replace occurences of {?} with a number.
  66. var rxInterpolateString = /\{\?\}/g;
  67. //Finds rgb(a) colors, which don't use the percentage notation.
  68. var rxRGBAIntegerColor = /rgba?\(\s*-?\d+\s*,\s*-?\d+\s*,\s*-?\d+/g;
  69. //Finds all gradients.
  70. var rxGradient = /[a-z\-]+-gradient/g;
  71. //Vendor prefix. Will be set once skrollr gets initialized.
  72. var theCSSPrefix = '';
  73. var theDashedCSSPrefix = '';
  74. //Will be called once (when skrollr gets initialized).
  75. var detectCSSPrefix = function() {
  76. //Only relevant prefixes. May be extended.
  77. //Could be dangerous if there will ever be a CSS property which actually starts with "ms". Don't hope so.
  78. var rxPrefixes = /^(?:O|Moz|webkit|ms)|(?:-(?:o|moz|webkit|ms)-)/;
  79. //Detect prefix for current browser by finding the first property using a prefix.
  80. if(!getStyle) {
  81. return;
  82. }
  83. var style = getStyle(body, null);
  84. for(var k in style) {
  85. //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.
  86. theCSSPrefix = (k.match(rxPrefixes) || (+k == k && style[k].match(rxPrefixes)));
  87. if(theCSSPrefix) {
  88. break;
  89. }
  90. }
  91. //Did we even detect a prefix?
  92. if(!theCSSPrefix) {
  93. theCSSPrefix = theDashedCSSPrefix = '';
  94. return;
  95. }
  96. theCSSPrefix = theCSSPrefix[0];
  97. //We could have detected either a dashed prefix or this camelCaseish-inconsistent stuff.
  98. if(theCSSPrefix.slice(0,1) === '-') {
  99. theDashedCSSPrefix = theCSSPrefix;
  100. //There's no logic behind these. Need a look up.
  101. theCSSPrefix = ({
  102. '-webkit-': 'webkit',
  103. '-moz-': 'Moz',
  104. '-ms-': 'ms',
  105. '-o-': 'O'
  106. })[theCSSPrefix];
  107. } else {
  108. theDashedCSSPrefix = '-' + theCSSPrefix.toLowerCase() + '-';
  109. }
  110. };
  111. var polyfillRAF = function() {
  112. var requestAnimFrame = window.requestAnimationFrame || window[theCSSPrefix.toLowerCase() + 'RequestAnimationFrame'];
  113. var lastTime = _now();
  114. if(_isMobile || !requestAnimFrame) {
  115. requestAnimFrame = function(callback) {
  116. //How long did it take to render?
  117. var deltaTime = _now() - lastTime;
  118. var delay = Math.max(0, 1000 / 60 - deltaTime);
  119. return window.setTimeout(function() {
  120. lastTime = _now();
  121. callback();
  122. }, delay);
  123. };
  124. }
  125. return requestAnimFrame;
  126. };
  127. var polyfillCAF = function() {
  128. var cancelAnimFrame = window.cancelAnimationFrame || window[theCSSPrefix.toLowerCase() + 'CancelAnimationFrame'];
  129. if(_isMobile || !cancelAnimFrame) {
  130. cancelAnimFrame = function(timeout) {
  131. return window.clearTimeout(timeout);
  132. };
  133. }
  134. return cancelAnimFrame;
  135. };
  136. //Built-in easing functions.
  137. var easings = {
  138. begin: function() {
  139. return 0;
  140. },
  141. end: function() {
  142. return 1;
  143. },
  144. linear: function(p) {
  145. return p;
  146. },
  147. quadratic: function(p) {
  148. return p * p;
  149. },
  150. cubic: function(p) {
  151. return p * p * p;
  152. },
  153. swing: function(p) {
  154. return (-Math.cos(p * Math.PI) / 2) + 0.5;
  155. },
  156. sqrt: function(p) {
  157. return Math.sqrt(p);
  158. },
  159. outCubic: function(p) {
  160. return (Math.pow((p - 1), 3) + 1);
  161. },
  162. //see https://www.desmos.com/calculator/tbr20s8vd2 for how I did this
  163. bounce: function(p) {
  164. var a;
  165. if(p <= 0.5083) {
  166. a = 3;
  167. } else if(p <= 0.8489) {
  168. a = 9;
  169. } else if(p <= 0.96208) {
  170. a = 27;
  171. } else if(p <= 0.99981) {
  172. a = 91;
  173. } else {
  174. return 1;
  175. }
  176. return 1 - Math.abs(3 * Math.cos(p * a * 1.028) / a);
  177. }
  178. };
  179. /**
  180. * Constructor.
  181. */
  182. function Skrollr(options) {
  183. documentElement = document.documentElement;
  184. body = document.body;
  185. detectCSSPrefix();
  186. _instance = this;
  187. options = options || {};
  188. _constants = options.constants || {};
  189. //We allow defining custom easings or overwrite existing.
  190. if(options.easing) {
  191. for(var e in options.easing) {
  192. easings[e] = options.easing[e];
  193. }
  194. }
  195. _edgeStrategy = options.edgeStrategy || 'set';
  196. _listeners = {
  197. //Function to be called right before rendering.
  198. beforerender: options.beforerender,
  199. //Function to be called right after finishing rendering.
  200. render: options.render,
  201. //Function to be called whenever an element with the `data-emit-events` attribute passes a keyframe.
  202. keyframe: options.keyframe
  203. };
  204. //forceHeight is true by default
  205. _forceHeight = options.forceHeight !== false;
  206. if(_forceHeight) {
  207. _scale = options.scale || 1;
  208. }
  209. _mobileDeceleration = options.mobileDeceleration || DEFAULT_MOBILE_DECELERATION;
  210. _smoothScrollingEnabled = options.smoothScrolling !== false;
  211. _smoothScrollingDuration = options.smoothScrollingDuration || DEFAULT_SMOOTH_SCROLLING_DURATION;
  212. //Dummy object. Will be overwritten in the _render method when smooth scrolling is calculated.
  213. _smoothScrolling = {
  214. targetTop: _instance.getScrollTop()
  215. };
  216. //A custom check function may be passed.
  217. _isMobile = ((options.mobileCheck || function() {
  218. return (/Android|iPhone|iPad|iPod|BlackBerry/i).test(navigator.userAgent || navigator.vendor || window.opera);
  219. })());
  220. if(_isMobile) {
  221. _skrollrBody = document.getElementById('skrollr-body');
  222. //Detect 3d transform if there's a skrollr-body (only needed for #skrollr-body).
  223. if(_skrollrBody) {
  224. _detect3DTransforms();
  225. }
  226. _initMobile();
  227. _updateClass(documentElement, [SKROLLR_CLASS, SKROLLR_MOBILE_CLASS], [NO_SKROLLR_CLASS]);
  228. } else {
  229. _updateClass(documentElement, [SKROLLR_CLASS, SKROLLR_DESKTOP_CLASS], [NO_SKROLLR_CLASS]);
  230. }
  231. //Triggers parsing of elements and a first reflow.
  232. _instance.refresh();
  233. _addEvent(window, 'resize orientationchange', function() {
  234. var width = documentElement.clientWidth;
  235. var height = documentElement.clientHeight;
  236. //Only reflow if the size actually changed (#271).
  237. if(height !== _lastViewportHeight || width !== _lastViewportWidth) {
  238. _lastViewportHeight = height;
  239. _lastViewportWidth = width;
  240. _requestReflow = true;
  241. }
  242. });
  243. var requestAnimFrame = polyfillRAF();
  244. //Let's go.
  245. (function animloop(){
  246. _render();
  247. _animFrame = requestAnimFrame(animloop);
  248. }());
  249. return _instance;
  250. }
  251. /**
  252. * (Re)parses some or all elements.
  253. */
  254. Skrollr.prototype.refresh = function(elements) {
  255. var elementIndex;
  256. var elementsLength;
  257. var ignoreID = false;
  258. //Completely reparse anything without argument.
  259. if(elements === undefined) {
  260. //Ignore that some elements may already have a skrollable ID.
  261. ignoreID = true;
  262. _skrollables = [];
  263. _skrollableIdCounter = 0;
  264. elements = document.getElementsByTagName('*');
  265. } else if(elements.length === undefined) {
  266. //We also accept a single element as parameter.
  267. elements = [elements];
  268. }
  269. elementIndex = 0;
  270. elementsLength = elements.length;
  271. for(; elementIndex < elementsLength; elementIndex++) {
  272. var el = elements[elementIndex];
  273. var anchorTarget = el;
  274. var keyFrames = [];
  275. //If this particular element should be smooth scrolled.
  276. var smoothScrollThis = _smoothScrollingEnabled;
  277. //The edge strategy for this particular element.
  278. var edgeStrategy = _edgeStrategy;
  279. //If this particular element should emit keyframe events.
  280. var emitEvents = false;
  281. //If we're reseting the counter, remove any old element ids that may be hanging around.
  282. if(ignoreID && SKROLLABLE_ID_DOM_PROPERTY in el) {
  283. delete el[SKROLLABLE_ID_DOM_PROPERTY];
  284. }
  285. if(!el.attributes) {
  286. continue;
  287. }
  288. //Iterate over all attributes and search for key frame attributes.
  289. var attributeIndex = 0;
  290. var attributesLength = el.attributes.length;
  291. for (; attributeIndex < attributesLength; attributeIndex++) {
  292. var attr = el.attributes[attributeIndex];
  293. if(attr.name === 'data-anchor-target') {
  294. anchorTarget = document.querySelector(attr.value);
  295. if(anchorTarget === null) {
  296. throw 'Unable to find anchor target "' + attr.value + '"';
  297. }
  298. continue;
  299. }
  300. //Global smooth scrolling can be overridden by the element attribute.
  301. if(attr.name === 'data-smooth-scrolling') {
  302. smoothScrollThis = attr.value !== 'off';
  303. continue;
  304. }
  305. //Global edge strategy can be overridden by the element attribute.
  306. if(attr.name === 'data-edge-strategy') {
  307. edgeStrategy = attr.value;
  308. continue;
  309. }
  310. //Is this element tagged with the `data-emit-events` attribute?
  311. if(attr.name === 'data-emit-events') {
  312. emitEvents = true;
  313. continue;
  314. }
  315. var match = attr.name.match(rxKeyframeAttribute);
  316. if(match === null) {
  317. continue;
  318. }
  319. var kf = {
  320. props: attr.value,
  321. //Point back to the element as well.
  322. element: el,
  323. //The name of the event which this keyframe will fire, if emitEvents is
  324. eventType: attr.name.replace(rxCamelCase, rxCamelCaseFn)
  325. };
  326. keyFrames.push(kf);
  327. var constant = match[1];
  328. if(constant) {
  329. //Strip the underscore prefix.
  330. kf.constant = constant.substr(1);
  331. }
  332. //Get the key frame offset.
  333. var offset = match[2];
  334. //Is it a percentage offset?
  335. if(/p$/.test(offset)) {
  336. kf.isPercentage = true;
  337. kf.offset = (offset.slice(0, -1) | 0) / 100;
  338. } else {
  339. kf.offset = (offset | 0);
  340. }
  341. var anchor1 = match[3];
  342. //If second anchor is not set, the first will be taken for both.
  343. var anchor2 = match[4] || anchor1;
  344. //"absolute" (or "classic") mode, where numbers mean absolute scroll offset.
  345. if(!anchor1 || anchor1 === ANCHOR_START || anchor1 === ANCHOR_END) {
  346. kf.mode = 'absolute';
  347. //data-end needs to be calculated after all key frames are known.
  348. if(anchor1 === ANCHOR_END) {
  349. kf.isEnd = true;
  350. } else if(!kf.isPercentage) {
  351. //For data-start we can already set the key frame w/o calculations.
  352. //#59: "scale" options should only affect absolute mode.
  353. kf.offset = kf.offset * _scale;
  354. }
  355. }
  356. //"relative" mode, where numbers are relative to anchors.
  357. else {
  358. kf.mode = 'relative';
  359. kf.anchors = [anchor1, anchor2];
  360. }
  361. }
  362. //Does this element have key frames?
  363. if(!keyFrames.length) {
  364. continue;
  365. }
  366. //Will hold the original style and class attributes before we controlled the element (see #80).
  367. var styleAttr, classAttr;
  368. var id;
  369. if(!ignoreID && SKROLLABLE_ID_DOM_PROPERTY in el) {
  370. //We already have this element under control. Grab the corresponding skrollable id.
  371. id = el[SKROLLABLE_ID_DOM_PROPERTY];
  372. styleAttr = _skrollables[id].styleAttr;
  373. classAttr = _skrollables[id].classAttr;
  374. } else {
  375. //It's an unknown element. Asign it a new skrollable id.
  376. id = (el[SKROLLABLE_ID_DOM_PROPERTY] = _skrollableIdCounter++);
  377. styleAttr = el.style.cssText;
  378. classAttr = _getClass(el);
  379. }
  380. _skrollables[id] = {
  381. element: el,
  382. styleAttr: styleAttr,
  383. classAttr: classAttr,
  384. anchorTarget: anchorTarget,
  385. keyFrames: keyFrames,
  386. smoothScrolling: smoothScrollThis,
  387. edgeStrategy: edgeStrategy,
  388. emitEvents: emitEvents,
  389. lastFrameIndex: -1
  390. };
  391. _updateClass(el, [SKROLLABLE_CLASS], []);
  392. }
  393. //Reflow for the first time.
  394. _reflow();
  395. //Now that we got all key frame numbers right, actually parse the properties.
  396. elementIndex = 0;
  397. elementsLength = elements.length;
  398. for(; elementIndex < elementsLength; elementIndex++) {
  399. var sk = _skrollables[elements[elementIndex][SKROLLABLE_ID_DOM_PROPERTY]];
  400. if(sk === undefined) {
  401. continue;
  402. }
  403. //Parse the property string to objects
  404. _parseProps(sk);
  405. //Fill key frames with missing properties from left and right
  406. _fillProps(sk);
  407. }
  408. return _instance;
  409. };
  410. /**
  411. * Transform "relative" mode to "absolute" mode.
  412. * That is, calculate anchor position and offset of element.
  413. */
  414. Skrollr.prototype.relativeToAbsolute = function(element, viewportAnchor, elementAnchor) {
  415. var viewportHeight = documentElement.clientHeight;
  416. var box = element.getBoundingClientRect();
  417. var absolute = box.top;
  418. //#100: IE doesn't supply "height" with getBoundingClientRect.
  419. var boxHeight = box.bottom - box.top;
  420. if(viewportAnchor === ANCHOR_BOTTOM) {
  421. absolute -= viewportHeight;
  422. } else if(viewportAnchor === ANCHOR_CENTER) {
  423. absolute -= viewportHeight / 2;
  424. }
  425. if(elementAnchor === ANCHOR_BOTTOM) {
  426. absolute += boxHeight;
  427. } else if(elementAnchor === ANCHOR_CENTER) {
  428. absolute += boxHeight / 2;
  429. }
  430. //Compensate scrolling since getBoundingClientRect is relative to viewport.
  431. absolute += _instance.getScrollTop();
  432. return (absolute + 0.5) | 0;
  433. };
  434. /**
  435. * Animates scroll top to new position.
  436. */
  437. Skrollr.prototype.animateTo = function(top, options) {
  438. options = options || {};
  439. var now = _now();
  440. var scrollTop = _instance.getScrollTop();
  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: options.duration || DEFAULT_DURATION,
  447. startTime: now,
  448. endTime: now + (options.duration || DEFAULT_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. //That's were we actually "scroll" on mobile.
  885. if(_isMobile && _skrollrBody) {
  886. //Set the transform ("scroll it").
  887. skrollr.setStyle(_skrollrBody, 'transform', 'translate(0, ' + -(_mobileOffset) + 'px) ' + _translateZ);
  888. }
  889. //Did the scroll position even change?
  890. if(_forceRender || _lastTop !== renderTop) {
  891. //Remember in which direction are we scrolling?
  892. _direction = (renderTop > _lastTop) ? 'down' : (renderTop < _lastTop ? 'up' : _direction);
  893. _forceRender = false;
  894. var listenerParams = {
  895. curTop: renderTop,
  896. lastTop: _lastTop,
  897. maxTop: _maxKeyFrame,
  898. direction: _direction
  899. };
  900. //Tell the listener we are about to render.
  901. var continueRendering = _listeners.beforerender && _listeners.beforerender.call(_instance, listenerParams);
  902. //The beforerender listener function is able the cancel rendering.
  903. if(continueRendering !== false) {
  904. //Now actually interpolate all the styles.
  905. _calcSteps(renderTop, _instance.getScrollTop());
  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 = (_skrollrBody && _skrollrBody.offsetHeight || 0);
  1254. var bodyHeight = Math.max(skrollrBodyHeight, body.scrollHeight, body.offsetHeight, documentElement.scrollHeight, documentElement.offsetHeight, documentElement.clientHeight);
  1255. return bodyHeight - documentElement.clientHeight;
  1256. };
  1257. /**
  1258. * Returns a string of space separated classnames for the current element.
  1259. * Works with SVG as well.
  1260. */
  1261. var _getClass = function(element) {
  1262. var prop = 'className';
  1263. //SVG support by using className.baseVal instead of just className.
  1264. if(window.SVGElement && element instanceof window.SVGElement) {
  1265. element = element[prop];
  1266. prop = 'baseVal';
  1267. }
  1268. return element[prop];
  1269. };
  1270. /**
  1271. * Adds and removes a CSS classes.
  1272. * Works with SVG as well.
  1273. * add and remove are arrays of strings,
  1274. * or if remove is ommited add is a string and overwrites all classes.
  1275. */
  1276. var _updateClass = function(element, add, remove) {
  1277. var prop = 'className';
  1278. //SVG support by using className.baseVal instead of just className.
  1279. if(window.SVGElement && element instanceof window.SVGElement) {
  1280. element = element[prop];
  1281. prop = 'baseVal';
  1282. }
  1283. //When remove is ommited, we want to overwrite/set the classes.
  1284. if(remove === undefined) {
  1285. element[prop] = add;
  1286. return;
  1287. }
  1288. //Cache current classes. We will work on a string before passing back to DOM.
  1289. var val = element[prop];
  1290. //All classes to be removed.
  1291. var classRemoveIndex = 0;
  1292. var removeLength = remove.length;
  1293. for(; classRemoveIndex < removeLength; classRemoveIndex++) {
  1294. val = _untrim(val).replace(_untrim(remove[classRemoveIndex]), ' ');
  1295. }
  1296. val = _trim(val);
  1297. //All classes to be added.
  1298. var classAddIndex = 0;
  1299. var addLength = add.length;
  1300. for(; classAddIndex < addLength; classAddIndex++) {
  1301. //Only add if el not already has class.
  1302. if(_untrim(val).indexOf(_untrim(add[classAddIndex])) === -1) {
  1303. val += ' ' + add[classAddIndex];
  1304. }
  1305. }
  1306. element[prop] = _trim(val);
  1307. };
  1308. var _trim = function(a) {
  1309. return a.replace(rxTrim, '');
  1310. };
  1311. /**
  1312. * Adds a space before and after the string.
  1313. */
  1314. var _untrim = function(a) {
  1315. return ' ' + a + ' ';
  1316. };
  1317. var _now = Date.now || function() {
  1318. return +new Date();
  1319. };
  1320. var _keyFrameComparator = function(a, b) {
  1321. return a.frame - b.frame;
  1322. };
  1323. /*
  1324. * Private variables.
  1325. */
  1326. //Singleton
  1327. var _instance;
  1328. /*
  1329. A list of all elements which should be animated associated with their the metadata.
  1330. Exmaple skrollable with two key frames animating from 100px width to 20px:
  1331. skrollable = {
  1332. element: <the DOM element>,
  1333. styleAttr: <style attribute of the element before skrollr>,
  1334. classAttr: <class attribute of the element before skrollr>,
  1335. keyFrames: [
  1336. {
  1337. frame: 100,
  1338. props: {
  1339. width: {
  1340. value: ['{?}px', 100],
  1341. easing: <reference to easing function>
  1342. }
  1343. },
  1344. mode: "absolute"
  1345. },
  1346. {
  1347. frame: 200,
  1348. props: {
  1349. width: {
  1350. value: ['{?}px', 20],
  1351. easing: <reference to easing function>
  1352. }
  1353. },
  1354. mode: "absolute"
  1355. }
  1356. ]
  1357. };
  1358. */
  1359. var _skrollables;
  1360. var _skrollrBody;
  1361. var _listeners;
  1362. var _forceHeight;
  1363. var _maxKeyFrame = 0;
  1364. var _scale = 1;
  1365. var _constants;
  1366. var _mobileDeceleration;
  1367. //Current direction (up/down).
  1368. var _direction = 'down';
  1369. //The last top offset value. Needed to determine direction.
  1370. var _lastTop = -1;
  1371. //The last time we called the render method (doesn't mean we rendered!).
  1372. var _lastRenderCall = _now();
  1373. //For detecting if it actually resized (#271).
  1374. var _lastViewportWidth = 0;
  1375. var _lastViewportHeight = 0;
  1376. var _requestReflow = false;
  1377. //Will contain data about a running scrollbar animation, if any.
  1378. var _scrollAnimation;
  1379. var _smoothScrollingEnabled;
  1380. var _smoothScrollingDuration;
  1381. //Will contain settins for smooth scrolling if enabled.
  1382. var _smoothScrolling;
  1383. //Can be set by any operation/event to force rendering even if the scrollbar didn't move.
  1384. var _forceRender;
  1385. //Each skrollable gets an unique ID incremented for each skrollable.
  1386. //The ID is the index in the _skrollables array.
  1387. var _skrollableIdCounter = 0;
  1388. var _edgeStrategy;
  1389. //Mobile specific vars. Will be stripped by UglifyJS when not in use.
  1390. var _isMobile = false;
  1391. //The virtual scroll offset when using mobile scrolling.
  1392. var _mobileOffset = 0;
  1393. //If the browser supports 3d transforms, this will be filled with 'translateZ(0)' (empty string otherwise).
  1394. var _translateZ;
  1395. //Will contain data about registered events by skrollr.
  1396. var _registeredEvents = [];
  1397. //Animation frame id returned by RequestAnimationFrame (or timeout when RAF is not supported).
  1398. var _animFrame;
  1399. //Expose skrollr as either a global variable or a require.js module
  1400. if(typeof define === 'function' && define.amd) {
  1401. define('skrollr', function () {
  1402. return skrollr;
  1403. });
  1404. } else if (typeof module !== 'undefined' && module.exports) {
  1405. module.exports = skrollr;
  1406. } else {
  1407. window.skrollr = skrollr;
  1408. }
  1409. }(window, document));