Team:LMU-Munich/xscrolly.js

From 2014.igem.org

Revision as of 12:45, 15 October 2014 by Mdotzler (Talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

/*jshint expr:true*/

// define(['jquery', 'underscore'], function($, _) { window.XScrollY = (function($, _) {

 'use strict';
 var defaultOptions = {
   // setup options: these can only be set once
   container: window,     // selector for scroll container, should also be an offset parent
   targets: 'section',    // selector for the targets
   // configuration options
   updateOffsets: 3,      // force script to re-calculate offsets:
                          //   0 (default)   calculate only the first time
                          //   1             re-calculate after `unveil`
                          //   2             re-calculate after `change`
                          //   3             re-calculate every scroll
   offset: 200,             // pixels from the top of the page to set the origin
   throttle: 200          // milliseconds to de-bounce the scroll event
 };
 function XScrollY(options) {
   var self = this;
   this._seen = [];
   this.options = $.extend({}, defaultOptions, options || {});
   this.$scrollElement = $(this.options.container);
   this.$active = $();
   this.updateTargets();
   this.updateOffsets();
   this.$scrollElement.on('scroll.xscrolly',
     _.throttle(function() { self.process.call(self); }, this.options.throttle));
   this.process();
   // TODO move this around? If it's inside `process` we have to branch inside
   // an event handler, which is almost as bad as a branch inside a loop. If
   // it's above `process` then we don't know what's active.
   this.options.start && this.options.start.call(this, this.$active);
 }
 // get targets and offsets
 XScrollY.prototype.updateTargets = function() {
   this.$targets = $(this.options.targets);
 };
 // update the offsets cache and offset-to-target map, passed in by reference.
 XScrollY.prototype._updateOffsets = function($target, _offsets, _map) {
   $target.each(function(idx, target) {
     var offset = $(target).position().top;
     if (!_map[offset]) {
       _map[offset] = [];
     }
     _offsets.push(offset);
     _map[offset].push(target);
   });
   _offsets = _offsets.sort(function(a, b) { return a - b; });
 };
 // update target offset lookup
 XScrollY.prototype.updateOffsets = function() {
   this.offsets = [];
   this.offsetMap = {};
   this._updateOffsets(this.$targets, this.offsets, this.offsetMap);
 };
 // convenience method to update targets and offsets
 XScrollY.prototype.update = function() {
   this.updateTargets();
   this.updateOffsets();
 };
 // get the active offset for the current scroll depth
 XScrollY.prototype.getActiveOffset = function() {
   var scrollTop = this.$scrollElement.scrollTop() + this.options.offset,
       i = 0, n = this.offsets.length;
   for (; i < n; ++i) {
     if (scrollTop < this.offsets[i]) {
       return this.offsets[Math.max(0, i - 1)];
     }
   }
   return this.offsets[n - 1];
 };
 // Find target(s) directly above the origin. If there are multiple targets at
 // the same vertical position, return all of them.
 XScrollY.prototype.getActive = function() {
   var offset = this.getActiveOffset();
   this.activeOffset = offset;
   return $(this.offsetMap[offset]);
 };


 // EVENTS
 // Callback for the scroll event
 XScrollY.prototype.process = function() {
   if (this.options.updateOffsets === 3) {
     this.updateOffsets();
   }
   var oldActiveOffset = this.activeOffset,
       $active = this.getActive();  // this.activeOffset gets set in here :(
   if (this.activeOffset != oldActiveOffset) {
     this.change($active);
   }
   this.options.scroll && this.options.scroll.call(this, $active);
   this.$active = $active;
 };
 // when target scope changes
 XScrollY.prototype.change = function($active) {
   if (this.options.updateOffsets === 2) {
     this.updateOffsets();
   }
   if (_.indexOf(this._seen, $active[0]) == -1) {
     this._seen.push($active[0]);
     this.unveil($active);
   }
   this.options.change && this.options.change.call(this, $active);
 };
 // when target scope changes for the first time
 XScrollY.prototype.unveil = function($active) {
   if (this.options.updateOffsets === 1) {
     this.updateOffsets();
   }
   this.options.unveil && this.options.unveil.call(this, $active);
 };


 // METHODS
 XScrollY.prototype.disable = function() {
   // TODO
   this.$scrollElement.off('.xscrolly');
 };
 XScrollY.prototype.enable = function() {
   // TODO
 };


 // HELPER METHODS
 //
 // to get elements based purely on internally stored offsets
 // get elements between `top` and `bottom` scroll depth.
 XScrollY.prototype.slice = function(top, bottom, $target) {
   var offsets = this.offsets,
       offsetMap = this.offsetMap;
   if ($target) {
     offsets = [];
     offsetMap = {};
     this._updateOffsets($target, offsets, offsetMap);
   }
   var i = 0, n = offsets.length,
       $ret = $(),
       off;
   for (; i < n; ++i) {
     off = offsets[i];
     if (top <= off && off < bottom) {
       $ret = $ret.add(offsetMap[off]);
     }
   }
   return $ret;
 };
 // get all targets visible on screen
 //
 // arguments:
 //
 //   localOffset    (default: 0) shift the viewport down this many pixels.
 //                  Useful if you need to counter a global offset.
 //   bleed          (default: 0) Extend the viewport this many pixels. Just
 //                  like how bleed works in print.
 //   $targets       (default: undefined) Use a custom set of targets.
 //
 XScrollY.prototype.visible = function(localOffset, bleed, $targets) {
   // re-interpret the arguments
   if (typeof localOffset === 'object' && localOffset.jquery) {
     $targets = localOffset;
     localOffset = 0;
     bleed = 0;
   } else {
     localOffset = localOffset || 0;
     bleed = bleed || 0;
   }
   var scrollTop = this.$scrollElement.scrollTop(),
       scrollBottom = scrollTop + this.$scrollElement.height();
   return this.slice(scrollTop + this.options.offset + localOffset - bleed,
       scrollBottom + this.options.offset + localOffset + bleed,
       $targets);
 };
 // get all targets above
 XScrollY.prototype.above = function(localOffset, bleed, $targets) {
   // re-interpret the arguments
   if (typeof localOffset === 'object' && localOffset.jquery) {
     $targets = localOffset;
     localOffset = 0;
     bleed = 0;
   } else {
     localOffset = localOffset || 0;
     bleed = bleed || 0;
   }
   var origin = this.$scrollElement.scrollTop();
   return this.slice(0,
       origin + this.options.offset + localOffset + bleed,
       $targets);
 };
 // get all targets above origin + screen
 XScrollY.prototype.aboves = function(localOffset, bleed, $targets) {
   // re-interpret the arguments
   if (typeof localOffset === 'object' && localOffset.jquery) {
     $targets = localOffset;
     localOffset = 0;
     bleed = 0;
   } else {
     localOffset = localOffset || 0;
     bleed = bleed || 0;
   }
   var origin = this.$scrollElement.scrollTop(),
       origins = origin + this.$scrollElement.height();
   return this.slice(0,
       origins + this.options.offset + localOffset + bleed,
       $targets);
 };
 // get all targets below
 XScrollY.prototype.below = function(localOffset, bleed, $targets) {
   // re-interpret the arguments
   if (typeof localOffset === 'object' && localOffset.jquery) {
     $targets = localOffset;
     localOffset = 0;
     bleed = 0;
   } else {
     localOffset = localOffset || 0;
     bleed = bleed || 0;
   }
   var origin = this.$scrollElement.scrollTop();
   return this.slice(origin + this.options.offset + localOffset - bleed,
     Infinity,
     $targets);
 };
 // get all targets below screen
 XScrollY.prototype.belows = function(localOffset, bleed, $targets) {
   // re-interpret the arguments
   if (typeof localOffset === 'object' && localOffset.jquery) {
     $targets = localOffset;
     localOffset = 0;
     bleed = 0;
   } else {
     localOffset = localOffset || 0;
     bleed = bleed || 0;
   }
   var origin = this.$scrollElement.scrollTop(),
       origins = origin + this.$scrollElement.height();
   return this.slice(origins + this.options.offset + localOffset - bleed,
     Infinity,
     $targets);
 };
 // set an option: `option('offset', 100)`
 // set several options: `option({offset: 100, throttle: 1000})`
 XScrollY.prototype.option = function(key, value) {
   if (typeof key === 'object' && typeof value === 'undefined') {
     $.extend(this.options, key);
   } else {
     this.options[key] = value;
   }
 };
 return XScrollY;

})(window.jQuery, window._);