Source: tracklib.js

/*! evtrack -- Lib module */

/**
 * Auxiliary functions to track the user activity. 
 * Library borrowed from {@link https://github.com/luileito/evtrack}
 * @author Luis Leiva
 * @version 0.2
 * @license Dual licensed under the MIT and GPL licenses.
 */
var TrackLib = window.TrackLib || {};
/**
 * XPath functions.
 * Not documented yet.
 * Code adapted from window.js at {@link http://code.google.com/p/xpathchecker/}
 * @author Brian Slesinsky ({@link http://slesinsky.org})
 */
TrackLib.XPath = {

    queryXPath: function(document, xpath) {
      var iterator;
      if (typeof document.evaluate === 'function') {
        iterator = document.evaluate(xpath, document.documentElement, null, XPathResult.ANY_TYPE, null);
      } else {
        try {
          // IE5 and later has implemented that [0] should be the first node, 
          // but according to the W3C standard it should have been [1]!
          document.setProperty("SelectionLanguage", "XPath");
          iterator = document.selectNodes(xpath);
        } catch(err) {
          iterator = false;
        }
      }
      
      return iterator;
    },
    
    getXPathNodes: function(document, xpath) {
      var iterator = this.queryXPath(document, xpath);
      var result = [];
      var item = iterator.iterateNext();
      while (item) {
        result.push(item);
        item = iterator.iterateNext();
      }
      
      return result;
    },

    getXPath: function(targetNode, absolute) {
      var lowerCase = (targetNode.ownerDocument instanceof HTMLDocument)
        , xNodePath = this.getNodePath(targetNode, absolute)
        , nodeNames = []
        ;
      for (var i in xNodePath) {
        var node = xNodePath[i]
          , nIdx
          ;
        if (node.nodeType == 1) {
          if (i == 0 && !absolute && node.hasAttribute("id")) {
            nodeNames.push("/*[@id='" + node.getAttribute("id") + "']");
          } else {
            var tagName = node.tagName;
            if (lowerCase) {
              tagName = tagName.toLowerCase();
            }
            nIdx = this.getNodeIndex(node);
            if (nIdx != null) {
              nodeNames.push(tagName + "[" + nIdx + "]");
            } else {
              nodeNames.push(tagName);
            }
          }
        } else if (node.nodeType == 3) {
          nIdx = this.getTextNodeIndex(node);
          if (nIdx != null) {
            nodeNames.push("text()[" + nIdx + "]");
          } else {
            nodeNames.push("text()");
          }
        }
      }
      
      return "/" + nodeNames.join("/");
    },

    getNodeIndex: function(node) {
      if (node.nodeType != 1 || node.parentNode == null) return null;
      var list = this.getChildNodesWithTagName(node.parentNode, node.tagName);
      if (list.length == 1 && list[0] == node) return null;
      for (var i = 0; i < list.length; i++) {
        if (list[i] == node) return i + 1;
      }
      
      throw new Error("couldn't find node in parent's list: " + node.tagName);
    },

    getTextNodeIndex: function(node) {
      var list = this.getChildTextNodes(node.parentNode)
      if (list.length == 1 && list[0] == node) return null;
      for (var i = 0; i < list.length; i++) {
        if (list[i] == node) return i + 1;
      }
      
      throw new Error("couldn't find node in parent's list: " + node.tagName);
    },

    getChildNodesWithTagName: function(parent, tagName) {
      var result = [], child = parent.firstChild;
      while (child != null) {
        if (child.tagName && child.tagName == tagName) {
          result.push(child);
        }
        child = child.nextSibling;
      }
      
      return result;
    },

    getChildTextNodes: function(parent) {
      var result = [], child = parent.firstChild;
      while (child != null) {
        if (child.nodeType == 3) {
          result.push(child);
        }
        child = child.nextSibling;
      }
      
      return result;
    },

    getNodePath: function(node, absolute) {
      var result = [];
      while (node.nodeType == 1 || node.nodeType == 3) {
        result.unshift(node);
        if (node.nodeType == 1 && node.hasAttribute("id") && !absolute) return result;
        node = node.parentNode;
      }
      
      return result;
    },

    getNodeValues: function(resultList) {
      var result = [];
      for (var i in resultList) {
        result.push(resultList[i].nodeValue);
      }
      
      return result;
    }
  
};
/**
 * Ajax handling object.
 */
TrackLib.XHR = {
  /**
   * Creates an XML/HTTP request to provide async communication with the server.
   * @return {object} XHR object
   * @autor Peter-Paul Koch ({@link http://quirksMode.org})
   */
  createXMLHTTPObject: function() {
    var xmlhttp = false;
    // Current AJAX flavors
    var factories = [
      function(){ return new XMLHttpRequest(); },
      function(){ return new ActiveXObject("Msxml2.XMLHTTP"); },
      function(){ return new ActiveXObject("Msxml3.XMLHTTP"); },
      function(){ return new ActiveXObject("Microsoft.XMLHTTP"); }
    ];
    // Check AJAX flavor
    for (var i = 0; i < factories.length; ++i) {
      try {
        xmlhttp = factories[i]();
      } catch(e) { continue; }
      break;
    }
    
    return xmlhttp;
  },
  /**
   * Makes an asynchronous XMLHTTP request (XHR) via GET or POST.
   * Inspired by Peter-Paul Koch's old XMLHttpRequest wrapper function.
   * Note: CORS on IE will work only for version 8 or higher.
   * @param  {object} setup Request properties
   *    @config {string}    url       Request URL
   *    @config {boolean}  [async]    Asynchronous request (or not)
   *    @config {function} [callback] Response function
   *    @config {string}   [postdata] POST vars in the form "var1=name&var2=name..."
   *    @config {object}   [xmlhttp]  A previous XMLHTTP object can be reused
   */
  sendAjaxRequest: function(setup) {
    // Create XHR object or reuse it
    var request = setup.xmlhttp ? setup.xmlhttp : this.createXMLHTTPObject();
    var cors = !TrackLib.Util.sameDomain(window.location.href, setup.url);
    // CORS does work with XMLHttpRequest on modern browsers, except IE
    if (cors && window.XDomainRequest) {
      request = new XDomainRequest();
    }
    if (!request) return false;
    
    var method = setup.postdata ? "POST" : "GET";
    var asynchronous = setup.hasOwnProperty('async') ? setup.async : true;
    // Start request
    request.open(method, setup.url, asynchronous);
    
    var iecors = window.XDomainRequest && (request instanceof XDomainRequest);
    // Post requests must set the correct content type (not allowed under CORS + IE, though)
    if (setup.postdata && !iecors) {
      request.setRequestHeader('Content-Type', "application/x-www-form-urlencoded");
    }
    // Add load listener
    if (iecors) {
      request.onload = function(){
        if (typeof setup.callback === 'function') setup.callback(request.responseText);
      };
    } else {
      // Check for the 'complete' request state
      request.onreadystatechange = function(){
        if (request.readyState == 4 && typeof setup.callback === 'function') {
          setup.callback(request.responseText);
        }
      };
    }
    request.send(setup.postdata);
  }

};
/**
 * Event handling object.
 */
TrackLib.Events = {
    /**
     * Adds event listeners unobtrusively.
     * @author John Resig ({@link http://ejohn.org})
     * @param {object}    obj   Object to add listener(s) to.
     * @param {string}    type  Event type.
     * @param {function}  fn    Function to execute.
     */
    add: function(obj, type, fn) {
      if (!obj) return false;
      if (obj.addEventListener) { // W3C standard
        obj.addEventListener(type, fn, false);
      } else if (obj.attachEvent) { // IE versions
        obj.attachEvent("on"+type, fn);
      } else { // Really old browser
        obj[type+fn] = function(){ fn(window.event); };
      }
    },
    
    addMulti: function(obj, events, fn) {
      if (typeof events === "string") events = events.split(" ");
      for (var e = 0; e < events.length; e++) {
        this.add(obj, events[e], fn);
      }
    },
    /**
     * Removes event listeners unobtrusively.
     * @author John Resig ({@link http://ejohn.org})
     * @param {object}    obj   Object to remove listener(s) from
     * @param {string}    type  Event type
     * @param {function}  fn    Function to remove from event
     */
    remove: function(obj, type, fn) {
      if (!obj) return false;
      if (obj.removeEventListener) { // W3C standard
        obj.removeEventListener(type, fn, false);
      } else if (obj.detachEvent) { // IE versions
        obj.detachEvent("on"+type, fn);
      } else { // Really old browser
        obj[type+fn] = null;
      }
    }, 
    /**
     * Fixes event handling inconsistencies between browsers.
     * @param {object}  e Event
     * @return {object}   Fixed event
     */
    fix: function(e) {
      e = e || window.event;
      // Fix target property, if necessary (IE 6/7/8 & Safari 2)
      if (!e.target) e.target = e.srcElement || document;
      // Target should not be a text node (Safari bug)
      if (e.target.nodeType == 3) e.target = e.target.parentNode;
      // For mouse/key events; add metaKey if it's not there (IE 6/7/8)
      if (typeof e.metaKey === 'undefined') e.metaKey = e.ctrlKey;
      // Support multitouch events (index 0 is consistent with mobile devices)
      e.id = e.identifier || 0;
      
      return e;
    },
    /**
     * Executes callback on DOM load.
     * @param {function} callback
     */
    domReady: function(callback) {
      if (arguments.callee.done) return;
      arguments.callee.done = true;
      if (document.addEventListener) {
        // W3C browsers
        document.addEventListener('DOMContentLoaded', callback, false);
      }
      else if (document.attachEvent) {
        // Internet Explorer ¬¬
        try {
          document.write("<scr"+"ipt id=__ie_onload defer=true src=//:><\/scr"+"ipt>");
          var script = document.getElementById("__ie_onload");
          script.onreadystatechange = function() {
            if (this.readyState === 'complete') { callback(); }
          };
        } catch(err) {}
      }
      else {
        // Really old browsers
        TrackLib.Events.add(window, 'load', callback);
      }
    }

};
/**
 * Dimension handling object.
 */
TrackLib.Dimension = {
  /**
   * Gets the browser's window size (aka 'the viewport').
   * @return {object} window dimmensions
   *    @config {integer} width
   *    @config {integer} height
   */
  getWindowSize: function() {
    var d = document;
    var w = (window.innerWidth) ? window.innerWidth
            : (d.documentElement && d.documentElement.clientWidth) ? d.documentElement.clientWidth
            : (d.body && d.body.clientWidth) ? d.body.clientWidth
            : 0;
    var h = (window.innerHeight) ? window.innerHeight
            : (d.documentElement && d.documentElement.clientHeight) ? d.documentElement.clientHeight
            : (d.body && d.body.clientHeight) ? d.body.clientHeight
            : 0;
    
    return { width: w, height: h };
  },
  /**
   * Gets the document's size.
   * @return {object} document dimensions
   *    @config {integer} width
   *    @config {integer} height
   */
  getDocumentSize: function() {
    var d = document;
    var w = (window.innerWidth && window.scrollMaxX) ? window.innerWidth + window.scrollMaxX
            : (d.body && d.body.scrollWidth > d.body.offsetWidth) ? d.body.scrollWidth
            : (d.body && d.body.offsetWidth) ? d.body.offsetWidth
            : 0;
    var h = (window.innerHeight && window.scrollMaxY) ? window.innerHeight + window.scrollMaxY
            : (d.body && d.body.scrollHeight > d.body.offsetHeight) ? d.body.scrollHeight
            : (d.body && d.body.offsetHeight) ? d.body.offsetHeight
            : 0;
    
    return { width: w, height: h };
  },
  /**
   * Gets the max value from both window (viewport's size) and document's size.
   * @return {object} viewport dimensions
   *    @config {integer} width
   *    @config {integer} height
   */
  getPageSize: function() {
    var win = this.getWindowSize(),
        doc = this.getDocumentSize();
    
    // Find max values from this group
    var w = (doc.width < win.width) ? win.width : doc.width;
    var h = (doc.height < win.height) ? win.height : doc.height;
    
    return { width: w, height: h };
  },
  
  getElementSize: function(node) {
    return { width: node.offsetWidth, height: node.offsetHeight };
  },

  getElementPosition: function(node) {
    var curleft = 0, curtop = 0;
    if (node && node.offsetParent) {
      do {
        curleft += node.offsetLeft;
        curtop  += node.offsetTop;
      } while (node = node.offsetParent);
    }
    return { left: curleft, top: curtop };
  }

};
/**
 * Some utilies.
 */
TrackLib.Util = {
  /**
   * Tests whether a set of URLs come from the same domain.
   * @return {boolean}
   */
  sameDomain: function() {
    var prevDomain, sameDomain = true;
    for (var i = 0, l = arguments.length; i < l; ++i) {
      if (i > 0) {
        sameDomain = (this.getDomain(prevDomain) == this.getDomain(arguments[i]));
      }
      prevDomain = arguments[i];
    }
    
    return sameDomain;
  },
  /**
   * Gets the domain of a given URL.
   * @return {string}
   */
  getDomain: function(url) {
    var d, link = document.createElement("a");
    link.href = url;
    d = link.hostname;
    link = null; // free
    
    return d;
  },
  /**
   * Serializes the attributes of a DOM node.
   * @param {object} elem  DOM node
   * @return {string} JSON representation of the node attributes
   */   
  serializeAttrs: function(elem) {
    var obj = {};
    if (elem && elem.attributes) {
      obj[elem.nodeName] = {};
      for (var i = 0, t = elem.attributes.length; i < t; i++) {
        var attrib = elem.attributes[i];
        if (attrib.specified) {
          obj[elem.nodeName][attrib.name] = attrib.value;
        }
      }
    }
    
    return JSON.stringify(obj);
  }
  
};