Source: hui.js

/**
 * The root namespace of Humanise User Interface
 * @namespace
 */
hui = window.hui || {};

(function(hui,agent,window) {

  /**
   * @namespace
   */
  hui.browser = {};
  var browser = hui.browser;

  /** If the browser is any version of InternetExplorer */
  browser.msie = !/opera/i.test(agent) && /MSIE/.test(agent) || /Trident/.test(agent);
  /** If the browser is InternetExplorer 6 */
  browser.msie6 = agent.indexOf('MSIE 6') !== -1;
  /** If the browser is InternetExplorer 7 */
  browser.msie7 = agent.indexOf('MSIE 7') !== -1;
  /** If the browser is InternetExplorer 8 */
  browser.msie8 = agent.indexOf('MSIE 8') !== -1;
  /** If the browser is InternetExplorer 9 */
  browser.msie9 = agent.indexOf('MSIE 9') !== -1;
  /** If the browser is InternetExplorer 9 in compatibility mode */
  browser.msie9compat = browser.msie7 && agent.indexOf('Trident/5.0') !== -1;
  /** If the browser is InternetExplorer 10 */
  browser.msie10 = agent.indexOf('MSIE 10') !== -1;
  /** If the browser is InternetExplorer 11 */
  browser.msie11 = agent.indexOf('Trident/7.0') !== -1;
  /** If the browser is WebKit based */
  browser.webkit = agent.indexOf('WebKit') !== -1;
  /** If the browser is any version of Safari */
  browser.safari = agent.indexOf('Safari') !== -1;
  /** If the browser is any version of Chrome */
  //browser.chrome = agent.indexOf('Chrome') !== -1;
  /** The version of WebKit (null if not WebKit) */
  browser.webkitVersion = null;
  /** If the browser is Gecko based */
  browser.gecko = !browser.webkit && !browser.msie && agent.indexOf('Gecko') !== -1;
  /** If the browser is Gecko based */
  //browser.chrome = agent.indexOf('Chrome') !== -1;
  /** If the browser is safari on iPad */
  browser.ipad = browser.webkit && agent.indexOf('iPad') !== -1;
  /** If the browser is on Windows */
  browser.windows = agent.indexOf('Windows') !== -1;

  /** If the browser supports CSS opacity */
  browser.opacity = !browser.msie6 && !browser.msie7 && !browser.msie8;
  /** If the browser supports CSS Media Queries */
  browser.mediaQueries = browser.opacity;
  /** If the browser supports CSS animations */
  browser.animation = !browser.msie6 && !browser.msie7 && !browser.msie8 && !browser.msie9;

  browser.wordbreak = !browser.msie6 && !browser.msie7 && !browser.msie8;

  browser.touch = (!!('ontouchstart' in window) || (!!('onmsgesturechange' in window) && !!window.navigator.maxTouchPoints)) ? true : false;

  var result = /Safari\/([\d.]+)/.exec(agent);
  if (result) {
    browser.webkitVersion = parseFloat(result[1]);
  }
})(hui,navigator.userAgent,window);



/**
 * Log something
 * @param {Object} obj The object to log
 */
hui.log = function(obj) {
  if (window.console && console.log) {
    try { // Somehow it fails in IE if dev tools are not open
      console.log.apply(null,arguments);
    } catch (e) {}
  }
};

// Postponed listeners
hui._postponed = [];

/**
 * Register code as ready
 * @param name The name of the module
 * @param obj The object, function, namespace
 */
hui.define = function(name, obj) {
  var postponed = hui._postponed;
  for (var i = postponed.length - 1; i >= 0; i--) {
    var item = postponed[i];
    if (!item) continue;
    var deps = [];
    for (var j = 0; j < item.requirements.length; j++) {
      var path = item.requirements[j];
      var found = hui.evaluate(path); // TODO Handle objects defined by name but not evaluated by that name
      if (found) {
        deps.push(found);
      } else {
        break;
      }
    }
    if (item.requirements.length == deps.length) {
      hui.array.remove(postponed, item);
      item.callback.apply(null, deps);
    }
  }
};

hui._runOrPostpone = function() {
  var dependencies = [];
  var callback = null;
  var i;
  for (i = 0; i < arguments.length; i++) {
    if (typeof(arguments[i])=='function') {
      callback = arguments[i];
    } else if (hui.isArray(arguments[i])) {
      dependencies = arguments[i];
    }
  }
  var found = [];
  for (i = 0; i < dependencies.length; i++) {
    var vld = hui.evaluate(dependencies[i]);
    if (!vld) {
      found = false;
      break;
    }
    found[i] = vld;
  }
  if (found) {
    callback.apply(null, found);
  } else {
    hui.log('Postponing: ' + dependencies);
    hui._postponed.push({requirements:dependencies,callback:callback});
  }
};

/**
 * Evaluate an expression
 */
hui.evaluate = function(expression, context) {
  var path = expression.split('.');
  var cur = context || window;
  for (var i = 0; i < path.length && cur!==undefined; i++) {
    cur = cur[path[i]];
  }
  return cur;
};

/**
 * Defer a function so it will fire when the current "thread" is done
 * @param {Function} func The function to defer
 * @param {Object} ?bind Optional, the object to bind "this" to
 */
hui.defer = function(func,bind) {
  if (bind) {
    func = func.bind(bind);
  }
  window.setTimeout(func);
};

hui.extend = function(subClass, superClass) {
  var methods = subClass.prototype;
  for (var p in superClass) {
    if (superClass.hasOwnProperty(p)) {
      subClass[p] = superClass[p];
    }
  }
  function __() { this.constructor = subClass; }
  __.prototype = superClass.prototype;
  subClass.prototype = new __();
  if (methods) {
    for (var pr in methods) {
      subClass.prototype[pr] = methods[pr];
    }
  }
};

/**
 * Override the properties on the first argument with properties from the last object
 * @param {Object} original The object to override
 * @param {Object} subject The object to copy the properties from
 * @return {Object} The original
 */
hui.override = function(original,subject) {
  if (subject) {
    for (var prop in subject) {
      original[prop] = subject[prop];
    }
  }
  return original;
};

/**
 * Loop through items in array or properties in an object.
 * If «items» is an array «func» is called with each item.
 * If «items» is an object «func» is called with each (key,value)
 * @param {Object | Array} items The object or array to loop through
 * @param {Function} func The callback to handle each item
 */
hui.each = function(items, func) {
  var i;
  if (hui.isArray(items)) {
    for (i = 0; i < items.length; i++) {
      func(items[i], i);
    }
  } else if (items instanceof NodeList) {
    for (i = 0; i < items.length; i++) {
      func(items.item(i), i);
    }
  } else if (hui.isDefined(items)) {
    for (var key in items) {
      if (items.hasOwnProperty(key)) {
        func(key, items[key]);
      }
    }
  }
};

/**
 * Return text if condition is met
 * @param {Object} condition The condition to test
 * @param {String} text The text to return when condition evaluates to true
 */
hui.when = function(condition,text) {
  return condition ? text : '';
};

/**
 * Converts a string to an int if it is only digits, otherwise remains a string
 * @param {String} str The string to convert
 * @returns {Object} An int of the string or the same string
 */
hui.intOrString = function(str) {
  if (hui.isDefined(str)) {
    var result = /[0-9]+/.exec(str);
    if (result !== null && result[0] == str) {
      if (parseInt(str,10) == str) {
        return parseInt(str, 10);
      }
    }
  }
  return str;
};

/**
 * Make sure a number is between a min / max
 */
hui.between = function(min, value, max) {
  var result = Math.min(max, Math.max(min, value));
  return isNaN(result) ? min : result;
};

/**
 * Fit a box inside a container while preserving aspect ratio (note: expects sane input)
 * @param {Object} box The box to scale {width : 200, height : 100}
 * @param {Object} container The container to fit the box inside {width : 20, height : 40}
 * @returns {Object} An object of the new box {width : 20, height : 10}
 */
hui.fit = function(box,container,options) {
  options = options || {};
  var boxRatio = box.width / box.height;
  var containerRatio = container.width / container.height;
  var width, height;
  if (options.upscale===false && box.width<=container.width && box.height<=container.height) {
    width = box.width;
    height = box.height;
  }
  else if (boxRatio > containerRatio) {
    width = container.width;
    height = Math.round(container.width/box.width * box.height);
  } else {
    width = Math.round(container.height/box.height * box.width);
    height = container.height;
  }
  return {width : width, height : height};
};

/**
 * Checks if a string has non-whitespace characters
 * @param {String} str The string
 */
hui.isBlank = function(str) {
  if (str===null || typeof(str)==='undefined' || str==='') {
    return true;
  }
  return typeof(str)=='string' && hui.string.trim(str).length === 0;
};

/**
 * Checks that an object is not null and not undefined
 * @param {Object} obj The object to check
 */
hui.isDefined = function(obj) {
  return obj!==null && typeof(obj)!=='undefined';
};



/**
 * Checks if an object is a string
 * @param {Object} obj The object to check
 */
hui.isString = function(obj) {
  return typeof(obj)==='string';
};

/**
 * Checks if an object is a number
 * @param {Object} obj The object to check
 */
hui.isNumber = function(obj) {
  return typeof(obj)==='number';
};

/**
 * Checks if an object is an array
 * @param {Object} obj The object to check
 */
hui.isArray = function(obj) {
  if (obj === null || obj === undefined) {
    return false;
  }
  if (obj.constructor == Array) {
    return true;
  } else {
    return Object.prototype.toString.call(obj) === '[object Array]';
  }
};

///////////////////////// Strings ///////////////////////

/** @namespace */
hui.string = {

  /**
   * Test that a string start with another string
   * @param {String} str The string to test
   * @param {String} start The string to look for at the start
   * @returns {Boolean} True if «str» starts with «start»
   */
  startsWith : function(str, start) {
    if (!hui.isString(str) || !hui.isString(start)) {
      return false;
    }
    return str.match("^"+start) == start;
  },
  /**
   * Test that a string ends with another string
   * @param {String} str The string to test
   * @param {String} end The string to look for at the end
   * @returns {Boolean} True if «str» ends with «end»
   */
  endsWith : function(str, end) {
    if (!hui.isString(str) || !hui.isString(end)) {
      return false;
    }
    return str.match(end+"$") == end;
  },

  /**
   * Make a string camelized
   * @param {String} str The string to camelize
   * @returns {String} The camelized string
   */
  camelize : function(str) {
    if (str.indexOf('-')==-1) {
      return str;
    }
    var oStringList = str.split('-');

    var camelizedString = str.indexOf('-') === 0 ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) : oStringList[0];

    for (var i = 1, len = oStringList.length; i < len; i++) {
      var s = oStringList[i];
      camelizedString += s.charAt(0).toUpperCase() + s.substring(1);
    }

    return camelizedString;
  },
  /**
   * Trim whitespace including unicode chars
   * @param {String} str The text to trim
   * @returns {String} The trimmed text
   */
  trim : function(str) {
    if (!hui.isDefined(str)) {
      return '';
    }
    if (!hui.isString(str)) {
      str = String(str);
    }
    return str.replace(/^[\s\x0b\xa0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000]+|[\s\x0b\xa0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000]+$/g, '');
  },
  /**
   * Inserts invisible break chars in string so it will wrap
   * @param {String} str The text to wrap
   * @returns {String} The wrapped text
   */
  wrap : function(str) {
    if (str===null || str===undefined) {
      return '';
    }
    return str.split('').join("\u200B");
  },
  /**
   * Shorten a string to a maximum length
   * @param {String} str The text to shorten
   * @param {int} length The maximum length
   * @returns {String} The shortened text, '' if undefined or null string
   */
  shorten : function(str,length) {
    if (hui.isNumber(str)) {
      str = str+'';
    }
    else if (!hui.isString(str)) {return '';}
    if (str.length > length) {
      return str.substring(0,length-3) + '...';
    }
    return str;
  },
  /**
   * Escape the html in a string (robust)
   * @param {String} str The text to escape
   * @returns {String} The escaped text
   */
  escapeHTML : function(str) {
    if (!hui.isDefined(str)) {return '';}
    return hui.build('div',{text:str}).innerHTML;
  },
  /**
   * Escape the html in a string (fast)
   * @param {String} str The text to escape
   * @returns {String} The escaped text
   */
  escape : function(str) {
    if (!hui.isString(str)) {
      return str;
    }
    var tagsToReplace = {
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#x27;',
      '`': '&#x60;'
    };
    return str.replace(/[&<>'`"]/g, function(tag) {
      return tagsToReplace[tag] || tag;
    });
  },
  /**
   * Converts a JSON string into an object
   * @param json {String} The JSON string to parse
   * @returns {Object} The object
   */
  fromJSON : function(json) {
    try {
      return JSON.parse(json);
    } catch (e) {
      hui.log(e);
      return null;
    }
  },

  /**
   * Converts an object into a JSON string
   * @param obj {Object} the object to convert
   * @returns {String} A JSON representation
   */
  toJSON : function(obj) {
    return JSON.stringify(obj);
  }
};







//////////////////////// Array //////////////////////////

/** @namespace */
hui.array = {
  /**
   * Add an object to an array if it not already exists
   * @param {Array} arr The array
   * @param {Object} value The object to add
   */
  add : function(arr, value) {
    if (value.constructor == Array) {
      for (var i=0; i < value.length; i++) {
        if (!hui.array.contains(arr,value[i])) {
          arr.push(value);
        }
      }
    } else {
      if (!hui.array.contains(arr,value)) {
        arr.push(value);
      }
    }
  },
  /**
   * Check if an array contains a value
   * @param {Array} arr The array
   * @param {Object} value The object to check for
   * @returns {boolean} true if the value is in the array
   */
  contains : function(arr,value) {
    return hui.array.indexOf(arr,value) !== -1;
  },
  /**
   * Add or remove a value from an array.
   * If the value exists all instances are removed, otherwise the value is added
   * @param {Array} arr The array to change
   * @param {Object} value The value to flip
   */
  flip : function(arr,value) {
    if (hui.array.contains(arr,value)) {
      hui.array.remove(arr,value);
    } else {
      arr.push(value);
    }
  },
  /**
   * Remove all instances of a value from an array
   * @param {Array} arr The array to change
   * @param {Object} value The value to remove
   */
  remove : function(arr,value) {
    for (var i = arr.length - 1; i >= 0; i--){
      if (arr[i]==value) {
        arr.splice(i,1);
      }
    }
  },
  /**
   * Find the first index of a value in an array, -1 if not found
   * @param {Array} arr The array to inspect
   * @param {Object} value The value to find
   * @returns {Number} The index of the first occurrence, -1 if not found.
   */
  indexOf : function(arr,value) {
    for (var i=0; i < arr.length; i++) {
      if (arr[i]===value) {
        return i;
      }
    }
    return -1;
  },
  /**
   * Split a string, like "1,4,6" into an array of integers.
   * @param {String} The string to split
   * @returns {Array} An array of integers
   */
  toIntegers : function(str) {
    var array = str.split(',');
    for (var i = array.length - 1; i >= 0; i--){
      array[i] = parseInt(array[i],10);
    }
    return array;
  }
};










////////////////////// DOM ////////////////////

/** @namespace */
hui.dom = {
  isElement : function(node,name) {
    return node.nodeType==1 && (name===undefined ? true : node.nodeName.toLowerCase()==name);
  },
  isDefinedText : function(node) {
    return node.nodeType==3 && node.nodeValue.length>0;
  },
  addText : function(node,text) {
    node.appendChild(document.createTextNode(text));
  },
  // TODO: Move to hui.get
  firstChild : function(node) {
    var children = node.childNodes;
    for (var i=0; i < children.length; i++) {
      if (children[i].nodeType==1) {
        return children[i];
      }
    }
    return null;
  },
  parse : function(html) {
    var dummy = hui.build('div',{html:html});
    return hui.get.firstChild(dummy);
  },
  clear : function(node) {
    var children = node.childNodes;
    for (var i = children.length - 1; i >= 0; i--) {
      children[i].parentNode.removeChild(children[i]);
    }
  },
  remove : function(node) {
    if (node.parentNode) {
      node.parentNode.removeChild(node);
    }
  },
  replaceNode : function(oldNode,newNode) {
    if (newNode.parentNode) {
      newNode.parentNode.removeChild(newNode);
    }
    oldNode.parentNode.insertBefore(newNode,oldNode);
    oldNode.parentNode.removeChild(oldNode);
  },
  changeTag : function(node,tagName) {
    var replacement = hui.build(tagName);

    // Copy the children
    while (node.firstChild) {
        replacement.appendChild(node.firstChild); // *Moves* the child
    }

    // Copy the attributes
    for (var i = node.attributes.length - 1; i >= 0; --i) {
        replacement.attributes.setNamedItem(node.attributes[i].cloneNode());
    }

    // Insert it
    node.parentNode.insertBefore(replacement, node);

    // Remove the wns
    node.parentNode.removeChild(node);
    return replacement;
  },
  insertBefore : function(target,newNode) {
    target.parentNode.insertBefore(newNode,target);
  },
  insertAfter : function(target,newNode) {
    var next = target.nextSibling;
    if (next) {
      next.parentNode.insertBefore(newNode,next);
    } else {
      target.parentNode.appendChild(newNode);
    }
  },
  replaceHTML : function(node,html) {
    node = hui.get(node);
    node.innerHTML = html;
  },
  runScripts : function(node) {
    if (hui.dom.isElement(node)) {
      if (hui.dom.isElement(node,'script')) {
        /*jshint evil:true */
        eval(node.innerHTML);
      } else {
        var scripts = node.getElementsByTagName('script');
        for (var i=0; i < scripts.length; i++) {
          /*jshint evil:true */
          eval(scripts[i].innerHTML);
        }
      }
    }
  },
  setText : function(node,text) {
    if (text===undefined || text===null) {
      text = '';
    }
    var c = node.childNodes;
    var updated = false;
    for (var i = c.length - 1; i >= 0; i--){
      if (!updated && c[i].nodeType === 3) {
        c[i].nodeValue=text;
        updated = true;
      } else {
        node.removeChild(c[i]);
      }
    }
    if (!updated) {
      hui.dom.addText(node,text);
    }
  },
  getText : function(node) {
    var txt = '';
    var c = node.childNodes;
    for (var i=0; i < c.length; i++) {
      if (c[i].nodeType === 3 && c[i].nodeValue !== null) {
        txt+= c[i].nodeValue;
      } else if (c[i].nodeType == 1) {
        txt+= hui.dom.getText(c[i]);
      }
    }
    return txt;
  },
  isVisible : function(node) {
    while (node) {
      if (node.style && (hui.style.get(node,'display')==='none' || hui.style.get(node,'visibility')==='hidden')) {
        return false;
      }
      node = node.parentNode;
    }
    return true;
  },
  isDescendantOrSelf : function(element,parent) {
    while (element) {
      if (element==parent) {
        return true;
      }
      element = element.parentNode;
    }
    return false;
  }
};









///////////////////// Form //////////////////////

/** @namespace */
hui.form = {
  getValues : function(node) {
    var params = {};
    var inputs = node.getElementsByTagName('input');
    for (var i=0; i < inputs.length; i++) {
      if (hui.isDefined(inputs[i].name)) {
        params[inputs[i].name] = inputs[i].value;
      }
    }
    return params;
  }
};







///////////////////////////// Quering ////////////////////////

/**
 * @namespace
 * Functions for finding elements
 *
 * @function
 * Get an element by ID. If the ID is not a string it is returned.
 * @param {String | Element} id The ID to find
 * @returns {Element} The element with the ID or null
 */
hui.get = function(id) {
  if (typeof(id)=='string') {
    return document.getElementById(id);
  }
  return id;
};

/**
 * Get array of child elements of «node», not a NodeList
 */
hui.get.children = function(node) {
  var children = [];
  var x = node.childNodes;
  for (var i=0; i < x.length; i++) {
    if (hui.dom.isElement(x[i])) {
      children.push(x[i]);
    }
  }
  return children;
};

hui.get.next = function(element) {
  if (!element) {
    return null;
  }
  if (element.nextElementSibling) {
    return element.nextElementSibling;
  }
  if (!element.nextSibling) {
    return null;
  }
  var next = element.nextSibling;
  while (next && next.nodeType!=1) {
    next = next.nextSibling;
  }
  if (next && next.nodeType==1) {
      return next;
  }
  return null;
};

hui.get.previous = function(element) {
  if (!element) {
    return null;
  }
  if (element.previousElementSibling) {
    return element.previousElementSibling;
  }
  if (!element.previousSibling) {
    return null;
  }
  var previous = element.previousSibling;
  while (previous && previous.nodeType!=1) {
    previous = previous.previousSibling;
  }
  if (previous && previous.nodeType==1) {
      return previous;
  }
  return null;
};

hui.get.before = function(element) {
  var elements = [];
  if (element) {
    var nodes = element.parentNode.childNodes;
    for (var i=0; i < nodes.length; i++) {
      if (nodes[i]==element) {
        break;
      } else if (nodes[i].nodeType===1) {
        elements.push(nodes[i]);
      }
    }
  }
  return elements;
};

/**
 * Find all sibling elements after «element»
 */
hui.get.after = function(element) {
  var elements = [];
  var next = hui.get.next(element);
  while (next) {
    elements.push(next);
    next = hui.get.next(next);
  }
  return elements;
};

hui.get.firstByClass = function(parentElement,className,tag) {
  parentElement = hui.get(parentElement) || document.body;
  if (parentElement.querySelector) {
    return parentElement.querySelector((tag ? tag+'.' : '.')+className);
  } else {
    var children = parentElement.getElementsByTagName(tag || '*');
    for (var i=0;i<children.length;i++) {
      if (hui.cls.has(children[i],className)) {
        return children[i];
      }
    }
  }
  return null;
};

hui.get.byClass = function(parentElement,className,tag) {
  parentElement = hui.get(parentElement) || document.body;
    var i;
  if (parentElement.querySelectorAll) {
    var nl = parentElement.querySelectorAll((tag ? tag+'.' : '.')+className);
    // Important to convert into array...
    var l=[];
    for(i=0, ll=nl.length; i!=ll; l.push(nl[i++]));
    return l;
  } else {
    var children = parentElement.getElementsByTagName(tag || '*'),
    out = [];
    for (i=0;i<children.length;i++) {
      if (hui.cls.has(children[i],className)) {
        out[out.length] = children[i];
      }
    }
    return out;
  }
};

hui.get.byId = function(e,id) {
  var children = e.childNodes;
  for (var i = children.length - 1; i >= 0; i--) {
    if (children[i].nodeType===1 && children[i].getAttribute('id')===id) {
      return children[i];
    } else {
      var found = hui.get.byId(children[i],id);
      if (found) {
        return found;
      }
    }
  }
  return null;
};

/**
 * Find first descendant by tag (excluding self)
 * @param {Element} node The node to start from, will start from body if null
 * @param {String} tag The name of the node to find
 * @returns {Element} The found element or null
 */
hui.get.firstByTag = function(node,tag) {
  node = hui.get(node) || document.body;
  if (node.querySelector && tag!=='*') {
    return node.querySelector(tag);
  }
  var children = node.getElementsByTagName(tag);
  return children[0];
};


hui.get.firstChild = hui.dom.firstChild;

hui.find = function(selector,context) {
  return (context || document).querySelector(selector);
};

hui.findAll = function(selector,context) {
  var nl = (context || document).querySelectorAll(selector);
  return Array.prototype.slice.call(nl);
};

hui.closest = function(selector, context) {
  var parent = context;
  while (parent && parent.nodeType === 1) {
    if (hui.matches(parent, selector)) {
      return parent;
    }
    parent = parent.parentNode;
  }
};

hui.matches = function(node, selector) {
  var docEl = document.documentElement,
    matches = docEl.matches || docEl.webkitMatchesSelector || docEl.mozMatchesSelector || docEl.msMatchesSelector || docEl.oMatchesSelector;
  return matches.call(node, selector);
};

if (!document.querySelector) {
  hui.find = function(selector,context) {
    context = context || document.documentElement;
    if (selector[0] == '.') {
      return hui.get.firstByClass(context,selector.substr(1));
    } else {
      return hui.get.firstByTag(context,selector);
    }
  };
}

hui.collect = function(selectors,context) {
  var copy = {};
  for (var key in selectors) {
    copy[key] = hui.find(selectors[key],context);
  }
  return copy;
};







//////////////////////// Elements ///////////////////////////


/**
 * Builds an element with the «name» and «options»
 *
 * @param {String} name The name of the new element (. adds class)
 * @param {Object} options The options
 * @param {String} options.html Inner HTML
 * @param {String} options.text Inner text
 * @param {String} options.className
 * @param {String} options.class
 * @param {Object} options.style Map of styles (see: hui.style.set)
 * @param {Element} options.parent
 * @param {Element} options.parentFirst
 * @param {Element} options.before
 * @param {Document} doc (Optional) The document to create the element for
 * @returns {Element} The new element
 */
hui.build = function(name,options,doc) {
  doc = doc || document;
  var cls = '';
  if (name.indexOf('.') !== -1) {
    var split = name.split('.');
    name = split[0];
    for (var i = 1; i < split.length; i++) {
      if (i>1) {cls+=' ';}
      cls+=split[i];
    }
  }
  var e = doc.createElement(name);
  if (cls) {
    e.className = cls;
  }
  if (options) {
    for (var prop in options) {
      if (prop=='text') {
        e.appendChild(doc.createTextNode(options.text));
      } else if (prop=='html') {
        e.innerHTML=options.html;
      } else if (prop=='parent' && hui.isDefined(options.parent)) {
        options.parent.appendChild(e);
      } else if (prop=='parentFirst') {
        if (options.parentFirst.childNodes.length === 0) {
          options.parentFirst.appendChild(e);
        } else {
          options.parentFirst.insertBefore(e,options.parentFirst.childNodes[0]);
        }
      } else if (prop=='children') {
        for (var i = 0; i < options.children.length; i++) {
          e.appendChild(options.children[i]);
        }
      } else if (prop=='before') {
        options.before.parentNode.insertBefore(e,options.before);
      } else if (prop=='className') {
        hui.cls.add(e,options.className);
      } else if (prop=='class') {
        hui.cls.add(e,options['class']);
      } else if (prop=='style' && typeof(options[prop])=='object') {
        hui.style.set(e,options[prop]);
      } else if (prop=='style' && (hui.browser.msie7 || hui.browser.msie6)) {
        e.style.setAttribute('cssText',options[prop]);
      } else if (hui.isDefined(options[prop])) {
        e.setAttribute(prop,options[prop]);
      }
    }
  }
  return e;
};








/////////////////////// Position ///////////////////////

/**
 * Functions for getting and changing the position of elements
 * @namespace
 */
hui.position = {
  getTop : function(element) {
    element = hui.get(element);
    if (element) {
      var top = element.offsetTop,
        tempEl = element.offsetParent;
      while (tempEl !== null) {
        top += tempEl.offsetTop;
        tempEl = tempEl.offsetParent;
      }
      return top;
    }
    else return 0;
  },
  getLeft : function(element) {
    element = hui.get(element);
    if (element) {
      var left = element.offsetLeft,
        tempEl = element.offsetParent;
      while (tempEl !== null) {
        left += tempEl.offsetLeft;
        tempEl = tempEl.offsetParent;
      }
      return left;
    }
    else return 0;
  },
  get : function(element) {
    return {
      left : hui.position.getLeft(element),
      top : hui.position.getTop(element)
    };
  },
  getScrollOffset : function(element) {
    element = hui.get(element);
    var top = 0, left = 0;
    do {
      top += element.scrollTop  || 0;
      left += element.scrollLeft || 0;
      element = element.parentNode;
    } while (element);
    return {top:top,left:left};
  },
  /**
   * Place on element relative to another
   * Example hui.position.place({target : {element : «node», horizontal : «0-1»}, source : {element : «node», vertical : «0 - 1»}, insideViewPort:«boolean», viewPortMargin:«integer»})
   */
  place : function(options) {
    var left = 0,
      top = 0,
      src = hui.get(options.source.element),
      trgt = hui.get(options.target.element),
      trgtPos = {left : hui.position.getLeft(trgt), top : hui.position.getTop(trgt) };

    left = trgtPos.left + trgt.clientWidth * (options.target.horizontal || 0);
    top = trgtPos.top + trgt.clientHeight * (options.target.vertical || 0);

    left -= src.clientWidth * (options.source.horizontal || 0);
    top -= src.clientHeight * (options.source.vertical || 0);

    if (options.top) {
      top += options.top;
    }
    if (options.left) {
      left += options.left;
    }

    if (options.insideViewPort) {
      var w = hui.window.getViewWidth();
      if (left + src.clientWidth > w) {
        left = w - src.clientWidth - (options.viewPortMargin || 0);
      }
      if (left < 0) {left=0;}
      if (top < 0) {top=0;}

      var height = hui.window.getViewHeight();
      var vertMax = hui.window.getScrollTop()+hui.window.getViewHeight()-src.clientHeight,
        vertMin = hui.window.getScrollTop();
      top = Math.max(Math.min(top,vertMax),vertMin);
    }
    src.style.top = Math.round(top)+'px';
    src.style.left = Math.round(left)+'px';
  },
  /** Get the remaining height within parent when all siblings has used their height */
  getRemainingHeight : function(element) {
    var height = element.parentNode.clientHeight;
    var siblings = element.parentNode.childNodes;
    for (var i=0; i < siblings.length; i++) {
      var sib = siblings[i];
      if (sib!==element && hui.dom.isElement(siblings[i])) {
        if (hui.style.get(sib,'position')!='absolute') {
          height-=sib.offsetHeight;
        }
      }
    }
    return height;
  }
};







////////////////////// Window /////////////////////

/** @namespace */
hui.window = {
  getScrollTop : function() {
    if (window.pageYOffset) {
      return window.pageYOffset;
    } else if (document.documentElement && document.documentElement.scrollTop) {
      return document.documentElement.scrollTop;
    } else if (document.body) {
      return document.body.scrollTop;
    }
    return 0;
  },
  getScrollLeft : function() {
    if (window.pageYOffset) {
      return window.pageXOffset;
    } else if (document.documentElement && document.documentElement.scrollTop) {
      return document.documentElement.scrollLeft;
    } else if (document.body) {
      return document.body.scrollLeft;
    }
    return 0;
  },
  /**
   * Scroll to an element, will try to show the element in the middle of the screen and only scroll if it makes sence
   * @param {Object} options {element:«the element to scroll to»,duration:«milliseconds»,top:«disregarded pixels at top»}
   */
  scrollTo : function(options) {
    options = hui.override({duration:0,top:0},options);
    var node = options.element;
    var pos = hui.position.get(node);
    var viewTop = hui.window.getScrollTop();
    var viewHeight = hui.window.getViewHeight();
    var viewBottom = viewTop+viewHeight;
    if (viewTop < pos.top + node.clientHeight || (pos.top)<viewBottom) {
      var top = pos.top - Math.round((viewHeight - node.clientHeight) / 2);
      top-= options.top/2;
      top = Math.max(0, top);

      var startTime = new Date().getTime();
      var func;

      func = function() {
        var pos = (new Date().getTime()-startTime)/options.duration;
        if (pos>1 || isNaN(pos)) {
          pos = 1;
        }
        var scrl = viewTop+Math.round((top-viewTop)*hui.ease.fastSlow(pos));
        window.scrollTo(0, scrl);
        if (pos<1) {
          window.setTimeout(func);
        }
      };
      func();
    }
  },
  /**
   * Get the height of the viewport (the visible part of the page)
   */
  getViewHeight : function() {
    if (window.innerHeight) {
      return window.innerHeight;
    } else if (document.documentElement && document.documentElement.clientHeight) {
      return document.documentElement.clientHeight;
    } else if (document.body) {
      return document.body.clientHeight;
    }
  },
  /**
   * Get the width of the viewport (the visible part of the page)
   */
  getViewWidth : function() {
    if (document.documentElement && document.documentElement.clientWidth) {
      return document.documentElement.clientWidth;
    } else if (document.body) {
      return document.body.clientWidth;
    }
    return window.innerWidth;
  }
};













/////////////////////////// Class handling //////////////////////

/** @namespace */
hui.cls = {
  /**
   * Check if an element has a class
   * @param {Element} element The element
   * @param {String} className The class
   * @returns {boolean} true if the element has the class
   */
  has : function(element, className) {
    element = hui.get(element);
    if (!element || !element.className) {
      return false;
    }
    if (element.hasClassName) {
      return element.hasClassName(className);
    }
    if (element.className==className) {
      return true;
    }
    if (element.className.animVal!==undefined) {
      return false; // TODO (handle SVG stuff)
    }
    var a = element.className.split(/\s+/);
    for (var i = 0; i < a.length; i++) {
      if (a[i] == className) {
        return true;
      }
    }
    return false;
  },
  /**
   * Add a class to an element
   * @param {Element} element The element to add the class to
   * @param {String} className The class
   */
  add : function(element, className) {
      element = hui.get(element);
    if (!element) {
      return;
    }
    if (element.classList && className.indexOf(' ') === -1) {
      element.classList.add(className);
    } else if (element.addClassName) {
      element.addClassName(className);
    } else {
      hui.cls.remove(element, className);
      element.className += ' ' + className;
    }
  },
  /**
   * Remove a class from an element
   * @param {Element} element The element to remove the class from
   * @param {String} className The class
   */
  remove : function(element, className) {
    element = hui.get(element);
    if (!element || !element.className) {return;}
    if (element.removeClassName) {
      element.removeClassName(className);
    }
    if (element.className==className) {
      element.className='';
      return;
    }
    var newClassName = '';
    var a = element.className.split(/\s+/);
    for (var i = 0; i < a.length; i++) {
      if (a[i] != className) {
        if (i > 0) {
          newClassName += ' ';
        }
        newClassName += a[i];
      }
    }
    element.className = newClassName;
  },
  /**
   * Add or remove a class from an element
   * @param {Element} element The element
   * @param {String} className The class
   */
  toggle : function(element,className) {
    if (hui.cls.has(element,className)) {
      hui.cls.remove(element,className);
    } else {
      hui.cls.add(element,className);
    }
  },
  /**
   * Add or remove a class from an element
   * @param {Element} element The element
   * @param {String} className The class
   * @param {boolean} add If the class should be added or removed
   */
  set : function(element,className,add) {
    if (add) {
      hui.cls.add(element,className);
    } else {
      hui.cls.remove(element,className);
    }
  }
};




///////////////////// Events //////////////////

hui.on = function(node,event,func,bind) {
  if (arguments.length == 1 && typeof(node) == 'function') {
    return hui.onReady(node);
  }
  if (arguments.length == 2 && typeof(event) == 'function' && hui.isArray(node)) {
    return hui._runOrPostpone.apply(hui, arguments);
  }
  if (event=='tap') {
    if (bind) {
      func = func.bind(bind);
    }
    var moved = false;
    var touched = false;
    hui.listen(node,'touchstart',function() {
      touched = true;
      moved = false;
    },bind);
    hui.listen(node,'touchmove',function() {
      moved = true;
    },bind);
    /*hui.listen(node,'touchcancel',function() {
      hui.log('cancel')
    },bind);*/
    hui.listen(node,'touchend',function(ev) {
      touched = false;
      if (!moved) {
        touched = true;
        hui._callListener(func, ev);
      }
    },bind);
    hui.listen(node,'click',function(ev) {
      if (!moved && !touched) {
        hui._callListener(func, ev);
      }
    },bind);
  } else {
    hui.listen(node,event,func,bind);
  }
};

hui._callListener = function(listener, ev) {
  if (typeof(listener)=='function') {
    listener(ev);
  } else {
    for (var key in listener) {
      if (listener.hasOwnProperty(key)) {
        var found = hui.closest(key, ev.target);
        if (found) {
          listener[key](found, ev);
          return;
        }
      }
    }
  }
};

/**
 * Add an event listener to an element
 * @param {Element} element The element to listen on
 * @param {String} type The event to listen for
 * @param {Function} listener The function to be called
 * @param {object} ?bindTo Bind the listener to it
 */
hui.listen = function(element, type, listener, bindTo) {
  element = hui.get(element);
  if (!element) {
    return;
  }
  var l = listener;
  if (typeof(listener)!=='function') {
    l = function(e) {
      hui._callListener(listener, e);
    }
  }
  else if (bindTo) {
    l = listener.bind(bindTo);
  }
  if(document.addEventListener) {
    element.addEventListener(type, l);
  } else {
    element.attachEvent('on'+type, l);
  }
};

/**
 * Add an event listener to an element, it will only fire once
 * @param {Element} element The element to listen on
 * @param {String} type The event to listen for
 * @param {Function} listener The function to be called
 */
hui.listenOnce = function(element,type,listener) {
  var func = null;
  func = function(e) {
    hui.unListen(element,type,func);
    listener(e);
  };
  hui.listen(element,type,func);
};

/**
 * Remove an event listener from an element
 * @param {Element} element The element to remove listener from
 * @param {String} type The event to remove
 * @param {Function} listener The function to remove
 * @param {boolean} useCapture If the listener should "capture"
 */
hui.unListen = function(el,type,listener,useCapture) {
  el = hui.get(el);
  if(document.removeEventListener) {
    el.removeEventListener(type,listener,useCapture ? true : false);
  } else {
    el.detachEvent('on'+type, listener);
  }
};

/** Creates an event wrapper for an event
 * @param event The DOM event
 * @returns {hui.Event} An event wrapper
 */
hui.event = function(event) {
  if (event!==undefined && event.huiEvent===true) {
    return event;
  }
  return new hui.Event(event);
};

/**
 * Wrapper for events
 * @class
 * @param event {Event} The DOM event
 */
hui.Event = function(event) {
  this.huiEvent = true;
  /** The event */
  this.event = event = event || window.event;

  if (!event) {
    hui.log('No event');
  }

  /** The target element */
  this.element = event.target ? event.target : event.srcElement;
  /** If the shift key was pressed */
  this.shiftKey = event.shiftKey;
  /** If the alt key was pressed */
  this.altKey = event.altKey;
  /** If the command key was pressed */
  this.metaKey = event.metaKey;
  /** If the return key was pressed */
  this.returnKey = event.keyCode==13;
  /** If the escape key was pressed */
  this.escapeKey = event.keyCode==27;
  /** If the space key was pressed */
  this.spaceKey = event.keyCode==32;
  /** If the backspace was pressed */
  this.backspaceKey = event.keyCode==8;
  /** If the up key was pressed */
  this.upKey = event.keyCode==38;
  /** If the down key was pressed */
  this.downKey = event.keyCode==40;
  /** If the left key was pressed */
  this.leftKey = event.keyCode==37;
  /** If the right key was pressed */
  this.rightKey = event.keyCode==39;
  /** The key code */
  this.keyCode = event.keyCode;
};

hui.Event.prototype = {
  /**
   * Get the left coordinate
   * @returns {Number} The left coordinate
   * @type {Number}
   */
  getLeft : function() {
    var left = 0;
    if (this.event) {
      if (this.event.pageX) {
        left = this.event.pageX;
      } else if (this.event.clientX) {
        left = this.event.clientX + hui.window.getScrollLeft();
      }
    }
    return left;
  },
  /**
   * Get the top coordinate
   * @returns {Number} The top coordinate
   */
  getTop : function() {
    var top = 0;
    if (this.event) {
      if (this.event.pageY) {
        top = this.event.pageY;
      } else if (this.event.clientY) {
        top = this.event.clientY + hui.window.getScrollTop();
      }
    }
    return top;
  },
  /** Get the node the event originates from
   * @returns {ELement} The originating element
   */
  getElement : function() {
    return this.element;
  },
  /** Finds the nearest ancester with a certain class name
   * @param cls The css class name
   * @returns {Element} The found element or null
   */
  findByClass : function(cls) {
    return hui.closest('.' + cls, this.element);
  },
  /** Finds the nearest ancester with a certain tag name
   * @param tag The tag name
   * @returns {Element} The found element or null
   */
  findByTag : function(tag) {
    return hui.closest(tag, this.element);
  },
  /* @Deprecated */
  find : function(func) {
    return this.closest(func);
  },
  closest : function(func) {
    if (hui.isString(func)) {
      return hui.closest(func, this.element);
    }
    var parent = this.element;
    while (parent) {
      if (func(parent)) {
        return parent;
      }
      parent = parent.parentNode;
    }
    return null;
  },
  isDescendantOf : function(node) {
    var parent = this.element;
    while (parent) {
      if (parent===node) {
        return true;
      }
      parent = parent.parentNode;
    }
    return false;
  },
  /** Stops the event from propagating */
  stop : function() {
    hui.stop(this.event);
  },
  prevent : function() {
    if (this.event.preventDefault) {
      this.event.preventDefault();
    }
  }
};

/**
 * Stops an event from propagating
 * @param event A standard DOM event, NOT an hui.Event
 */
hui.stop = function(event) {
  if (!event) {event = window.event;}
  if (event.stopPropagation) {event.stopPropagation();}
  if (event.preventDefault) {event.preventDefault();}
  event.cancelBubble = true;
  event.stopped = true;
};

hui._ = hui._ || [];

hui._ready = document.readyState == 'complete';// || document.readyState;
// TODO Maybe interactive is too soon???

hui.onReady = function() {
  if (hui._ready) {
    hui._runOrPostpone.apply(hui, arguments);
  } else {
    hui._.push(arguments);
  }
};

hui.onDraw = function(func) {
  window.setTimeout(func,13);
};

hui.onDraw = (function(vendors,window) {
  var found = window.requestAnimationFrame;
  for(var x = 0; x < vendors.length && !found; ++x) {
      found = window[vendors[x]+'RequestAnimationFrame'];
  }
  return found ? found.bind(window) : hui.onDraw;
})(['ms', 'moz', 'webkit', 'o'],window);

/**
 * Execute a function when the DOM is ready
 * @param delegate The function to execute
 */
hui._onReady = function(delegate) {
  if (document.readyState == 'interactive') {
    window.setTimeout(delegate);
  }
  else if (window.addEventListener) {
    window.addEventListener('DOMContentLoaded',delegate,false);
  }
  else if (window.attachEvent) {
    document.attachEvent("onreadystatechange", function(){
      if(document.readyState === "complete"){
        document.detachEvent("onreadystatechange", arguments.callee);
        delegate();
      }
    });
  }
};







///////////////////////// Request /////////////////////////

/**
 * Send a HTTP request
 * <pre><strong>options:</strong> {
 *  method : «'<strong>POST</strong>' | 'get' | 'rEmOVe'»,
 *  async : <strong>true</strong>,
 *  headers : {<strong>Ajax : true</strong>, header : 'value'},
 *  file : «HTML5-file»,
 *  files : «HTML5-files»,
 *  parameters : {key : 'value'},
 *
 *  $success : function(transport) {
 *    // when status is 200
 *  },
 *  $forbidden : function(transport) {
 *    // when status is 403
 *  },
 *  $abort : function(transport) {
 *    // when request is aborted
 *  },
 *  $failure : function(transport) {
 *    // when status is not 200 (if status is 403 and $forbidden is set then $failure will not be called)
 *  },
 *  $exception : function(exception,transport) {
 *    // When an exception has occurred while calling on«Something», If not set the exception will be thrown
 *  },
 *  $progress : function(current,total) {
 *    // Progress for file uploads (maybe also other requests?)
 *  },
 *  $load : functon() {
 *    // When file upload is transfered?
 *  }
 *}
 * </pre>
 *
 * @param options The options
 * @returns {XMLHttpRequest} The transport
 */
hui.request = function(options) {
  options = hui.override({
    method : 'POST',
    async : true,
    headers : {Accept : 'application/json'}
  },options);
  var transport = new XMLHttpRequest();
  if (options.credentials) {
    transport.withCredentials = true;
  }
  if (!transport) {return;}
  transport.onreadystatechange = function() {
    if (transport.readyState == 4) {
      if (transport.status == 200 && options.$success) {
        options.$success(transport);
      } else if (transport.status == 403 && options.$forbidden) {
        options.$forbidden(transport);
      } else if (transport.status !== 0 && options.$failure) {
        options.$failure(transport);
      } else if (transport.status === 0 && options.$abort) {
        options.$abort(transport);
      }
      if (options.$finally) {
        options.$finally();
      }
    }
  };
  var method = options.method.toUpperCase();
  transport.open(method, options.url, options.async);
  var body = null;
  if (method=='POST' && options.file) {
    if (false) {
      body = options.file;
      transport.setRequestHeader("Content-type", options.file.type);
      transport.setRequestHeader("X_FILE_NAME", options.file.name);
    } else {
      body = new FormData();
      body.append('file', options.file);
      if (options.parameters) {
        for (var param in options.parameters) {
          var value = options.parameters[param];
          if (hui.isArray(value)) {
            for (var i = 0; i < value.length; i++) {
              body.append(param, value[i]);
            }
          } else {
            body.append(param, value);
          }
        }
      }
    }
    if (options.$progress) {
      transport.upload.addEventListener("progress", function(e) {
        options.$progress(e.loaded,e.total);
      }, false);
    }
    if (options.$load) {
      transport.upload.addEventListener("load", function(e) {
        options.$load();
      }, false);
    }
  } else if (method=='POST' && options.files) {
    body = new FormData();
    //form.append('path', '/');
    for (var j = 0; j < options.files.length; j++) {
      body.append('file'+j, options.files[j]);
    }
  } else if (options.parameters) {
    body = hui.request._buildPostBody(options.parameters);
    transport.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=utf-8");
  } else {
    body = '';
  }
  if (options.headers) {
    for (var name in options.headers) {
      transport.setRequestHeader(name, options.headers[name]);
    }
  }
  transport.send(body);
  //hui.request._transports.push(transport);
  //hui.log('Add: '+hui.request._transports.length);
  return transport;
};

/*
hui.request._transports = [];

hui.request._forget = function(t) {
  hui.log('Forget: '+hui.request._transports.length);
  hui.array.remove(hui.request._transports, t);
}

hui.request.abort = function() {
  window.stop();
  for (var i = hui.request._transports.length - 1; i >= 0; i--){
    hui.log('aborting: '+hui.request._transports[i].readyState)
    hui.request._transports[i].abort();
  };
}*/

/**
 * Check if a http request has a valid XML response
 * @param {XMLHttpRequest} t The request
 * @return true if a valid XML request exists
 */
hui.request.isXMLResponse = function(t) {
  return t.responseXML && t.responseXML.documentElement && t.responseXML.documentElement.nodeName!='parsererror';
};

hui.request._buildPostBody = function(parameters) {
  if (!parameters) return null;
  var output = '',
    param, i;
    if (hui.isArray(parameters)) {
      for (i = 0; i < parameters.length; i++) {
        param = parameters[i];
        if (i > 0) {output += '&';}
        output+=encodeURIComponent(param.name)+'=';
        if (param.value!==undefined && param.value!==null) {
          output+=encodeURIComponent(param.value);
        }
      }
    } else {
      for (param in parameters) {
        var value = parameters[param];
        if (hui.isArray(value)) {
          for (i = 0; i < value.length; i++) {
            if (output.length > 0) {output += '&';}
            output += encodeURIComponent(param)+'=';
            if (value[i]!==undefined && value[i]!==null) {
              output += encodeURIComponent(value[i]);
            }
          }
        } else {
          if (output.length > 0) {output += '&';}
          output += encodeURIComponent(param)+'=';
          if (value!==undefined && value!==null) {
            output += encodeURIComponent(value);
          }
        }
      }
    }
  return output;
};

///////////////////// Style ///////////////////

/** @namespace */
hui.style = {
  /**
   * Copy the style from one element to another
   * @param source The element to copy from
   * @param target The element to copy to
   * @param styles An array of properties to copy
   */
  copy : function(source,target,styles) {
    for (var i=0; i < styles.length; i++) {
      var property = styles[i];
      var value = hui.style.get(source,property);
      if (value) {
        target.style[hui.string.camelize(property)] = value;
      }
    }
  },
  set : function(element,styles) {
    for (var style in styles) {
      if (style==='transform') {
        element.style.webkitTransform = styles[style];
      } else if (style==='opacity') {
        hui.style.setOpacity(element,styles[style]);
      } else {
        element.style[style] = styles[style];
      }
    }
  },
  /**
   * Get the computed style of an element
   * @param {Element} element The element
   * @param {String} style The CSS property in the form font-size NOT fontSize;
   */
  get : function(element, style) {
    element = hui.get(element);
    var cameled = hui.string.camelize(style);
    var value = element.style[cameled];
    if (!value) {
      if (document.defaultView && document.defaultView.getComputedStyle) {
        var css = document.defaultView.getComputedStyle(element, null);
        value = css ? css.getPropertyValue(style) : null;
      } else if (element.currentStyle) {
        value = element.currentStyle[cameled];
      }
    }
    return value == 'auto' ? '' : value;
  },
  /** Cross browser way of setting opacity */
  setOpacity : function(element,opacity) {
    if (!hui.browser.opacity) {
      if (opacity==1) {
        element.style.filter = null;
      } else {
        element.style.filter = 'alpha(opacity='+(opacity*100)+')';
      }
    } else {
      element.style.opacity = opacity;
    }
  },
  length : function(value) {
    if (typeof(value) === 'number') {
      return value + 'px';
    }
    return value;
  }
};






//////////////////// Frames ////////////////////

/** @namespace */
hui.frame = {
  /**
   * Get the document object of a frame
   * @param frame The frame to get the document from
   */
  getDocument : function(frame) {
    if (frame.contentDocument) {
      return frame.contentDocument;
    } else if (frame.contentWindow) {
      return frame.contentWindow.document;
    } else if (frame.document) {
      return frame.document;
    }
  },
  /**
   * Get the window object of a frame
   * @param frame The frame to get the window from
   */
  getWindow : function(frame) {
    if (frame.defaultView) {
      return frame.defaultView;
    } else if (frame.contentWindow) {
      return frame.contentWindow;
    }
  }
};






/////////////////// Selection /////////////////////

/** @namespace */
hui.selection = {
  /** Clear the text selection */
  clear : function() {
    var sel ;
    if(document.selection && document.selection.empty ){
      document.selection.empty() ;
    } else if(window.getSelection) {
      sel=window.getSelection();
      if(sel && sel.removeAllRanges) {
        sel.removeAllRanges() ;
      }
    }
  },
  /** Get the selected text
   * @param doc The document, defaults to current document
   */
  getText : function(doc) {
    doc = doc || document;
    if (doc.getSelection) {
      return doc.getSelection()+'';
    } else if (doc.selection) {
      return doc.selection.createRange().text;
    }
    return '';
  },
  getNode : function(doc) {
    doc = doc || document;
    if (doc.getSelection) {
      var range = doc.getSelection().getRangeAt(0);
      if (typeof(range.commonAncestorContainer) == 'function') {
        return range.commonAncestorContainer(); // TODO Not sure why?
      }
      return range.commonAncestorContainer;
    }
    return null;
  },
  get : function(doc) {
    return {
      node : hui.selection.getNode(doc),
      text : hui.selection.getText(doc)
    };
  },
  enable : function(on) {
    document.onselectstart = on ? null : function () { return false; };
    document.body.style.webkitUserSelect = on ? null : 'none';
  }
};





/////////////////// Effects //////////////////////

/** @namespace */
hui.effect = {
  makeFlippable : function(options) {
    if (hui.browser.webkit) {
      hui.cls.add(options.container,'hui_flip_container');
      hui.cls.add(options.front,'hui_flip_front');
      hui.cls.add(options.back,'hui_flip_back');
    } else {
      hui.cls.add(options.front,'hui_flip_front_legacy');
      hui.cls.add(options.back,'hui_flip_back_legacy');
    }
  },
  flip : function(options) {
    if (!hui.browser.webkit) {
      hui.cls.toggle(options.element,'hui_flip_flipped_legacy');
    } else {
      var element = hui.get(options.element);
      var duration = options.duration || '1s';
      var front = hui.get.firstByClass(element,'hui_flip_front');
      var back = hui.get.firstByClass(element,'hui_flip_back');
      front.style.webkitTransitionDuration=duration;
      back.style.webkitTransitionDuration=duration;
      hui.cls.toggle(options.element,'hui_flip_flipped');
    }
  },
  /**
   * Reveal an element using a bounce/zoom effect
   * @param {Object} options {element:«Element»}
   */
  bounceIn : function(options) {
    var node = options.element;
    if (hui.browser.msie) {
      hui.style.set(node,{'display':'block',visibility:'visible'});
    } else {
      hui.style.set(node,{'display':'block','opacity':0,visibility:'visible'});
      hui.animate(node,'transform','scale(0.1)',0);// rotate(10deg)
      window.setTimeout(function() {
        hui.animate(node,'opacity',1,300);
        hui.animate(node,'transform','scale(1)',400,{ease:hui.ease.backOut}); // rotate(0deg)
      });
    }
  },
  /**
   * Fade an element in - making it visible
   * @param {Object} options {element : «Element», duration : «milliseconds», delay : «milliseconds», $complete : «Function» }
   */
  fadeIn : function(options) {
    var node = options.element;
    if (hui.style.get(node,'display')=='none') {
      hui.style.set(node,{opacity : 0,display : 'inherit'});
    }
    hui.animate({
      node : node,
      css : { opacity : 1 },
      delay : options.delay || null,
      duration : options.duration || 500,
      $complete : options.onComplete || options.$complete
    });
  },
  /**
   * Fade an element out - making it invisible
   * @param {Object} options {element : «Element», duration : «milliseconds», delay : «milliseconds», $complete : «Function» }
   */
  fadeOut : function(options) {
    hui.animate({
      node : options.element,
      css : { opacity : 0 },
      delay : options.delay || null,
      duration : options.duration || 500,
      hideOnComplete : true,
      complete : options.onComplete || options.$complete
    });
  },
  /**
   * Make an element wiggle
   * @param {Object} options {element : «Element», duration : «milliseconds» }
   */
  wiggle : function(options) {
    var e = hui.ui.getElement(options.element);
    hui.cls.add(options.element,'hui_effect_wiggle');
    window.setTimeout(function() {
      hui.cls.remove(options.element,'hui_effect_wiggle');
    },options.duration || 1000);

  },
  /**
   * Make an element shake
   * @param {Object} options {element : «Element», duration : «milliseconds» }
   */
  shake : function(options) {
    this._do(options.element,'hui_effect_shake',options.duration || 1000);
  },
  /**
   * Make an element shake
   * @param {Object} options {element : «Element», duration : «milliseconds» }
   */
  tada : function(options) {
    this._do(options.element,'hui_effect_tada',1000);
  },
  _do : function(e,cls,time) {
    e = hui.ui.getElement(e);
    hui.cls.add(e,cls);
    window.setTimeout(function() {
      hui.cls.remove(e,cls);
    },time);
  }
};








/////////////////////////////// Drag ///////////////////////////

/** @namespace */
hui.drag = {
  /** Register dragging on an element
   * <pre><strong>options:</strong> {
   *  element : «Element»
   *  <em>see hui.drag.start for more options</em>
   * }
   * @param {Object} options The options
   * @param {Element} options.element The element to attach to
   */
  attach : function(options) {
    var touch = options.touch && hui.browser.touch;
    hui.listen(options.element,touch ? 'touchstart' : 'mousedown',function(e) {
      e = hui.event(e);
      // TODO This shuould be a hui.Event
      if (options.$check && options.$check(e)===false) {
        return;
      }
      e.stop();
      hui.drag.start(options,e);
    });
  },
  register : function(options) {
    this.attach(options);
  },
  /** Start dragging
   * <pre><strong>options:</strong> {
   *  onBeforeMove ($firstMove) : function(event), // Called when the cursor moves for the first time
   *  onMove ($move): function(event), // Called when the cursor moves
   *  onAfterMove ($didMove): function(event), // Called if the cursor has moved
   *  onNotMoved ($notMoved): function(event), // Called if the cursor has not moved
   *  onEnd ($finally) : function(event), // Called when the mouse is released, even if the cursor has not moved
   * }
   * @param {Object} options The options
   * @param {function} options.$before When the user starts interacting (mousedown/touchstart)
   * @param {function} options.$startMove When the user starts moving (before first move - called once)
   * @param {function} options.$move When the user moves
   * @param {function} options.$endMove After a move has finished
   * @param {function} options.$notMoved If the user started interacting but did not move (maybe treat as click/tap)
   * @param {function} options.$finally After everything - moved or not
   */
  start : function(options,e) {
    var win = options.window || window;
    var root = window.document.body.parentNode;
    var target = hui.browser.msie ? win.document : win;
    var touch = options.touch && hui.browser.touch;
    if (options.$before) options.$before();
    if (options.onStart) options.onStart();
    var latest = {
      x: e.getLeft(),
      y: e.getTop(),
      time: Date.now()
    };
    var initial = latest;
    var mover,
      upper,
      moved = false;
    mover = function(e) {
      e = hui.event(e);
      e.stop(e);
      if (!moved) {
        if (options.onBeforeMove) options.onBeforeMove(e); // TODO: deprecated
        if (options.$startMove) options.$startMove(e);
      }
      moved = true;
      if (options.onMove) options.onMove(e);
      if (options.$move) options.$move(e);
    }.bind(this);
    hui.listen(root,touch ? 'touchmove' : 'mousemove',mover);
    upper = function(e) {
      hui.unListen(root,touch ? 'touchmove' : 'mousemove',mover);
      hui.unListen(target,touch ? 'touchend' : 'mouseup',upper);
      if (options.onEnd) options.onEnd(); // TODO: deprecated
      if (moved) {
        if (options.onAfterMove) options.onAfterMove(e); // TODO: deprecated
        if (options.$endMove) options.$endMove(e);
      } else {
        if (options.onNotMoved) options.onNotMoved(e); // TODO: deprecated
        if (options.$notMoved) options.$notMoved(e);
      }
      if (options.$finally) options.$finally();
      hui.selection.enable(true);
    }.bind(this);
    hui.listen(target,touch ? 'touchend' : 'mouseup',upper);
    hui.selection.enable(false);
  },

  _nativeListeners : [],

  _activeDrop : null,

  /** Listen for native drops
   * <pre><strong>options:</strong> {
   *  elements : «Element»,
   *  hoverClass : «String»,
   *  $drop : function(event),
   *  $dropFiles : function(fileArray),
   *  $dropURL : function(url),
   *  $dropText : function(url)
   * }
   * @param {Object} options The options
   */
  listen : function(options) {
    if (hui.browser.msie) {
      return;
    }
    hui.drag._nativeListeners.push(options);
    if (hui.drag._nativeListeners.length>1) {
            return;
        }
    hui.listen(document.body,'dragenter',function(e) {
      var l = hui.drag._nativeListeners;
      var found = null;
      for (var i=0; i < l.length; i++) {
        var lmnt = l[i].element;
        if (hui.dom.isDescendantOrSelf(e.target,lmnt)) {
          found = l[i];
          if (hui.drag._activeDrop === null || hui.drag._activeDrop != found) {
            hui.cls.add(lmnt,found.hoverClass);
          }
          break;
        }
      }
      if (hui.drag._activeDrop) {
        //var foundElement = found ? found.element : null;
        if (hui.drag._activeDrop!=found) {
          hui.cls.remove(hui.drag._activeDrop.element,hui.drag._activeDrop.hoverClass);
          if (hui.drag._activeDrop.$leave) {
            hui.drag._activeDrop.$leave(e);
          }
        } else if (hui.drag._activeDrop.$hover) {
          hui.drag._activeDrop.$hover(e);
        }

      }
      hui.drag._activeDrop = found;
    });

    hui.listen(document.body,'dragover',function(e) {
      hui.stop(e);
      if (hui.drag._activeDrop) {
        if (hui.drag._activeDrop.$hover) {
          hui.drag._activeDrop.$hover(e);
        }
      }
    });

    hui.listen(document.body,'dragend',function(e) {
      hui.log('drag end');
    });

    hui.listen(document.body,'dragstart',function(e) {
      hui.log('drag start');
    });

    hui.listen(document.body,'drop',function(e) {
      var event = hui.event(e);
      event.stop();
      var options = hui.drag._activeDrop;
      hui.drag._activeDrop = null;
      if (options) {
        hui.cls.remove(options.element,options.hoverClass);
        if (options.$drop) {
          options.$drop(e,{event:event});
        }
        if (e.dataTransfer) {
          hui.log(e.dataTransfer.types);
          if (options.$dropFiles && e.dataTransfer.files && e.dataTransfer.files.length>0) {
            options.$dropFiles(e.dataTransfer.files,{event:event});
          } else if (options.$dropURL && e.dataTransfer.types !== null && (hui.array.contains(e.dataTransfer.types,'public.url') || hui.array.contains(e.dataTransfer.types,'text/uri-list'))) {
            var url = e.dataTransfer.getData('public.url');
            var uriList = e.dataTransfer.getData('text/uri-list');
            if (url && !hui.string.startsWith(url,'data:')) {
              options.$dropURL(url,{event:event});
            } else if (uriList && !hui.string.startsWith(url,'data:')) {
              options.$dropURL(uriList,{event:event});
            }
          } else if (options.$dropText && e.dataTransfer.types !== null && hui.array.contains(e.dataTransfer.types,'text/plain')) {
            options.$dropText(e.dataTransfer.getData('text/plain'),{event:event});
          }
        }
      }
    });
  }
};


///////////////////////// Location /////////////////////

/** @namespace */
hui.location = {
  /** Get an URL parameter */
  getParameter : function(name) {
    var parms = hui.location.getParameters();
    for (var i=0; i < parms.length; i++) {
      if (parms[i].name==name) {
        return parms[i].value;
      }
    }
    return null;
  },
  /** Set an URL parameter - initiates a new request */
  setParameter : function(name,value) {
    var parms = hui.location.getParameters();
    var found = false;
    for (var i=0; i < parms.length; i++) {
      if (parms[i].name==name) {
        parms[i].value=value;
        found=true;
        break;
      }
    }
    if (!found) {
      parms.push({name:name,value:value});
    }
    hui.location.setParameters(parms);
  },
  /** Checks if the URL has a certain hash */
  hasHash : function(name) {
    var h = document.location.hash;
    if (h!=='') {
      return h=='#'+name;
    }
    return false;
  },
  getHash : function() {
    var h = document.location.hash;
    if (h!=='') {
      return h.substring(1);
    }
    return null;
  },
  /** Gets a hash parameter (#name=value&other=text) */
  getHashParameter : function(name) {
    var h = document.location.hash;
    if (h!=='') {
      var i = h.indexOf(name+'=');
      if (i!==-1) {
        var remaining = h.substring(i+name.length+1);
        if (remaining.indexOf('&')!==-1) {
          return remaining.substring(0,remaining.indexOf('&'));
        }
        return remaining;
      }
    }
    return null;
  },
  /** Clears the URL hash */
  clearHash : function() {
    document.location.hash='#';
  },
  /** Sets a number of parameters
   * @param params {Array} Parameters [{name:'hep',value:'hey'}]
   */
  setParameters : function(parms) {
    var query = '';
    for (var i=0; i < parms.length; i++) {
      query+= i === 0 ? '?' : '&';
      query+=parms[i].name+'='+parms[i].value;
    }
    document.location.search=query;
  },
  /** Checks if a parameter exists with the value 'true' or 1 */
  getBoolean : function(name) {
    var value = hui.location.getParameter(name);
    return (value=='true' || value=='1');
  },
  /** Checks if a parameter exists with the value 'true' or 1 */
  getInt : function(name) {
    var value = parseInt(hui.location.getParameter(name));
    if (!isNaN(value)) {
      return value;
    }
    return null;
  },
  /** Gets all parameters as an array like : [{name:'hep',value:'hey'}] */
  getParameters : function() {
    var items = document.location.search.substring(1).split('&');
    var parsed = [];
    for( var i = 0; i < items.length; i++) {
      var item = items[i].split('=');
      var name = unescape(item[0]).replace(/^\s*|\s*$/g,"");
      var value = unescape(item[1]).replace(/^\s*|\s*$/g,"");
      if (name) {
        parsed.push({name:name,value:value});
      }
    }
    return parsed;
  }
};

/**
 * Run through all postponed actions in "_" and remove it afterwards
 */
hui._onReady(function() {
  hui._ready = true;
  for (var i = 0; i < hui._.length; i++) {
    var item = hui._[i];
    if (typeof(item) === 'function') {
      item = [item];
    }
    hui._runOrPostpone.apply(hui, item);
  }
  delete hui._;
});