alongslide 0.9.1 → 0.9.2

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