tmaiaroto/telepathic-black-panther

View on GitHub
dev/js/telepathic-black-panther.js

Summary

Maintainability
F
2 mos
Test Coverage
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/*
 * Cookies.js - 1.2.1
 * https://github.com/ScottHamper/Cookies
 *
 * This is free and unencumbered software released into the public domain.
 */
(function (global, undefined) {
    'use strict';

    var factory = function (window) {
        if (typeof window.document !== 'object') {
            throw new Error('Cookies.js requires a `window` with a `document` object');
        }

        var Cookies = function (key, value, options) {
            return arguments.length === 1 ?
                Cookies.get(key) : Cookies.set(key, value, options);
        };

        // Allows for setter injection in unit tests
        Cookies._document = window.document;

        // Used to ensure cookie keys do not collide with
        // built-in `Object` properties
        Cookies._cacheKeyPrefix = 'cookey.'; // Hurr hurr, :)
        
        Cookies._maxExpireDate = new Date('Fri, 31 Dec 9999 23:59:59 UTC');

        Cookies.defaults = {
            path: '/',
            secure: false
        };

        Cookies.get = function (key) {
            if (Cookies._cachedDocumentCookie !== Cookies._document.cookie) {
                Cookies._renewCache();
            }

            return Cookies._cache[Cookies._cacheKeyPrefix + key];
        };

        Cookies.set = function (key, value, options) {
            options = Cookies._getExtendedOptions(options);
            options.expires = Cookies._getExpiresDate(value === undefined ? -1 : options.expires);

            Cookies._document.cookie = Cookies._generateCookieString(key, value, options);

            return Cookies;
        };

        Cookies.expire = function (key, options) {
            return Cookies.set(key, undefined, options);
        };

        Cookies._getExtendedOptions = function (options) {
            return {
                path: options && options.path || Cookies.defaults.path,
                domain: options && options.domain || Cookies.defaults.domain,
                expires: options && options.expires || Cookies.defaults.expires,
                secure: options && options.secure !== undefined ?  options.secure : Cookies.defaults.secure
            };
        };

        Cookies._isValidDate = function (date) {
            return Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date.getTime());
        };

        Cookies._getExpiresDate = function (expires, now) {
            now = now || new Date();

            if (typeof expires === 'number') {
                expires = expires === Infinity ?
                    Cookies._maxExpireDate : new Date(now.getTime() + expires * 1000);
            } else if (typeof expires === 'string') {
                expires = new Date(expires);
            }

            if (expires && !Cookies._isValidDate(expires)) {
                throw new Error('`expires` parameter cannot be converted to a valid Date instance');
            }

            return expires;
        };

        Cookies._generateCookieString = function (key, value, options) {
            key = key.replace(/[^#$&+\^`|]/g, encodeURIComponent);
            key = key.replace(/\(/g, '%28').replace(/\)/g, '%29');
            value = (value + '').replace(/[^!#$&-+\--:<-\[\]-~]/g, encodeURIComponent);
            options = options || {};

            var cookieString = key + '=' + value;
            cookieString += options.path ? ';path=' + options.path : '';
            cookieString += options.domain ? ';domain=' + options.domain : '';
            cookieString += options.expires ? ';expires=' + options.expires.toUTCString() : '';
            cookieString += options.secure ? ';secure' : '';

            return cookieString;
        };

        Cookies._getCacheFromString = function (documentCookie) {
            var cookieCache = {};
            var cookiesArray = documentCookie ? documentCookie.split('; ') : [];

            for (var i = 0; i < cookiesArray.length; i++) {
                var cookieKvp = Cookies._getKeyValuePairFromCookieString(cookiesArray[i]);

                if (cookieCache[Cookies._cacheKeyPrefix + cookieKvp.key] === undefined) {
                    cookieCache[Cookies._cacheKeyPrefix + cookieKvp.key] = cookieKvp.value;
                }
            }

            return cookieCache;
        };

        Cookies._getKeyValuePairFromCookieString = function (cookieString) {
            // "=" is a valid character in a cookie value according to RFC6265, so cannot `split('=')`
            var separatorIndex = cookieString.indexOf('=');

            // IE omits the "=" when the cookie value is an empty string
            separatorIndex = separatorIndex < 0 ? cookieString.length : separatorIndex;

            return {
                key: decodeURIComponent(cookieString.substr(0, separatorIndex)),
                value: decodeURIComponent(cookieString.substr(separatorIndex + 1))
            };
        };

        Cookies._renewCache = function () {
            Cookies._cache = Cookies._getCacheFromString(Cookies._document.cookie);
            Cookies._cachedDocumentCookie = Cookies._document.cookie;
        };

        Cookies._areEnabled = function () {
            var testKey = 'cookies.js';
            var areEnabled = Cookies.set(testKey, 1).get(testKey) === '1';
            Cookies.expire(testKey);
            return areEnabled;
        };

        Cookies.enabled = Cookies._areEnabled();

        return Cookies;
    };

    var cookiesExport = typeof global.document === 'object' ? factory(global) : factory;

    // AMD support
    if (typeof define === 'function' && define.amd) {
        define(function () { return cookiesExport; });
    // CommonJS/Node.js support
    } else if (typeof exports === 'object') {
        // Support Node.js specific `module.exports` (which can be a function)
        if (typeof module === 'object' && typeof module.exports === 'object') {
            exports = module.exports = cookiesExport;
        }
        // But always support CommonJS module 1.1.1 spec (`exports` cannot be a function)
        exports.Cookies = cookiesExport;
    } else {
        global.Cookies = cookiesExport;
    }
})(typeof window === 'undefined' ? this : window);
},{}],2:[function(require,module,exports){
/*! minibus - v3.1.0 - 2014-11-22
 * https://github.com/axelpale/minibus
 *
 * Copyright (c) 2014 Akseli Palen <akseli.palen@gmail.com>;
 * Licensed under the MIT license */

(function (root, factory) {
  'use strict';
  // UMD pattern commonjsStrict.js
  // https://github.com/umdjs/umd
  if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define(['exports'], factory);
  } else if (typeof exports === 'object') {
    // CommonJS & Node
    factory(exports);
  } else {
    // Browser globals
    factory((root.Minibus = {}));
  }
}(this, function (exports) {
  'use strict';

// Minibus

//**************
// Constructor *
//**************

var Bus = function () {
  // event string -> sub route map
  this.eventMap = {};

  // route string -> route object
  this.routeMap = {};

  // free namespace shared between the event handlers on the bus.
  this.busContext = {};
};

exports.create = function () {
  return new Bus();
};

// For extendability.
// Usage: Minibus.extension.myFunction = function (...) {...};
exports.extension = Bus.prototype;



//*******************
// Helper functions *
//*******************

var isArray = function (v) {
  return Object.prototype.toString.call(v) === '[object Array]';
};

var isEmpty = function (obj) {
  for (var prop in obj) {
    if (obj.hasOwnProperty(prop)) {
      return false;
    }
  }
  return true;
};



//*************
// Exceptions *
//*************

var InvalidEventStringError = function (eventString) {
  // Usage
  //   throw new InvalidEventStringError(eventString)
  this.name = 'InvalidEventStringError';
  this.message = 'Invalid event string given: ' + eventString;
};

var InvalidRouteStringError = function (routeString) {
  // Usage
  //   throw new InvalidRouteStringError(routeString)
  this.name = 'InvalidRouteStringError';
  this.message = 'Invalid route string given: ' + routeString;
};

var InvalidEventHandlerError = function (eventHandler) {
  // Usage
  //   throw new InvalidEventHandlerError(eventHandler)
  this.name = 'InvalidEventHandlerError';
  this.message = 'Invalid event handler function given: ' + eventHandler;
};



//*******************************************
// Member functions. They all are mutators. *
//*******************************************

var _emit = function (eventString) {
  // Emit an event to execute the bound event handler functions.
  // The event handlers are executed immediately.
  //
  // Parameter
  //   eventString
  //     Event string or array of event strings.
  //   arg1 (optional)
  //     Argument to be passed to the handler functions.
  //   arg2 (optional)
  //   ...
  //
  // Return
  //   nothing
  //
  // Throw
  //   InvalidEventStringError
  //     if given event string is not a string or array of strings.
  //
  var emitArgs, i, subRouteMap, routeString, eventHandlers, busContext;

  // Turn to array for more general code.
  if (!isArray(eventString)) {
    eventString = [eventString];
  }

  // Validate all eventStrings before mutating anything.
  // This makes the on call more atomic.
  for (i = 0; i < eventString.length; i += 1) {
    if (typeof eventString[i] !== 'string') {
      throw new InvalidEventStringError(eventString[i]);
    }
  }

  // Collect passed arguments after the eventString argument.
  emitArgs = [];
  for (i = 1; i < arguments.length; i += 1) {
    emitArgs.push(arguments[i]);
  }

  // Collect all the event handlers bound to the given eventString
  eventHandlers = [];
  for (i = 0; i < eventString.length; i += 1) {
    if (this.eventMap.hasOwnProperty(eventString[i])) {
      subRouteMap = this.eventMap[eventString[i]];
      for (routeString in subRouteMap) {
        if (subRouteMap.hasOwnProperty(routeString)) {
          eventHandlers.push(subRouteMap[routeString].eventHandler);
        }
      }
    }
  }

  // Apply the event handlers.
  // All event handlers on the bus share a same bus context.
  busContext = this.busContext;
  for (i = 0; i < eventHandlers.length; i += 1) {
    eventHandlers[i].apply(busContext, emitArgs);
  }
};

// See Node.js events.EventEmitter.emit
Bus.prototype.emit = _emit;

// See Backbone.js Events.trigger
Bus.prototype.trigger = _emit;

// See Mozilla Web API EventTarget.dispatchEvent
// See http://stackoverflow.com/a/10085161/638546
// Uncomment to enable. Too lengthy to be included by default.
//Bus.prototype.dispatchEvent = _emit;

// See http://stackoverflow.com/a/9672223/638546
// Uncomment to enable. Too rare to be included by default.
//Bus.prototype.fireEvent = _emit;



var _on = function (eventString, eventHandler) {
  // Bind an event string(s) to an event handler function.
  //
  // Parameter
  //   eventString
  //     Event string or array of event strings.
  //     Empty array is ok but does nothing.
  //   eventHandler
  //     Event handler function to be executed when the event is emitted.
  //
  // Throw
  //   InvalidEventStringError
  //   InvalidEventHandlerError
  //
  // Return
  //   a route string or an array of route strings
  //
  var wasArray, i, routeObject, routeString, routeStringArray;

  // Turn to array for more general code.
  // Store whether parameter was array to return right type of value.
  wasArray = isArray(eventString);
  if (!wasArray) {
    eventString = [eventString];
  }

  // Validate all eventStrings before mutating anything.
  // This makes the on call more atomic.
  for (i = 0; i < eventString.length; i += 1) {
    if (typeof eventString[i] !== 'string') {
      throw new InvalidEventStringError(eventString[i]);
    }
  }

  // Validate the eventHandler
  if (typeof eventHandler !== 'function') {
    throw new InvalidEventHandlerError(eventHandler);
  }

  routeStringArray = [];
  for (i = 0; i < eventString.length; i += 1) {
    routeObject = {
      eventString: eventString[i],
      eventHandler: eventHandler
    };

    routeString = Identity.create();
    routeStringArray.push(routeString);

    if (!this.eventMap.hasOwnProperty(eventString[i])) {
      this.eventMap[eventString[i]] = {};
    }
    this.eventMap[eventString[i]][routeString] = routeObject;
    this.routeMap[routeString] = routeObject;
  }

  if (wasArray) {
    return routeStringArray;
  } // else
  return routeStringArray[0];
};

// See Backbone.js Events.on
// See Node.js events.EventEmitter.on
Bus.prototype.on = _on;

// See http://stackoverflow.com/a/9672223/638546
Bus.prototype.listen = _on;

// See Node.js events.EventEmitter.addListener
// Uncomment to enable. Too lengthy to be included by default.
//Bus.prototype.addListener = _on;

// See Mozilla Web API EventTarget.addEventListener
// See http://stackoverflow.com/a/11237657/638546
// Uncomment to enable. Too lengthy to be included by default.
//Bus.prototype.addEventListener = _on;



var _once = function (eventString, eventHandler) {
  // Like _on but reacts to emit only once.
  //
  // Parameter
  //   See _on
  //
  // Return
  //   See _on
  //
  // Throw
  //   InvalidEventStringError
  //   InvalidEventHandlerError
  //
  var that, routeString, called;

  // Validate the eventHandler. On does the validation also.
  // Duplicate validation is required because a wrapper function
  // is feed into on instead the given eventHandler.
  if (typeof eventHandler !== 'function') {
    throw new InvalidEventHandlerError(eventHandler);
  }

  that = this;
  called = false;
  routeString = this.on(eventString, function () {
    if (!called) {
      called = true; // Required to prevent duplicate sync calls
      that.off(routeString);
      // Apply. Use the context given by emit to embrace code dryness.
      eventHandler.apply(this, arguments);
    }
  });
  return routeString;
};

// See Node.js events.EventEmitter.once
// See Backbone.js Events.once
Bus.prototype.once = _once;



var _off = function (routeString) {
  // Unbind one or many event handlers.
  //
  // Parameter
  //   routeString
  //     A route string or an array of route strings or
  //     an array of arrays of route strings.
  //     The route to be shut down.
  //
  // Parameter (Alternative)
  //   eventString
  //     An event string or an array of event strings or
  //     an array of arrays of event strings.
  //     Shut down all the routes with this event string.
  //
  // Parameter (Alternative)
  //   (nothing)
  //     Shut down all the routes i.e. unbind all the event handlers.
  //
  // Throws
  //   InvalidRouteStringError
  //
  // Return
  //   nothing
  //
  var noArgs, i, routeObject, eventString, subRouteMap, rs;

  noArgs = (typeof routeString === 'undefined');
  if (noArgs) {
    this.routeMap = {};
    this.eventMap = {};
    return;
  }

  // Turn to array for more general code.
  if (!isArray(routeString)) {
    routeString = [routeString];
  }

  // Flatten arrays to allow arrays of arrays of route strings.
  // This is needed if user wants to off an array of routes. Some routes
  // might be arrays or route strings because 'on' interface.
  // http://stackoverflow.com/a/10865042/638546
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/
  //   Reference/Global_Objects/Array/concat
  var flat = [];
  flat = flat.concat.apply(flat, routeString);
  routeString = flat;

  // Validate all routeStrings before mutating anything.
  // This makes the off call more atomic.
  for (i = 0; i < routeString.length; i += 1) {
    if (typeof routeString[i] !== 'string') {
      throw new InvalidRouteStringError(routeString[i]);
    }
  }

  for (i = 0; i < routeString.length; i += 1) {
    if (this.routeMap.hasOwnProperty(routeString[i])) {
      routeObject = this.routeMap[routeString[i]];
      delete this.routeMap[routeString[i]];
      delete this.eventMap[routeObject.eventString][routeString[i]];
      // Remove sub route map from the event map if it is empty.
      // This prevents outdated eventStrings piling up on the eventMap.
      if (isEmpty(this.eventMap[routeObject.eventString])) {
        delete this.eventMap[routeObject.eventString];
      }
    } else {
      // As eventString
      eventString = routeString[i];
      if (this.eventMap.hasOwnProperty(eventString)) {
        subRouteMap = this.eventMap[eventString];
        for (rs in subRouteMap) {
          if (subRouteMap.hasOwnProperty(rs)) {
            delete this.routeMap[rs];
          }
        }
        delete this.eventMap[eventString];
      }
    }
  }
  // Assert: event handlers and their routes removed.
};

// Backbone.js Events.off
Bus.prototype.off = _off;

// Node.js events.EventEmitter.removeListener
Bus.prototype.removeListener = _off;

// See Mozilla Web API EventTarget.removeEventListener
// Uncomment to enable. Too lengthy to be included by default.
//Bus.prototype.removeEventListener = _off;


var Identity = (function () {
  // A utility for creating unique strings for identification.
  // Abstracts how uniqueness is archieved.
  //
  // Usages
  //   >>> Identity.create();
  //   '532402059994638'
  //   >>> Identity.create();
  //   '544258285779506'
  //
  var exports = {};
  /////////////////

  exports.create = function () {
    return Math.random().toString().substring(2);
  };

  ///////////////
  return exports;
}());


  // Version
  exports.version = '3.1.0';


// End of intro
}));

},{}],3:[function(require,module,exports){
/**
 * Figures things out.
*/
module.exports = {
    /**
     * Returns the word count for any given page based on the paragraph elements.
     * This cuts out a lot of noise, though not every page uses paragraph tags (silly) and 
     * sometimes pages use paragraph tags within the footer or header or even navigation menus. 
     * So this function also has a simple threshold for what gets counted or not based on 
     * a minimum number of words.
     *
     * Note: All of the word count functions are approximations, so the use case should  
     * be something that works well with such approximations.
     *
     * @param  {number} minWords Minimum number of words required to count toward overall page word count
     * @param  {number} minChars Minimum number of characters required to count a word as a word (exclude "a", "and", "or" etc.)
     * @return {number}          The word count
    */
    paragraphPageWordCount: function(minWords, minChars) {
        minWords = (typeof(minWords) !== 'number') ? 2:minWords;
        minChars = (typeof(minChars) !== 'number') ? 3:minChars;
        var pageWordCount = 0;
        $ki('p').each(function(el) {
            var pText = el.innerText || el.textContent || "";
            var pWords = pText.split(/\s+/);
            var pWordCount = pWords.length;
            for(var w in pWords) {
                // Discount if the word is too short.
                if(pWords[w].length < minChars) {
                    pWordCount -= 1;
                }
            }
            // Count if the paragraph element's contents meets the minimum number of words.
            if(pWordCount > minWords) {
                pageWordCount += pWordCount;
            }
        });
        return pageWordCount;
    },
    /**
     * A more simple approach to counting the words on a page.
     *
     * @param  {array} discountedSelectors A list of selectors to discount
     * @return {number}                    The word count
    */
    pageWordCount: function(discountedSelectors) {
        // discount obvious selectors (so we don't count text in the footer, nav, navbar, etc.)
        discountedSelectors = (typeof(discountedSelectors) !== 'object') ? ['.footer', '.navbar', '.nav', '.header', '.ad', '.advertisement']:discountedSelectors;
        
        var bodyInnerText = $ki('body')[0].innerText || $ki('body')[0].textContent || "";
        var bodyWordCount = bodyInnerText.split(/\s+/).length;
        for(var i in discountedSelectors) {
            s = discountedSelectors[i];
            if($ki(s)[0] !== undefined) {
                var t = ($ki(s)[0].innerText || $ki(s)[0].textContent || "");
                bodyWordCount -= t.split(/\s+/).length;
            }
        }
        return bodyWordCount;
    },
    /**
     * Estimates reading time in minutes.
     *
     * @param  {number} wpm The number
     * @param  {mixed}  w   The number of words or a string
     * @return {number}     The estimated time, in minutes, to read
    */
    readTime: function(w, wpm) {
        wpm = (typeof(wpm) !== 'number') ? 250:wpm;
        wpm = wpm < 1 ? 1:wpm; // no divide by zero. no, don't disassemble number 5.
        var readTime = 0;
        var wc = 0;
        if(w !== undefined) {
            switch(typeof(w)) {
                case 'string':
                    wc = w.split(/\s+/).length;
                break;
                case 'number':
                    wc = w;
                break;
            }
            readTime = wc / wpm;
        }
        
        return readTime;
    },
    /**
     * Returns how far down the visitor has scrolled on the page in pixels or optionally as a percentage.
     *
     * @param  {boolean} percentage If true, a decmial percentage will be returned instead of a pixel value
     * @return {number}             Pixels or percentage
    */
    currentScrollPosition: function(percentage) {
        var top = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;
        var scrollPosition = top + window.innerHeight;
        if(percentage !== true) {
            return scrollPosition;
        }
        return (scrollPosition / this.pageHeight()).toFixed(2);
    },
    /**
     * Just returns the top of the current viewport.
     * 
     * @return {number} Pixels
    */
    windowTop: function() {
        return (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;
    },
    /**
     * Gets the current page's height.
     *
     * @return {number} The page height in pixels
    */
    pageHeight: function() {
        var body = document.body;
        var html = document.documentElement;
        return Math.max( body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight );
    } 
};
},{}],4:[function(require,module,exports){
/**
 * auto_detect.js is responsible for automatically detecting the proper events to track for any given page.
 * It calls functions within Tbp as necessary by analyzing what's on the page.
 *
 * NOTE: The types of events to be automatically detected and logged can be configured via the global options
 * under the `autoDetect` key value which would be an array of strings. These would be "linkOut" and "read"
 * and "scrolledPage" and so on.
 *
*/
module.exports = {
    autoDetectEvents: function() {
        var tbpContext = this;
        var methods = (typeof(this.config.autoDetect) === 'object') ? this.config.autoDetect:'all';

        tbpContext.log("Tbp.autoDetectEvents() Analyzing the page to watch for the following methods:", methods);

        // Detect outbound link clicks.
        if(methods.indexOf('linkOut') >= 0 || methods === 'all') {
            tbpContext.$('a').on('click', function(e) {
                // TODO: Detect social share URLs and discount those when tracking outbound links. Those will get tracked under social.js as shares using a different GA method.

                if((this.href).substr(0, 4).toLowerCase() === 'http') {
                    tbpContext.linkOut({
                        "url": this.href,
                        "element": this,
                        "elementEvent": e,
                        "trackDomainOnly": true
                    });
                } else {
                    return;
                }
            });
        }

        // Detect full page read.
        if(methods.indexOf('read') >= 0 || methods === 'all') {
            var count = tbpContext.analysis.paragraphPageWordCount();
            tbpContext.read({
                minTime: (tbpContext.analysis.readTime(count) * 60)
            });
        }

        // Detect page scroll percentage.
        if(methods.indexOf('scrolledPage') >= 0 || methods === 'all') {
            tbpContext.scrolledPage();
        }

        // Detect if mouse cursor leaves the page (toward the top)
        if(methods.indexOf('leave') >= 0 || methods === 'all') {
            tbpContext.leave();
        }

        // Detect history navigation state change (backward/forward buttons in browser)
        if(methods.indexOf('historyNavigate') >= 0 || methods === 'all') {
            tbpContext.historyNavigate();
        }

        // Detect hashbang change, many long pages have section navgiation for example. Or, single page JavaScript apps will use these.
        if(methods.indexOf('hashChange') >= 0 || methods === 'all') {
            tbpContext.hashChange();
        }

        // Detect set periods of inactivity (1 min, 3 min, and 5 min by default). 
        if(methods.indexOf('inactivity') >= 0 || methods === 'all') {
            tbpContext.inactivity();
        }
        
        // Detect time to engage the form(s) on the page.
        if(methods.indexOf('timeToEngage') >= 0 || methods === 'all') {
            tbpContext.$('form').each(function(el) {
                tbpContext.timeToEngage({element: el});
            });
        }
        
        // Detect form abandonment.
        if(methods.indexOf('formAbandonment') >= 0 || methods === 'all') {
            // tbpContext.formAbandonment();
        }
    }
};
},{}],5:[function(require,module,exports){
module.exports = {
    /**
     * In milliseconds, when this script has loaded. Not quite on DOM ready, but close.
     * Useful in mitigating false engagements, etc.
     * 
     * @type {Date}
    */
    loadTime: (new Date()).getTime(),
    /**
     * Returns how long it's been since this script was loaded.
     * 
     * @return {number} Time elapsed in milliseconds
    */
    timeSinceLoad: function() {
        return ((new Date()).getTime() - this.loadTime);
    },
    /**
     * A simple hash function.
     *
     * @param  {String}  str    String to hash
     * @param  {Boolean} retStr Return a string if true, else number
     * @return {mixed}             Hashed string as a numeric value -2^31 to 2^31
     */
    hashCode: function(str, retStr) {
        for(var ret = 0, i = 0, len = str.length; i < len; i++) {
            ret = (31 * ret + str.charCodeAt(i)) << 0;
        }
        return retStr ? parseInt(ret):ret;
    },
    /**
     * Returns a string value for use as an "target" in an "action:target" value.
     * We can't always assume elements on the page have an ID, so we need to use
     * a value that gives as much detail to the reporter as possible for helping them
     * identify the element on the page involved in the event.
     *
     * This isn't necessary if a specific action value is passed in the options 
     * to any event, but for automation purposes this will be used.
     * 
     * @return {string} The target value to be combined, for example; "action:" + target
     */
    getTargetName: function(element) {
        var tgtStr = "";
        if(element) {
            // $ki() can return an array. If there's only one item in it. Try that.
            if(element.tagName === undefined && element.length == 1) {
                element = element[0];
            }
            if(element.tagName !== undefined) {
                // The element can have a ```panther-target``` attribute for this case. Prefer that if set.
                // This, of course, is not automatic and requires some manual HTML adjustment. 
                if(element.getAttribute("panther-target")) {
                    tgtStr = element.getAttribute("panther-target");
                } else {
                    // If not, we'll use the best guess we have. Note that this may not be as user friendly 
                    // depending on how many similar elements are on the page. If we're talking about a "div" 
                    // for example...Good luck...
                    
                    // First, always begin with the tag name.
                    tgtStr = element.tagName;

                    // Then the ID if set and we're done since there's only one ID per page. Simple.
                    if(element.id !== "") {
                        tgtStr += "#" + element.id;
                    } else {
                        // Try the classList. Of course it's very possible for many elements to use the same class(es).
                        if(element.className) {
                            tgtStr += "." + element.className;
                        } else {
                            // No classes? Position on page? -- this could vary greatly and is up to the client device...so no.
                            // var offset = $ki(element).offset();
                            // tgtStr += "(x=" + offset.left + ",y=" + offset.top + ")";
                        }
                    }
                }
            }
        }
        return tgtStr;
    },
    /**
     * A wrapper around minibus that automatically adds the time the event occurred. 
     * This way, each funcion calling emit() doesn't need to pass it in the options.
     * Eeach emitted event also carries with it the visitor's first time seen for 
     * convenience.
     * 
     * The reason for this extra information is so that when users tap into the bus, 
     * they can take action on things and know when things happened.
     * Also, panther.do will use this data for conditions to take action.
     *
     * @param {Object} event The event object
    */
    emitEvent: function(event) {
        event._occurred = new Date();
        event._firstVisit = new Date(parseInt(this.cookies.get("_tbp_fv")));
        this.bus.emit('event', event);
    },
    /**
     * 
     * @param target can be any DOM Element or other EventTarget
     * @param type Event type (i.e. 'click')
     * @param doc Placeholder for document
     * @param event Placeholder for creating an Event
    */
    triggerEvent: function(target, type, doc, event) {
        doc = document;
        if(doc.createEvent) {
            event = new Event(type);
            event.preventLoop = true;
            target.dispatchEvent(event);
        } else {
            event = doc.createEventObject();
            event.preventLoop = true;
            target.fireEvent('on' + type, event);
        }
    },
    addEvent: function(element,type,callback){
        try {
            element.addEventListener(type,callback,!1);
        } catch(d) {
            element.attachEvent('on'+type,callback);
        }
    }
};
},{}],6:[function(require,module,exports){
/**
 * "do" is one of Telepathic Black Panther's coolest features.
 * While TBP can just run by itself without the user having to write any complex code or do anyhting at all really...
 * It's understood that some users will want to be more hands on.
 *
 * It is possible to work with TBP. Not just through the panther bus, but also through this semantic interface here.
 * To be clear, you can obtain the same results by listening to the bus though. This is just a faster way to do 
 * some common things.
 * 
 * For example:
 * panther.do.onPageVisit(10).action(function(visitorEvents){
 *     // Your code here is executed when a visitor has come to your page for the 10th time
 * });
 *
 * ...Or if you want the 10th visit to the entire site:
 * panther.do.onSiteVisit(10).action(function(visitorEvents){...});
 *
 * You'll notice `visitorEvents` being passed to your callback. This contains all events triggered by the user.
 * Ever. Since the very first time they came to your site (provided TBP was in use then).
 *
 * This relies heavily upon cookies and localstorage. It is of course possible for the visitor clear their cookies
 * and localstorage, so this is only as accurate as possible.
 *
 * This also allows the idea of "plugins" or extensions to be used here. All one need do is extend the "do" object.
 * For example, someone might add a plugin that displays a modal with configurable content when some visitor takes
 * a series of actions. Such things are outside the scope of Telepathic Black Panther, but TBP could drive that.
 * 
 * TODO
 * 
 */
},{}],7:[function(require,module,exports){
/**
 * The engagement.js module includes functions that track events related to a visitor's behavior
 * and level of engagement. How are visitors engaging with a page? Are they reading the content? 
 * Commenting? Filling out forms? Or are they getting stuck on forms? Do they abandom them?
 * These kind of questions are answered by the events this module sends to GA.
 * 
*/
module.exports = {
    /**
     * Tracks a "read" event when users have spent enough time on a page and scrolled far enough.
     * 
     * @param  {Object} opts
     *         minTime:  The amount of time, in seconds, that must pass before the event is considered valid (estimated time to read the content?).
     *         selector: The element selector (class, id, etc.) that is measured for scrolling.
     *         category: The Google Analytics Event category.
     *         action:   The Google Analytics Event action (likely no reason to change this given what the function is for).
     *         label:    The Google Analytics Event label (useful for categorizing events).
     *         debug:      Logs information to the console
     *         xMin:      The minimum amount of the element to be visible in the viewport to count (if the selector is "body" then this can't be set and will be bottom of page)
     *         yMin:      The minimum amount of the element to be visible in the viewport to count (if the selector is "body" then this can't be set and will be bottom of page)
     *         
     * @return {function}
    */
    read: function(opts) {
        opts = this.extend({
            "_method": "read",
            "minTime": 10,
            "selector": "body",
            "xMin": 0,
            "yMin": 1,
            // for GA events specifically
            "category": "behavior",
            "action": "read:page",
            "label": ""
        }, opts);

        var start = new Date().getTime();
        var enoughTimeHasPassed = false;
        var sentEvent = false;
        var hasScrolledFarEnough = false;
        var tbpContext = this;

        // Every 2 seconds, check the conditions and send the event if satisfied.
        setInterval(function(){
            var end = new Date().getTime();
            if((end - start) > (opts.minTime*1000)) {
                if(!enoughTimeHasPassed) {
                    tbpContext.log("Tbp.read() " + opts.minTime + " seconds have passed", "info");
                }
                enoughTimeHasPassed = true;
            }

            if(hasScrolledFarEnough === true && enoughTimeHasPassed === true) {
                // Send an event to Google Analytics.
                if(sentEvent === false) {
                    // Note: All options get on the bus. This way anything that's listening gets a report back of what options were passed
                    // to the method as well as which Telephatic Black Panther method was called (via the "_method" option).
                    tbpContext.emitEvent(opts);
                }
                sentEvent = true;
            }
        },(2*1000));

        var elem = $ki(opts.selector).first();
        $ki(document).on('scroll', function() {
            if(opts.selector === "body") {
                if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
                    $ki(document).off('scroll');
                    tbpContext.log("Tbp.read() The user has scrolled to the bottom of the page", "info");
                    hasScrolledFarEnough = true;
                }
            } else {
                if(elem.isOnScreen(opts.xMin,opts.yMin)) {
                    $ki(document).off('scroll');
                    tbpContext.log("Tbp.read() The user has scrolled far enough down the page", "info");
                    hasScrolledFarEnough = true;
                }
            }
        });

    },
    /**
     * Reports how much of the page came into the browser's viewport in a series of percentages.
     * 25%, 50%, 75%, and 100%
     *
     * Unlike the read() function, this one does not take time into account. It only cares how far 
     * down the page a user scrolled. It could be useful for building aggregate reports that could 
     * shed light on how much of a site is seen/used. It could also shed light on abandonment; perhaps 
     * a page has too much text on it and readers give up after a certain point.
     *
     * Note: This won't work for well for short pages. It will simply report 100% of the page came into 
     * view or perhaps 50% and then 100%.
     * 
     * @param  {object}     opts   
     *         minTime:               The amount of time, in seconds, that must pass before the event is considered valid (estimated time to read the content?)
     *         initialScrollRequired: If the user first must scroll in order for anything to count (default is true, this helps with short pages)
     *         category:              The Google Analytics category
     *         action:                The Google Analytics action
     *         hitCallback:           Optional callback function
     *         
     * @return {function}
    */
    scrolledPage: function(opts) {
        opts = this.extend({
            "_method": "scrolledPage",
            "minTime": 2,
            "initialScrollRequired": true,
            "category": "behavior",
            "action": "scroll:page"
        }, opts);

        var tbpContext = this;
        var sent = {};
        var send = function(label) {
            opts.label = label;
            if(sent[label] === undefined) {
                sent[label] = true;
                // tbpContext.log("Tbp.scrolledPage() Logging page scroll event, label: " + label);
                tbpContext.emitEvent(opts);            
            }
        };

        var percent = 0;
        var hasScrolled = false;
        var hasScrolledFn = function() {
            hasScrolled = true;
        };
        window.addEventListener("scroll", hasScrolledFn);

        // TODO: Think about checking if the window position started at the top of the page...
        // The idea was to prevent events from being logged about 25% scroll when the user started at the bottom of the page (a refresh for example or perhaps anhor link)
        // Though the page technically has scrolled down that far. It's the semantic difference between "has scrolled to this point" vs. "has seen up to this point"
        // ...which is of course completely different yet from "has actually read everything up to this point"
        // console.dir(tbpContext.windowTopOnLoad);

        // Check for this every two seconds. In fact, check the current scroll position each time rather than at any point.
        // This would negate situations where a user quickly scrolled down and back up again. While we aren't concerned with 
        // an actual "read" per se, we also want to do the best we can to avoid inaccuracies.
        // The "minTime" option also helps avoid tracking a user who comes to the page, scrolls real quick and leaves.
        // Again, not a "read" and so that "minTime" is meant to be short, but not zero. Though it could be set to zero of course.
        // One last reason here -- Google Analytics will throttle events if too many are sent too quickly.
        setTimeout(function() {
            var intervalId = setInterval(function() {
                // Only actually send something if the user has scrolled. If the user has not yet scrolled, don't do anything.
                // hasScrolled wil lbe true if the user has scrolled yet and it gets checked on this function's interval (every 2 seconds).
                // The reason for this check is because shorter pages may immediately meet the conditions for 25%, 50%, 75%, even 100% and
                // otherwise be automatically recorded when the user didn't actaully scroll. This may be desireable though, so the options
                // can bypass this check with `opts.initialScrollRequired` set to false.
                if(hasScrolled || !opts.initialScrollRequired) {
                    percent = tbpContext.analysis.currentScrollPosition(true);
                    if(percent >= 0.25) {
                        send("25%");
                    }
                    if(percent >= 0.5) {
                        send("50%");
                    }
                    if(percent >= 0.75) {
                        send("75%");
                    }
                    // Note: 98% will be considered close enough to 100% - there may even be times 100% isn't possible.
                    if(percent >= 0.98) {
                        send("100%");
                        // We can also stop checking at this point. It is theoretically possible the user quickly scrolled to the bottom of the page
                        // and could still hit a lower scroll percentage, but it's better to stop checking to not get in the way of anything else
                        // that may be running on the page.
                        clearInterval(intervalId);
                        window.removeEventListener("scroll", hasScrolledFn);
                    }
                }
            },(2*1000));
        },(opts.minTime*1000));
    },
    /**
     * Tracks a click on a link that takes a user away from the page.
     * This ensures the hit is recorded before directing the user onward.
     * 
     * Note: The elementEvent is a required option. Passing the element is preferred, but it 
     * should be available through the event. These are both very easily retrieved with $ki or jQuery, etc.
     *
     * They are needed because propagation needs to be stopped and a new simulated click event needs
     * to be triggered on the original element. This new click event will carry with it a new custom
     * property that is checked for by this function in order to prevent a loop.
     *
     * It's not a real challenge for links opening in a new window, but for those that open in the 
     * same window, we need to ensure our event is emitted and passed off to Google Analytics before
     * the browser is allowed to direct the visitor away from the page.
     *
     * Example usage can be found in auto_detect.js.
     * 
     * @param  {Object} opts
     *         element:         The (likely anchor) element with the link out
     *         elementEvent:    The event (likely MouseEvent on click) so that it can be cancelled while the event gets emitted
     *         trackDomainOnly: Just send the domain name to Google Analytics as the label instead of the full URL
     *         category:         The Google Analytics Event category
     *         action:             The Google Analytics Event action
     *         label:             The Google Analytics Event label (optional, this will be the URL by default)
     *         debug:             Logs information to the console
     *         
     * @return {function}         If a callback was specified, otherwise it redirects the user
    */
    linkOut: function(opts) {
        opts = this.extend({
            "_method": "linkOut",
            "element": false,
            "elementEvent": false,
            "trackDomainOnly": false,
            "category": "navigation",
            "action": "outbound",
            "label": ""
        }, opts);

        if(opts.elementEvent === undefined) {
            return;
        }
        // If an element was not passed, the event should have a target we can use...
        if(!opts.element) {
            opts.element = opts.elementEvent.target;
        }
        // Still no element? Really?
        if(!opts.element) {
            return;
        }
        var tbpContext = this;

        // When we set events, we add an extra property that prevents a loop...Because for links, we typically watch the click event and also dispatch a new one.
        if(opts.elementEvent.hasOwnProperty('preventLoop')) {
            return;
        }

        // Manually dispatch a (new, since we can't use the old one) click event on the element.
        var continueLinkOut = function() {
            tbpContext.addEvent(opts.element, 'click', function(){return;});
            tbpContext.triggerEvent(opts.element, 'click');
            return;
        };

        // By default the label is going to be the link out.
        var label = opts.element.href;
        var tmp = document.createElement('a');
        tmp.href = opts.element.href;
        // Check to ensure this is an outbound link
        if(tmp.hostname.toLowerCase() === window.location.host.toLowerCase()) {
            return;
        }

        // preventDefault() if the link target is not _blank because we need to ensure the event is sent to GA and handled 
        // by anything else before the page disappears.
        opts.elementEvent.preventDefault();
        opts.elementEvent.stopPropagation();

        if(opts.trackDomainOnly === true) {
            label = tmp.hostname;
        }
        // But that can be overridden by the call by passing a label value.
        if(opts.label !== "") {
            label = opts.label;
        }
        // Set label (whatever it is at this point) to opts so it can be passed to the panther bus as part of a single object.
        opts.label = label;

        opts.hitCallback = opts.hitCallback || function() {
            // Redirect if target is not _blank, on this callback (after the event has been emitted).
            if(opts.element && opts.element.target !== '_blank') {
                // tbpContext.log("Tbp.linkOut() The user will now be redirected to " + opts.element.href);
                if(opts.debug) {
                    return setTimeout(function(){
                        continueLinkOut();
                        return;
                    }, 5000);
                } else {
                    continueLinkOut();
                    return;
                }
            } else {
                continueLinkOut();
                return;
            }
        };

        tbpContext.emitEvent(opts);
    },
    /**
     * Detects a chance in the URL hash. This is common for single-page JavaScript apps with 
     * routing such as AngularJS.
     * 
     * Google Analytics doesn't track these by default (Google Tag Manager has some support around it though).
     * It's also useful for anchor links on page, #about #sectionA #sectionB etc.
     * Sometimes pages contain various sections with an index up top. These are considered separate but are
     * on the same page so GA doens't see that. In addition to seeing how far down the page a visitor scrolled, 
     * this will help show what content was consumed by a visitor.
     * 
     * @param {Object} opts
    */
    hashChange: function(opts) {
        opts = this.extend({
            "_method": "hashChange",
            "category": "navigation",
            "action": "hashbang",
            "label": ""
        }, opts);
        var tbpContext = this;

        window.onhashchange = function() {
            // The label will be the hash value and the event will only be emitted if the hash value exists. It will include and start with #.
            opts.label = window.location.hash;
            if(opts.label && opts.label.length > 1) {
                tbpContext.emitEvent(opts);
            }
        };
    },
    /**
     * Detects when a user presses forward or backward on their browser.
     * This can be useful in understanding if a web site has good UX or not.
     * If a user can't navigate their way around the site with ease, they may feel the need to use
     * their browser's back/forward button. Arguably this is why browsers have such buttons, but the
     * counter argument to that is it's not very easy to do that on mobile devices where the browser bar
     * (and back/forward controls) are often hidden in favor of screen space. Android devices do allow the
     * system back button to navigate backwards (though there's no system forward button).
     *
     * So the backward/forward button press can actually be important and telling of UX depending on the situation.
     *
     * Like linkOut() the label in this case will be the URL. This 
     *
     * @param {Object} opts
    */
    historyNavigate: function(opts) {
        opts = this.extend({
            "_method": "historyNavigate",
            "category": "navigation",
            "action": "history",
            "label": ""
        }, opts);
        var tbpContext = this;

        // http://stackoverflow.com/questions/4570093/how-to-get-notified-about-changes-of-the-history-via-history-pushstate
        // I'm not sure this will be supported in IE9...
        // Perhaps hashchange: https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onhashchange
        // But for now I think this will work. It is supported well enough to be statistically relevant and helpful.
        // There is also: https://github.com/browserstate/history.js - but that's a good bit of code to bring in.
        // Google Tag Manager can also track this stuff: http://www.simoahava.com/analytics/google-tag-manager-history-listener/
        (function(history){
            var pushState = history.pushState;
            history.pushState = function(state) {
                if (typeof history.onpushstate === "function") {
                    history.onpushstate({state: state});
                }
                // ... whatever else you want to do
                // maybe call onhashchange e.handler
                return pushState.apply(history, arguments);
            };
        })(window.history);
        window.onpopstate = history.onpushstate = function(e) {
            // Can't tell if we went forward or backward. Just that the history state has changed.
            tbpContext.emitEvent(opts);
        };
    },
    /**
     * Detects when the mouse cursor has left a particular element on the page (or the page itself).
     * Basically a proxy to the browser's `mouseleave` event with some configurable conditions.
     * 
     * By default, the entire document. This loosely detects the user's disinterest or attempt to use their navbar 
     * since it looks at mouseleave with regard to the y axis. If the user moves their mouse out of the window 
     * at the top, this sends an event.
     * 
     * It's how ouibounce works (https://github.com/carlsednaoui/ouibounce) and it's the best guess that can be made.
     * There are many uncaught scenarios.
     *
     * Of course don't forget visitors can go to another site by clicking on a link on the page too.
     * Though TBP knows about that through other functions.
     *
     * The problem here is that a user could also be bookmarking the site =) That's not exactly disinterest or abandonment.
     * Even if a timer is used here, it could take time for the user to organize the bookmark. It takes no time at all
     * to click the browser's "home" button or "back" button. So a timer would have missed those.
     * 
     * The only real guaranteed way is through `onbeforeunload` - the browser's event on exit, but that dialog can't be styled.
     *
     * Regardless, it is possible to respond to the `mouseleave` event fired by browsers and emit that as an event.
     *
     * Note: The "delay" option here makes 
     *
     * @param {Object} opts Various options including the category, action, label for the event (for GA)
     *                      trackOnce: If true (default), sets a cookie so the event may only occur once.
     *                      
     *                      perPage:   If true, the "trackOnce" is per page not site wide (cookie path gets set).
     *                      
     *                      delay:     An important one to note, it sets a time (ms) to count the leave, if a user moves their 
     *                                 mouse back into the element before the delay, it resets. So in order to "count" as a "leave"
     *                                 the user must have left and not came back for this period of time (default 1 second).
     *                                 
     *                      minTime:   Like delay, except this is the minimum amount of time that must pass before even considering
     *                                 something to be a valid mouseleave. Meaning, the page must be loaded for this period of time.
     *                                 This prevents mouseleave events on non-engagement. If a visitor loads the page but navigates 
     *                                 away real quick, it shouldn't be counted. Or should it? This adjusts that (default: 3 seconds).
     *                                 Note: This isn't on DOM ready, this timer starts upon this function being called.
     *                                 
     *                      proximity: How close the mouse cursor needs to be from the edge of the element {top: 0, right: 0, bottom: 0, left: 0}
     *                      
     *                      element:   Which element to watch (by default the entire page frame is watched)
     *
     *                         label:     Set this to a string that makes sense for the element. You'll want to use it to segment your analytics and 
     *                                    run reports, so make sure it's something useful and relevant.
    */
    leave: function(opts) {
        opts = this.extend({
            "_method": "leave",
            "category": "behavior",
            "action": "",
            "label": "",
            "nonInteraction": true,
            "trackOnce": true,
            "perPage": true,
            "delay": 1000,
            "minTime": 3000,
            // TODO: Proximity.
            "proximity": {top: 0, right: 0, bottom: 0, left: 0},
            "element": document.documentElement
        }, opts);

        // Ensure an element was passed.
        if(opts.element === "") {
            return;
        }

        var tbpContext = this;

        // The best we can do, probably not a good idea to pass an element without an id. Unless it's unique HTML code (which <html> and <body> will be of course).
        var elemId = opts.element.id || opts.element.outerHTML;
        var leaveKey = '_tbp_' + this.hashCode("leave_" + elemId, true);
        var path = opts.perPage ? window.location.pathname:"/";
        var _delayTimer = null;

        // Determine the action:actor value if not passed explicitly in the options.
        // If the element is not the page/document, then use its ID (if available) as the actor in the action:actor value.
        if(opts.action === "") {
            if(opts.element !== document.documentElement) {
                opts.action = "mouseleave:" + this.getTargetName(opts.element);
            } else {
                opts.action = "mouseleave:page";
            }
        }
        
        var cookies = this.cookies;
        setTimeout(function() {
            setTimeout(function() {
                opts.element.addEventListener('mouseleave', function(e) {
                    // TODO:
                    // Need to get the position of the element on the page and its bounding box to get the boundaries relative to the page then then subtract
                    // from the cursor position which is already relative to the page to get distance to the edge of the element.
                    // Then check against the proximity.
                    // 
                    // if(e.clientY > opts.proximity.top || e.clientY < opts.proximity.bottom || e.clientX > opts.proximity.right || e.clientX < opts.proximity.left) {
                    //     console.log("Not pushing event yet");
                    //     console.log("Y: " + e.clientY + " X: " + e.clientX);
                    //     return;
                    // }

                    // In the meantime, this will work just like ouibounce. Just set it when looking at the entire document.
                    // This default scenario is like ouibounce in that we are looking at when the visitor moves their cursor up toward the address bar
                    // or to a navigation button or perhaps even the menu or close button in their browser. Who knows...It's a guess really.
                    if(opts.element === document.documentElement) {
                        if(e.clientY > opts.proximity.top) {
                            return;
                        }
                    }

                    _delayTimer = setTimeout(function() {
                        if(!cookies.get(leaveKey)) {
                            // The label will contain the time in seconds it took to leave
                            opts.label = (tbpContext.timeSinceLoad()/1000);
                            tbpContext.log("Sending event for leaving.", "info");
                            tbpContext.emitEvent(opts);
                        } else {
                            tbpContext.log("Left, but event already sent.", "info");
                        }
                        if(opts.trackOnce) {
                            cookies.set(leaveKey, true, {path: path, expires: Infinity});
                        }
                    }, opts.delay);
                });

                opts.element.addEventListener('mouseenter', function() {
                    if (_delayTimer) {
                        clearTimeout(_delayTimer);
                        _delayTimer = null;
                    }
                });
            }, opts.delay);
        }, opts.minTime);
    },
    /**
     * Determines if a user has engaged with a form, but then abandoned 
     * it or had a difficult time completing it.
     *
     * Probably extremely handy on a checkout page.
     *
     * Maybe also have another function for time to fill out form.
     * Or, similar to inactivity, a "hesitation" timer. Once a visitor clicks on a form, how long does it take them to complete it?
     * Or once the mouse enters a button or certain section of a page, how long does it take for the visitor to click the CTA?
     * For that matter, many of these events might be useful: http://www.clicktale.com/products/mouse-tracking-suite/link-analytics
     * The problem in GA is matching the links to the data then telling/visually showing a reporter which link it was.
     *
     * Same issue exists for forms too...How many forms are on the page? How are they referenced/named?
     * We can simply say, "A form on this page was abandoned" but what if there are multiple?
     * So this may not be such an auotmatic thing...unless each form has an id of course.
     * 
     * 
     * @param {Object} opts
    */
    formAbandonment: function(opts) {
        opts = this.extend({
            "_method": "formAbandonment",
            "category": "behavior",
            "action": "formAbandonment",
            "label": "",
            "element": null
        }, opts);
        var tbpContext = this;

        // We need a form element to be passed.
        if(opts.element) {
        }
    },
    /**
     * Emits events for the period of time it took to complete a form.
     *
     * This can shed light on forms that may confuse visitors or otherwise create friction.
     * Forms that take a long time to complete may need to be broken up or made easier for better UX.
     *
     * @param {Object} opts
    */
    formCompletionTime: function(opts) {
        opts = this.extend({
            "_method": "formCompletionTime",
            "category": "behavior",
            "action": "formCompletionTime",
            "label": "",
            "element": null
        }, opts);
        var tbpContext = this;

        // We need a form element to be passed.
        if(opts.element) {
        }
    },
    /**
     * The time, in seconds, it took the visitor to engage with a given element and event type (click by default).
     * This could be the time it took a visitor to click a button or it could be the time it took
     * for a visitor to focus a form input field or to start typing into an input field.
     * Time to click a call to action, time to login, register, etc.
     *
     * Note: If a form element is passed, a listener will be applied to all of its input fields looking
     * for an onchange event.
     * 
     * @param  {Object} opts
     */
    timeToEngage: function(opts) {
        opts = this.extend({
            "_method": "timeToEngage",
            "category": "behavior",
            "action": "timeToEngage",
            "label": "",
            "element": null,
            "event": "click"
        }, opts);
        var tbpContext = this;

        if(opts.element) {
            opts.action = "timeToEngage:" + this.getTargetName(opts.element);

            var tteFn = function() {
                opts.element.removeEventListener(opts.event, tteFn);
                opts.label = (tbpContext.timeSinceLoad()/1000);
                tbpContext.emitEvent(opts);
            };

            if(opts.element.tagName.toLowerCase() === "form") {
                // Special handler for forms. Each input field will have a listener, so this needs to remove itself from all other inputs for the parent form.
                var tteFormFn = function(e) {
                    e.target.removeEventListener(e.type, tteFormFn);
                    for(var i=0; i < e.target.form.elements.length; i++) {
                        if(e.target.form.elements[i].type !== "fieldset") {
                            e.target.form.elements[i].removeEventListener(e.type, tteFormFn);
                        }
                    }
                    opts.label = (tbpContext.timeSinceLoad()/1000);
                    tbpContext.emitEvent(opts);
                };

                for(var i=0; i < opts.element.elements.length; i++) {
                    switch(opts.element.elements[i].type) {
                        default:
                            // We'll use focus over click, but other valid events include; keypress, keyup, keydown, and change
                            // For forms, the opts.event is applied to input fields.
                            // Note: Probably a bad idea for a web page to start a form input field off as being focused and 
                            // in such a case, "change" may be a better event. Personally, I think "change" is the best event 
                            // because clicking happens by "accident" sometimes. So auto_detect.js will use change.
                            if(opts.event === "click") {
                                opts.event = "focus";
                            }
                            opts.element.elements[i].addEventListener(opts.event, tteFormFn);
                            break;
                        case "fieldset":
                            // nada
                            break;
                        case "submit":
                            // do nothing here for now - submit could navigate the user away and we'd have to hijack the process like linkOut()
                            // and I don't see the value in it just yet...input fields should be changed by now right?
                            // 
                            // on click is the one to use here regardless of opts.event
                            // el.addEventListener("click", tteFn(el, "click"));
                            break;
                    }
                }

            } else {
                opts.element.addEventListener(opts.event, tteFn);
            }
        }
    },
    /**
     * Emits events for periods of inactivity on a page.
     * 
     * If the visitor does not move their mouse or scroll or click or do anything at all for specific 
     * intervals of time, events get emitted. These periods of time are configurable, by default they 
     * are 1 minute, 3 minute, and 5 minutes.
     *
     * Note: It is possible that the visitor is reading or watching a video. Though if they are reading, 
     * they likely should be moving their mouse or scroll the page. So adjust the timing accordingly.
     *
     * Though in the case of a video, this "inactivity" could actually be engagement. It could mean that 
     * the visitor is watching the video and therefore not moving their mouse.
     *
     * This inactivity timer can be paused in such cases:
     * http://stackoverflow.com/questions/16755129/detect-fullscreen-mode
     * https://gist.github.com/helgri/1336232
     *
     * Can also check for HTML5 video and if it is playing or not:
     * http://stackoverflow.com/questions/8599076/detect-if-html5-video-element-is-playing
     * By using: var stream = document.getElementsByTagName('video');
     * Then looping those (might be multiple, but an array is always returned of course) and checking
     * stream[0].paused  ... if paused is false, then media is playing on the page.
     *
     * So when media is playing or when the browser is perhaps fullscreen we can stop this inactivity counter.
     * We know the visitor is very likely engaged, just not moving their mouse around.
     *
     * Knowing when the page loaded (or when TBP was listening) we can also determine "true time on page"
     * in that we can figure out how long until the visitor went inactive. Google Analytics reports time on 
     * page and it's a bit inaccurate. It's inaccurate if a user leaves their computer open while they are at lunch.
     * It's inaccurate (I think) if visitors switch tabs because the timer is still going...But the visitor isn't
     * actually looking at the page.
     *
     * If there is no mouse movmement, keys pressed, or scrolling...Then we know they aren't paying attention.
     * We can assume the visitor left their computer to do something else physically. Or minimized the window.
     * So we can record our own time on page event and provide some accuracy over Google Analytics.
     *
     * This also shows something interesting. If a visitor has a web page open in a tab they value it.
     * They wanted to save it for later essentially. Possibly the immediate future. By looking at the pages
     * where there was inactivity one might be able to make those more engaging. We know visitors are interested
     * in these pages...Enough that they keep them open (just aren't actively looking at them). So how do we keep
     * the visitor more engaged? If we incrased engagment on the page, would it help (determined by other data)?
     * So this becomes a pretty cool metric. 
     *
     * Of course the web page can also listen for this event and do something upon inactivity. Maybe encourage
     * the visitor to engage...
     * 
     * @param {Object} opts
    */
    inactivity: function(opts) {
        opts = this.extend({
            "_method": "inactivity",
            "category": "behavior",
            "action": "inactive",
            "label": "",
            "nonInteraction": true,
            "periods": [60, 180, 300],
            // "timeToDisengage": 60, // can take lowest period for this. an event will be emitted that takes time since load to the period. 
            // that is how long it took a visitor to become inactive...so analytics reports can segment this. users are inactive after 3 minutes let's say.
            // and then we can ignore the fact that time on page was 10 minutes. because google's time on page is inaccurate in that case.
            "checkInterval": 2
        }, opts);

        var tbpContext = this;

        var sent = {};
        var timeSinceLastAcitivty = (new Date()).getTime();
        var active = false;
        var setActiveOnInput = function() {
            active = true;
            timeSinceLastAcitivty = (new Date()).getTime();
        };
        // All the events that tell us a visitor is actively engaging with the page.
        window.addEventListener("scroll", setActiveOnInput);
        window.addEventListener("mousemove", setActiveOnInput);
        window.addEventListener("keypress", setActiveOnInput);
        window.addEventListener("click", setActiveOnInput);

        // Checks for inactivity on the `opts.checkInterval` which can be tuned for performance.
        var inactivityCheck = setInterval(function() {
            // Check to see if active is false, if so - there has been no activity.
            if(!active) {
                // Then check if enough time has passed for the periods defined in `opts.periods`
                for(var i in opts.periods) {
                    if(opts.periods.hasOwnProperty(i)) {
                        var periodStr = opts.periods[i].toString();
                        if(!sent.hasOwnProperty(periodStr)) {
                            sent[periodStr] = false;
                        }
                        var now = (new Date()).getTime();
                        if(sent[periodStr] === false && ((now - timeSinceLastAcitivty) >= (opts.periods[i] * 1000))) {
                            opts.label = periodStr;
                            tbpContext.emitEvent(opts);
                            sent[periodStr] = true;
                        }
                    }
                }

                // watch all periods and once all have been reached. stop inactivityCheck and remove event listeners.
                var stopWatching = true;
                for(var j in sent) {
                    if(sent[j] === false) {
                        stopWatching = false;
                    }
                }
                if(stopWatching) {
                    // Keep it clean. Having all of these listeneers and the inactivity interval can affect performance.
                    tbpContext.log("Stop watching for activity, events for all periods have been sent.", "info");
                    clearInterval(inactivityCheck);
                    window.removeEventListener("scroll", setActiveOnInput);
                    window.removeEventListener("mousemove", setActiveOnInput);
                    window.removeEventListener("keypress", setActiveOnInput);
                    window.removeEventListener("click", setActiveOnInput);
                }
                
            }

            // Set active to false. It will get set back to true if there is activity before the next pass.
            active = false;
        }, opts.checkInterval*1000);
    }
};
},{}],8:[function(require,module,exports){
/*!
 * ki.js - jQuery-like API super-tiny JavaScript library
 * Copyright (c) 2014 Denis Ciccale (@tdecs)
 * Released under MIT license
 * Original source: https://github.com/dciccale/ki.js
 *
 * This was modified (not forked, though maybe it will be) to avoid conflicts with jQuery.
 * $ was changed to $ki
 * JSLint ignore comments were also added as well as a module.exports.
 * 
 */
/* jshint ignore:start */
!function (b, c, d, e, f) {

  // addEventListener support?
  f = b['add' + e];

  /*
   * init function (internal use)
   * a = selector, dom element or function
   * d = placeholder for matched elements
   * i = placeholder for length of matched elements
   */
  function i(a, d, i) {
    for(d = (a && a.nodeType ? [a] : '' + a === a ? b.querySelectorAll(a) : c), i = d.length; i--; c.unshift.call(this, d[i]));
  }

  /*
   * $ki main function
   * a = css selector, dom object, or function
   * http://www.dustindiaz.com/smallest-domready-ever
   * returns instance or executes function on ready
   */
  $ki = function (a) {
    return /^f/.test(typeof a) ? /in/.test(b.readyState) ? setTimeout('$ki('+a+')', 9) : a() : new i(a);
  };

  // set ki prototype
  $ki[d] = i[d] = {

    // default length
    length: 0,

    /*
     * on method
     * a = string event type i.e 'click'
     * b = function
     * return this
     */
    on: function (a, b) {
      return this.each(function (c) {
        return f ? c['add' + e](a, b, false) : c.attachEvent('on' + a, b);
      });
    },

    /*
     * off method
     * a = string event type i.e 'click'
     * b = function
     * return this
     */
    off: function (a, b) {
      return this.each(function (c) {
        return f ? c['remove' + e](a, b) : c.detachEvent('on' + a, b);
      });
    },

    /*
     * each method
     * a = the function to call on each iteration
     * b = the this value for that function (the current iterated native dom element by default)
     * return this
     */
    each: function (a, b) {
      for (var c = this, d = 0, e = c.length; d < e; ++d) {
        a.call(b || c[d], c[d], d, c);
      }
      return c;
    },

    // for some reason is needed to get an array-like
    // representation instead of an object
    splice: c.splice
  };
}(document, [], 'prototype', 'EventListener');
/* jshint ignore:end */

module.exports = $ki;
},{}],9:[function(require,module,exports){
module.exports = {
    first: function() {
        return $ki(this[0]);
    },
    last: function() {
        return $ki(this[this.length - 1]);
    },
    outerHeight: function() {
        this.each(function(b) {
            b.outerHeight = b.offsetHeight;
            var style = getComputedStyle(b);
            b.outerHeight += parseInt(style.marginTop) + parseInt(style.marginBottom);
        });
        return this.length > 1 ? this : this[0].outerHeight;
    },
    outerWidth: function() {
        this.each(function(b) {
            b.outerWidth = b.offsetWidth;
            var style = getComputedStyle(b);
            b.outerWidth += parseInt(style.marginLeft) + parseInt(style.marginRight);
        });
        return this.length > 1 ? this : this[0].outerWidth;
    },
    html: function(a) {
        return a === []._ ? this[0].innerHTML : this.each(function(b) {
            b.innerHTML = a;
        });
    },
    offset: function() {
        this.each(function(b) {
            var rect = b.getBoundingClientRect();
            b.offset = {
              top: rect.top + document.body.scrollTop,
              left: rect.left + document.body.scrollLeft
            };
        });
        return this.length > 1 ? this : this[0].offset;
    },
    hasClass: function(a) {
        return this[0].classList.contains(a);
    },
    /**
     * Determines if an element falls within the browser's viewport.
     * 
     * @param  {number}  x How much of the element must be visible along the x axis as a percentage (0, 0.5, 1, etc.)
     * @param  {number}  y How much of the element must be visible along the y axis as a percentage (0, 0.5, 1, etc.)
     * @return {boolean}   Whether or not enough of the element is visible on the screen to count
    */
    isOnScreen: function(x, y) {
        if(x === null || typeof x === 'undefined') { x = 1; }
        if(y === null || typeof y === 'undefined') { y = 1; }
        
        var viewport = {};
        viewport.left = (window.pageXOffset !== undefined) ? window.pageXOffset : (document.documentElement || document.body.parentNode || document.body).scrollLeft;
        viewport.top = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;

        viewport.right = viewport.left + window.innerWidth;
        viewport.bottom = viewport.top + window.innerHeight;
        
        var height = this.outerHeight();
        var width = this.outerWidth();
     
        if(!width || !height){
            return false;
        }
        
        var bounds = this.offset();
        bounds.right = bounds.left + width;
        bounds.bottom = bounds.top + height;
        
        var visible = (!(viewport.right < bounds.left || viewport.left > bounds.right || viewport.bottom < bounds.top || viewport.top > bounds.bottom));
        
        if(!visible){
            return false;   
        }
        
        var deltas = {
            top : Math.min( 1, ( bounds.bottom - viewport.top ) / height),
            bottom : Math.min(1, ( viewport.bottom - bounds.top ) / height),
            left : Math.min(1, ( bounds.right - viewport.left ) / width),
            right : Math.min(1, ( viewport.right - bounds.left ) / width)
        };
        
        return (deltas.left * deltas.right) >= x && (deltas.top * deltas.bottom) >= y;
    }
};
},{}],10:[function(require,module,exports){
(function() {
    // Make this available on the window for convenience and as $ki so it doesn't conflict with $
    window.$ki = require('./ki.ie8.js');

    Tbp = (function() {
        var defaults = {
            debug: false,
            autoDetect: true,
            // The default use case is to send events to Google Analytics, but that can be disabled...
            ga: true,
            // We also, by default, push events to the dataLayer (commonly used by GTM and many other things) 
            // pass an array in here, `dataLayer` || [] used by default if this is true
            dataLayer: true,
            // Visitor info
            clientId: null
        };

        /**
         * Telepathic Black Panther
         * 
         * @param {object} config Some configuration options used by Tbp
        */
        function Tbp(config) {
            // Tbp() or new Tbp() will work this way.
            if (!(this instanceof Tbp)) return new Tbp(config);

            // Shortcut Google Analytics, provide empty function if it doesn't exist so things don't bark at us elsewhere.
            if (typeof window.ga === "undefined") {
                if(console !== undefined && console.warn !== undefined) {
                    console.warn("Google Analytics not found.");
                }
                this.ga = function(){};
            } else {
                this.ga = window.ga;
            }

            this.ga(function(tracker) {
                defaults.clientId = tracker.get('clientId');
            });

            // Extend default config with passed config options.
            this.config = this.extend(defaults, config);
        
            // Load other core modules (kept separate for organization, still using require() for them).
            this.extend(this, require('./core.js'));
            this.extend(this, require('./engagement.js'));
            this.extend(this, require('./social.js'));
            this.extend(this, require('./auto_detect.js'));

            // Load some 3rd party modules.
            this.bus = require('../node_modules/minibus/minibus.js').create();
            this.analysis = require('./analysis.js');
            this.cookies = require('../node_modules/cookies-js/src/cookies.js');

            // Shortcut $ki.
            this.extend(window.$ki.prototype, require('./ki.plugins.js'));
            this.$ = window.$ki;

            // Setup auto detection for everything. If an array was passed then only on those defined methods (names of functions).
            if(this.config.autoDetect === true) {
                this.autoDetectEvents();
            } else if (typeof(this.config.autoDetect) === 'object') {
                this.autoDetectEvents(this.config.autoDetect);
            }

            // Cookie the user. Set the first time Telepathic Black Panther spotted them (trying to keep cookie names short, fv = first visit).
            if(!this.cookies.get("_tbp_fv")) {
                this.cookies.set("_tbp_fv", (new Date().getTime()), {expires: Infinity});
            }

            // There's going to be a few closures coming up here...
            var tbpContext = this;

            // Automatically submit to Google Analytics (unless configured otherwise) and log the event to console if debug was set true.
            // Also push on to the dataLayer if told to do so and emit an event for that through the bus too.
            // Anything else a user can handle via the bus.
            this.bus.on('event', function(event) {
                tbpContext.log("Emitted Event", event);

                // Push to the dataLayer
                if(typeof(tbpContext.config.dataLayer) === "object") {
                    tbpContext.config.dataLayer.push(event);
                } else if(tbpContext.config.dataLayer === true) {
                    if(typeof(window.dataLayer) === "object") {
                        window.dataLayer.push(event);
                    }
                }

                // Push to Google Analytics
                if(tbpContext.config.ga && event.label !== "" && event.label !== null) {
                    tbpContext.log("Sending event to Google Analytics", "info");
                    ga('send', {
                        'hitType': 'event',
                        'eventCategory': event.category,
                        'eventAction': event.action,
                        'eventLabel': event.label,
                        'hitCallback': event.hitCallback || null,
                        'nonInteraction': event.nonInteraction || false
                    });
                } else {
                    // Regardless of whether or not Google Analytics is in use, call "hitCallback" if it was defined.
                    // This is particularly important for the `linkOut` method as that one must stop the browser from navigating 
                    // in order to allow the event to be pushed. Then after the event has been pushed, it can continue.
                    if(typeof(event.hitCallback) === "function") {
                        event.hitCallback(event);
                    }
                }
            });

            // Here's how one could watch the dataLayer for anything Telepathic Black Panther pushes to it.
            // this.bus.on('dataLayer', function(event, theDataLayer) {
            //     console.dir(event);
            //     console.dir(theDataLayer);
            // });

            // Override push() on the dataLayer to catch changes to it.
            if(this.config.dataLayer) {
                var handleDataLayerPush = function () {
                    for (var i = 0, n = this.length, l = arguments.length; i < l; i++, n++) {
                        tbpContext.bus.emit('dataLayer', this[n] = arguments[i], this);
                        //RaiseMyEvent(this, n, this[n] = arguments[i]); // assign/raise your event
                    }
                    return n;
                };

                // Hopefully `dataLayer` will be an array already defined. However, some people may want to name it something different 
                // and that's ok too because `this.config.dataLayer` can be passed an array to use instead. If `dataLayer` doesn't exist yet, 
                // just make a new empty array to use.
                if(typeof(this.config.dataLayer) === "object") {
                    Object.defineProperty(this.config.dataLayer, "push", {
                        configurable: false,
                        enumerable: false, // hide from for...in
                        writable: false,
                        value: handleDataLayerPush
                    });
                } else if(this.config.dataLayer === true) {
                    if(typeof(window.dataLayer) === "object") {
                        Object.defineProperty(window.dataLayer, "push", {
                            configurable: false,
                            enumerable: false, // hide from for...in
                            writable: false,
                            value: handleDataLayerPush
                        });
                    }
                }
            }

        }

        Tbp.prototype = {
            /**
             * Simple extend to mimic jQuery's because we don't want a dep on jQuery for just this.
             * That'd be sillyness.
             * 
             * @return {Object} Returns an extended object
            */
            extend: function() {
                for(var i=1; i<arguments.length; i++) {
                    for(var key in arguments[i]) {
                        if(arguments[i].hasOwnProperty(key)) {
                            arguments[0][key] = arguments[i][key];
                        }
                    }
                }
                return arguments[0];
            },
            /**
             * A simple console log wrapper that first checks if debug mode is on.
             *
             * @var {mixed} message The string message to log
             * @var {mixed} obj     Either an object to log out or a string that will be matched for a log level that might change the color of the message
            */
            log: function(message, obj) {
                if(this.config.debug && console !== undefined) {
                    var style = "";
                    switch(obj) {
                        case "warn":
                            style = "color:orange;";
                            break;
                        case "error":
                            style = "color:red;font-weight:bold;";
                            break;
                        case "info":
                            style = "color:blue;";
                            break;
                        case "success":
                            style = "color:green;";
                            break;
                    }
                    if(style !== "") {
                        console.log("%c" + message, style);
                    } else {
                        if(obj) {
                            console.log(message, obj);
                        } else {
                            console.log(message);
                        }
                    }
                }
            },
            /**
             * Another simple wrapper for displaying an object as a collapsible tree via console.dir().
             * 
             * @param  {mixed} obj Object to print to the console
             */
            dir: function(obj) {
                if(this.config.debug && console !== undefined) {
                    console.dir(obj);
                }
            }
        };
        
        return Tbp;
    })();
    module.exports = Tbp;
})();
},{"../node_modules/cookies-js/src/cookies.js":1,"../node_modules/minibus/minibus.js":2,"./analysis.js":3,"./auto_detect.js":4,"./core.js":5,"./engagement.js":7,"./ki.ie8.js":8,"./ki.plugins.js":9,"./social.js":11}],11:[function(require,module,exports){
/**
 * The social.js module includes functions that track events related to social media.
 * For example, when visitors click social share buttons on the page.
 * 
*/
module.exports = {
};
},{}]},{},[3,4,5,6,7,8,9,10,11]);