PageSourceSearch

https://thepma.org/wp-content/themes/optimizewp/assets/js/modules/plugins/skrollr.js?ver=7.1.2

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