PageSourceSearch

https://www.manset.fr/bestof/js/skrollr.js

js manset.fr collected 2026-10-02 11:22:12 UTC 44,353 bytes, 1,639 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.